mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-25 06:05:47 +00:00
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:
parent
d5c1955ea3
commit
af6736d352
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -401,8 +401,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
dateOrReminderSlashMenuItem,
|
||||
photoGallerySlashMenuItem,
|
||||
fileSlashMenuItem,
|
||||
// disable subPageSlashMenuItem temporarily, enable it in version 0.7.2
|
||||
// subPageSlashMenuItem,
|
||||
subPageSlashMenuItem,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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: (_) {
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 !=
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user