fix: enable sub page block (#6595)

* fix: enable sub page block

* fix: open newly inserted page

* fix: created view should have empty name

* test: use secondary to rename page

* fix: make popover secondary interaction better

* test: amend test

* fix: icon color of sub page block

* test: fix tests

* test: fix hover issue

* feat: clean API for show at cursor on popover
This commit is contained in:
Mathias Mogensen 2024-10-30 22:29:01 +01:00 committed by GitHub
parent d5c1955ea3
commit af6736d352
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 275 additions and 235 deletions

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -73,10 +74,7 @@ void main() {
);
await tester.pumpAndSettle();
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@ -99,10 +97,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionAddButton([0], false);
@ -155,10 +150,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionAddButton([0], false);
@ -216,10 +208,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor
@ -260,10 +249,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor
@ -313,10 +299,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@ -326,6 +309,11 @@ void main() {
expect(find.text('Child page'), findsNothing);
expect(find.byType(SubPageBlockComponent), findsNothing);
// Since there is no selection active in editor before deleting Node,
// we need to give focus back to the editor
await tester.editor
.updateSelection(Selection.collapsed(Position(path: [0])));
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyZ,
isControlPressed: Platform.isLinux || Platform.isWindows,
@ -354,10 +342,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
// Delete
@ -405,15 +390,16 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
await tester.hoverOnPageName('Child page');
await tester.tapDeletePageButton();
await tester.hoverOnPageName(
'Child page',
onHover: () async {
await tester.tapDeletePageButton();
},
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('Child page'), findsNothing);
@ -432,10 +418,7 @@ void main() {
layout: ViewLayoutPB.Document,
);
await tester.hoverOnPageName(_defaultPageName);
await tester.renamePage('Child page');
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
await tester.editor.hoverAndClickOptionMenuButton([0]);
@ -498,4 +481,16 @@ extension _SubPageTestHelper on WidgetTester {
await pumpUntilFound(find.byType(SubPageBlockComponent));
}
Future<void> renamePageWithSecondary(
String currentName,
String newName,
) async {
await hoverOnPageName(currentName, onHover: () async => pumpAndSettle());
await rightClickOnPageName(currentName);
await tapButtonWithName(ViewMoreActionType.rename.name);
await enterText(find.byType(TextFormField), newName);
await tapOKButton();
await pumpAndSettle();
}
}

View File

@ -9,6 +9,7 @@ import 'document_with_date_reminder_test.dart'
as document_with_date_reminder_test;
import 'document_with_toggle_heading_block_test.dart'
as document_with_toggle_heading_block_test;
import 'document_sub_page_test.dart' as document_sub_page_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -21,6 +22,5 @@ void main() {
document_option_action_test.main();
document_inline_sub_page_test.main();
document_with_toggle_heading_block_test.main();
// Disable subPage test temporarily, enable it in version 0.7.2
// document_sub_page_test.main();
document_sub_page_test.main();
}

View File

@ -19,8 +19,13 @@ void main() {
await tester.tapAnonymousSignInButton();
// Right click on the view item and change icon
await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton);
await tester.pumpAndSettle();
await tester.hoverOnWidget(
find.byType(ViewItem),
onHover: () async {
await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton);
await tester.pumpAndSettle();
},
);
// Change icon
final changeIconButton =

View File

@ -192,8 +192,13 @@ extension CommonOperations on WidgetTester {
ViewLayoutPB layout = ViewLayoutPB.Document,
}) async {
final page = findPageName(name, layout: layout);
await tap(page, buttons: kSecondaryMouseButton);
await pumpAndSettle();
await hoverOnPageName(
name,
onHover: () async {
await tap(page, buttons: kSecondaryMouseButton);
await pumpAndSettle();
},
);
}
/// open the page with given name.

View File

@ -401,8 +401,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
dateOrReminderSlashMenuItem,
photoGallerySlashMenuItem,
fileSlashMenuItem,
// disable subPageSlashMenuItem temporarily, enable it in version 0.7.2
// subPageSlashMenuItem,
subPageSlashMenuItem,
];
}

View File

@ -13,6 +13,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
@ -248,17 +249,17 @@ class SubPageBlockComponentState extends State<SubPageBlockComponent>
view.icon.value,
fontSize: textStyle.fontSize,
lineHeight: textStyle.height,
color:
AFThemeExtension.of(context).strongText,
)
: Opacity(
opacity: 0.6,
child: view.defaultIcon(),
),
const HSpace(10),
: view.defaultIcon(),
const HSpace(6),
Flexible(
child: FlowyText(
view.nameOrDefault,
fontSize: textStyle.fontSize,
fontWeight: textStyle.fontWeight,
decoration: TextDecoration.underline,
lineHeight: textStyle.height,
overflow: TextOverflow.ellipsis,
),

View File

@ -1,3 +1,5 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:flutter/widgets.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -96,9 +98,9 @@ class SubPageTransactionHandler extends BlockTransactionHandler {
// This is a new Node, we need to create the view
final viewOrResult = await ViewBackendService.createView(
name: '',
layoutType: ViewLayoutPB.Document,
parentViewId: parentViewId,
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
await viewOrResult.fold(
@ -111,6 +113,9 @@ class SubPageTransactionHandler extends BlockTransactionHandler {
options: const ApplyOptions(recordUndo: false),
);
editorState.reload();
// Open view
getIt<TabsBloc>().openPlugin(view);
},
(error) async {
Log.error(error);
@ -131,10 +136,7 @@ class SubPageTransactionHandler extends BlockTransactionHandler {
_beingCreated.remove(node.id);
} else if (isPaste) {
// final wasCut = node.attributes[SubPageBlockKeys.wasCut];
if (isCut && parentViewId != null) {
// Just in case, we try to put back from trash before moving
await TrashService.putback(viewId);
final viewOrResult = await ViewBackendService.moveViewV2(

View File

@ -131,11 +131,11 @@ class _EditorTransactionServiceState extends State<EditorTransactionService> {
final Map<String, dynamic> added = {
for (final handler in _transactionHandlers)
handler.type: handler.livesInDelta ? <MentionBlockData>[] : [],
handler.type: handler.livesInDelta ? <MentionBlockData>[] : <Node>[],
};
final Map<String, dynamic> removed = {
for (final handler in _transactionHandlers)
handler.type: handler.livesInDelta ? <MentionBlockData>[] : [],
handler.type: handler.livesInDelta ? <MentionBlockData>[] : <Node>[],
};
for (final op in event.$2.operations) {

View File

@ -29,6 +29,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
@ -477,6 +478,8 @@ class SingleInnerViewItem extends StatefulWidget {
class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
final controller = PopoverController();
final viewMoreActionController = PopoverController();
bool isIconPickerOpened = false;
@override
@ -545,12 +548,13 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
children.add(
_buildViewMoreActionButton(
context,
(popover) => FlowyTooltip(
viewMoreActionController,
(_) => FlowyTooltip(
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
child: FlowyIconButton(
width: 24,
icon: const FlowySvg(FlowySvgs.workspace_three_dots_s),
onPressed: popover.show,
onPressed: viewMoreActionController.show,
),
),
),
@ -574,13 +578,19 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
height: widget.height,
child: Padding(
padding: EdgeInsets.only(left: widget.level * widget.leftPadding),
child: widget.enableRightClickContext
? _buildViewMoreActionButton(
context,
showAtCursor: true,
(_) => Row(children: children),
)
: Row(children: children),
child: Listener(
onPointerDown: (event) {
if (event.buttons == kSecondaryMouseButton &&
widget.enableRightClickContext) {
viewMoreActionController.showAt(
// We add some horizontal offset
event.position + const Offset(4, 0),
);
}
},
behavior: HitTestBehavior.opaque,
child: Row(children: children),
),
),
),
);
@ -710,9 +720,9 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
// ··· more action button
Widget _buildViewMoreActionButton(
BuildContext context,
Widget Function(PopoverController) buildChild, {
bool showAtCursor = false,
}) {
PopoverController controller,
Widget Function(PopoverController) buildChild,
) {
return BlocProvider(
create: (context) => SpaceBloc(
userProfile: context.read<SpaceBloc>().userProfile,
@ -720,9 +730,9 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
)..add(const SpaceEvent.initial(openFirstPage: false)),
child: ViewMoreActionPopover(
view: widget.view,
controller: controller,
isExpanded: widget.isExpanded,
spaceType: widget.spaceType,
showAtCursor: showAtCursor,
onEditing: (value) =>
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
buildChild: buildChild,

View File

@ -17,35 +17,38 @@ class ViewMoreActionPopover extends StatelessWidget {
const ViewMoreActionPopover({
super.key,
required this.view,
this.controller,
required this.onEditing,
required this.onAction,
required this.spaceType,
required this.isExpanded,
this.showAtCursor = false,
required this.buildChild,
this.showAtCursor = false,
});
final ViewPB view;
final PopoverController? controller;
final void Function(bool value) onEditing;
final void Function(ViewMoreActionType type, dynamic data) onAction;
final FolderSpaceType spaceType;
final bool isExpanded;
final bool showAtCursor;
final Widget Function(PopoverController) buildChild;
final bool showAtCursor;
@override
Widget build(BuildContext context) {
final wrappers = _buildActionTypeWrappers();
return PopoverActionList<ViewMoreActionTypeWrapper>(
controller: controller,
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 8),
actions: wrappers,
constraints: const BoxConstraints(minWidth: 260),
showAtCursor: showAtCursor,
onPopupBuilder: () => onEditing(true),
buildChild: buildChild,
onSelected: (_, __) {},
onClosed: () => onEditing(false),
showAtCursor: showAtCursor,
);
}

View File

@ -7,6 +7,7 @@ import 'package:styled_widget/styled_widget.dart';
class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
const PopoverActionList({
super.key,
this.controller,
this.popoverMutex,
required this.actions,
required this.buildChild,
@ -31,6 +32,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
this.showAtCursor = false,
});
final PopoverController? controller;
final PopoverMutex? popoverMutex;
final List<T> actions;
final Widget Function(PopoverController) buildChild;
@ -56,14 +58,26 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
class _PopoverActionListState<T extends PopoverAction>
extends State<PopoverActionList<T>> {
final PopoverController popoverController = PopoverController();
late PopoverController popoverController =
widget.controller ?? PopoverController();
@override
void dispose() {
popoverController.close();
if (widget.controller == null) {
popoverController.close();
}
super.dispose();
}
@override
void didUpdateWidget(covariant PopoverActionList<T> oldWidget) {
if (widget.controller != oldWidget.controller) {
popoverController.close();
popoverController = widget.controller ?? PopoverController();
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
final child = widget.buildChild(popoverController);
@ -80,9 +94,7 @@ class _PopoverActionListState<T extends PopoverAction>
direction: widget.direction,
mutex: widget.mutex,
offset: widget.offset,
triggerActions: widget.showAtCursor
? PopoverTriggerFlags.secondaryClick
: PopoverTriggerFlags.none,
triggerActions: PopoverTriggerFlags.none,
onClose: widget.onClosed,
showAtCursor: widget.showAtCursor,
popupBuilder: (_) {

View File

@ -11,16 +11,23 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
required this.direction,
required this.offset,
required this.windowPadding,
this.position,
this.showAtCursor = false,
this.cursorOffset,
});
PopoverLink link;
PopoverDirection direction;
final Offset offset;
final EdgeInsets windowPadding;
/// Required when [showAtCursor] is true.
///
final Offset? position;
/// If true, the popover will be shown at the cursor position.
/// This will ignore the [direction], and the child size.
///
final bool showAtCursor;
final Offset? cursorOffset;
@override
bool shouldRelayout(PopoverLayoutDelegate oldDelegate) {
@ -40,14 +47,6 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
return true;
}
if (showAtCursor != oldDelegate.showAtCursor) {
return true;
}
if (showAtCursor && cursorOffset != oldDelegate.cursorOffset) {
return true;
}
return false;
}
@ -70,143 +69,128 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
@override
Offset getPositionForChild(Size size, Size childSize) {
final effectiveOffset =
showAtCursor && cursorOffset != null && link.leaderOffset != null
? link.leaderOffset! + cursorOffset!
: link.leaderOffset;
final effectiveOffset = link.leaderOffset;
final leaderSize = link.leaderSize;
if (effectiveOffset == null || leaderSize == null) {
return Offset.zero;
}
final anchorRect = Rect.fromLTWH(
effectiveOffset.dx + offset.dx,
effectiveOffset.dy + offset.dy,
leaderSize.width,
leaderSize.height,
);
Offset position = effectiveOffset;
if (showAtCursor) {
return Offset(
math.max(
windowPadding.left,
math.min(
windowPadding.left + size.width - childSize.width,
anchorRect.left,
),
),
math.max(
windowPadding.top,
math.min(
windowPadding.top + size.height - childSize.height,
anchorRect.top,
),
),
Offset position;
if (showAtCursor && this.position != null) {
position = this.position! +
Offset(
effectiveOffset.dx + offset.dx,
effectiveOffset.dy + offset.dy,
);
} else {
final anchorRect = Rect.fromLTWH(
effectiveOffset.dx + offset.dx,
effectiveOffset.dy + offset.dy,
leaderSize.width,
leaderSize.height,
);
}
switch (direction) {
case PopoverDirection.topLeft:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topRight:
position = Offset(
anchorRect.right,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.bottomLeft:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomRight:
position = Offset(
anchorRect.right,
anchorRect.bottom,
);
break;
case PopoverDirection.center:
position = anchorRect.center;
break;
case PopoverDirection.topWithLeftAligned:
position = Offset(
anchorRect.left,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topWithCenterAligned:
position = Offset(
anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topWithRightAligned:
position = Offset(
anchorRect.right - childSize.width,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.rightWithTopAligned:
position = Offset(anchorRect.right, anchorRect.top);
break;
case PopoverDirection.rightWithCenterAligned:
position = Offset(
anchorRect.right,
anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,
);
break;
case PopoverDirection.rightWithBottomAligned:
position = Offset(
anchorRect.right,
anchorRect.bottom - childSize.height,
);
break;
case PopoverDirection.bottomWithLeftAligned:
position = Offset(
anchorRect.left,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomWithCenterAligned:
position = Offset(
anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomWithRightAligned:
position = Offset(
anchorRect.right - childSize.width,
anchorRect.bottom,
);
break;
case PopoverDirection.leftWithTopAligned:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top,
);
break;
case PopoverDirection.leftWithCenterAligned:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,
);
break;
case PopoverDirection.leftWithBottomAligned:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.bottom - childSize.height,
);
break;
default:
throw UnimplementedError();
switch (direction) {
case PopoverDirection.topLeft:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topRight:
position = Offset(
anchorRect.right,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.bottomLeft:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomRight:
position = Offset(
anchorRect.right,
anchorRect.bottom,
);
break;
case PopoverDirection.center:
position = anchorRect.center;
break;
case PopoverDirection.topWithLeftAligned:
position = Offset(
anchorRect.left,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topWithCenterAligned:
position = Offset(
anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.topWithRightAligned:
position = Offset(
anchorRect.right - childSize.width,
anchorRect.top - childSize.height,
);
break;
case PopoverDirection.rightWithTopAligned:
position = Offset(anchorRect.right, anchorRect.top);
break;
case PopoverDirection.rightWithCenterAligned:
position = Offset(
anchorRect.right,
anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,
);
break;
case PopoverDirection.rightWithBottomAligned:
position = Offset(
anchorRect.right,
anchorRect.bottom - childSize.height,
);
break;
case PopoverDirection.bottomWithLeftAligned:
position = Offset(
anchorRect.left,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomWithCenterAligned:
position = Offset(
anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,
anchorRect.bottom,
);
break;
case PopoverDirection.bottomWithRightAligned:
position = Offset(
anchorRect.right - childSize.width,
anchorRect.bottom,
);
break;
case PopoverDirection.leftWithTopAligned:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top,
);
break;
case PopoverDirection.leftWithCenterAligned:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,
);
break;
case PopoverDirection.leftWithBottomAligned:
position = Offset(
anchorRect.left - childSize.width,
anchorRect.bottom - childSize.height,
);
break;
default:
throw UnimplementedError();
}
}
return Offset(
@ -232,16 +216,16 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate {
PopoverDirection? direction,
Offset? offset,
EdgeInsets? windowPadding,
Offset? position,
bool? showAtCursor,
Offset? cursorOffset,
}) {
return PopoverLayoutDelegate(
link: link ?? this.link,
direction: direction ?? this.direction,
offset: offset ?? this.offset,
windowPadding: windowPadding ?? this.windowPadding,
position: position ?? this.position,
showAtCursor: showAtCursor ?? this.showAtCursor,
cursorOffset: cursorOffset ?? this.cursorOffset,
);
}
}

View File

@ -11,6 +11,7 @@ class PopoverController {
void close() => _state?.close();
void show() => _state?.showOverlay();
void showAt(Offset position) => _state?.showOverlay(position);
}
class PopoverTriggerFlags {
@ -135,11 +136,18 @@ class Popover extends StatefulWidget {
final String? debugId;
/// Whether the popover should be shown at the cursor position.
///
/// This only works when using [PopoverClickHandler.listener] as the click handler.
///
/// Alternatively for having a normal popover, and use the cursor position only on
/// secondary click, consider showing the popover programatically with [PopoverController.showAt].
///
final bool showAtCursor;
/// The content area of the popover.
final Widget child;
final bool showAtCursor;
@override
State<Popover> createState() => PopoverState();
}
@ -153,7 +161,6 @@ class PopoverState extends State<Popover> with SingleTickerProviderStateMixin {
link: popoverLink,
offset: widget.offset ?? Offset.zero,
windowPadding: widget.windowPadding ?? EdgeInsets.zero,
showAtCursor: widget.showAtCursor,
);
late AnimationController animationController;
@ -164,6 +171,8 @@ class PopoverState extends State<Popover> with SingleTickerProviderStateMixin {
// If the widget is disposed, prevent the animation from being called.
bool isDisposed = false;
Offset? cursorPosition;
@override
void initState() {
super.initState();
@ -205,13 +214,23 @@ class PopoverState extends State<Popover> with SingleTickerProviderStateMixin {
super.reassemble();
}
void showOverlay() {
void showOverlay([Offset? position]) {
close(withAnimation: true);
if (widget.mutex != null) {
widget.mutex?.state = this;
}
if (position != null) {
final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
final offset = renderBox?.globalToLocal(position);
layoutDelegate = layoutDelegate.copyWith(
position: offset ?? position,
windowPadding: EdgeInsets.zero,
showAtCursor: true,
);
}
final shouldAddMask = rootEntry.isEmpty;
rootEntry.addEntry(
context,
@ -272,7 +291,7 @@ class PopoverState extends State<Popover> with SingleTickerProviderStateMixin {
return;
}
showOverlay();
showOverlay(cursorPosition);
},
);
@ -290,10 +309,7 @@ class PopoverState extends State<Popover> with SingleTickerProviderStateMixin {
return switch (widget.clickHandler) {
PopoverClickHandler.listener => Listener(
onPointerDown: (event) {
if (widget.showAtCursor) {
layoutDelegate =
layoutDelegate.copyWith(cursorOffset: event.localPosition);
}
cursorPosition = widget.showAtCursor ? event.position : null;
if (event.buttons == kSecondaryMouseButton &&
widget.triggerActions & PopoverTriggerFlags.secondaryClick !=

View File

@ -66,6 +66,14 @@ class AppFlowyPopover extends StatelessWidget {
///
final bool skipTraversal;
/// Whether the popover should be shown at the cursor position.
/// If true, the [offset] will be ignored.
///
/// This only works when using [PopoverClickHandler.listener] as the click handler.
///
/// Alternatively for having a normal popover, and use the cursor position only on
/// secondary click, consider showing the popover programatically with [PopoverController.showAt].
///
final bool showAtCursor;
@override
@ -89,7 +97,6 @@ class AppFlowyPopover extends StatelessWidget {
offset: offset,
clickHandler: clickHandler,
skipTraversal: skipTraversal,
showAtCursor: showAtCursor,
popupBuilder: (context) => _PopoverContainer(
constraints: constraints,
margin: margin,
@ -97,6 +104,7 @@ class AppFlowyPopover extends StatelessWidget {
borderRadius: borderRadius,
child: popupBuilder(context),
),
showAtCursor: showAtCursor,
child: child,
);
}

View File

@ -1543,10 +1543,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
version: "3.1.4"
plugin_platform_interface:
dependency: "direct dev"
description:
@ -1941,10 +1941,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.2.0"
string_validator:
dependency: "direct main"
description:
@ -2246,10 +2246,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "14.2.5"
version: "14.2.1"
watcher:
dependency: transitive
description: