2022-08-31 15:36:47 +08:00
|
|
|
import 'package:appflowy_popover/layout.dart';
|
2022-08-30 12:51:59 +08:00
|
|
|
import 'package:flutter/gestures.dart';
|
2022-08-29 14:00:27 +08:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/services.dart';
|
2022-08-29 13:56:16 +08:00
|
|
|
|
2022-09-01 15:28:16 +08:00
|
|
|
/// If multiple popovers are exclusive,
|
|
|
|
/// pass the same mutex to them.
|
2022-08-30 14:02:06 +08:00
|
|
|
class PopoverMutex {
|
2022-08-30 18:26:44 +08:00
|
|
|
PopoverState? state;
|
2022-08-29 15:29:39 +08:00
|
|
|
}
|
|
|
|
|
2022-08-30 12:51:59 +08:00
|
|
|
class PopoverController {
|
|
|
|
PopoverState? state;
|
2022-08-29 14:00:27 +08:00
|
|
|
|
|
|
|
close() {
|
|
|
|
state?.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
show() {
|
|
|
|
state?.showOverlay();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-30 18:26:44 +08:00
|
|
|
class PopoverTriggerActionFlags {
|
|
|
|
static int click = 0x01;
|
|
|
|
static int hover = 0x02;
|
|
|
|
}
|
|
|
|
|
2022-08-31 15:36:47 +08:00
|
|
|
enum PopoverDirection {
|
|
|
|
// Corner aligned with a corner of the SourceWidget
|
|
|
|
topLeft,
|
|
|
|
topRight,
|
|
|
|
bottomLeft,
|
|
|
|
bottomRight,
|
|
|
|
center,
|
|
|
|
|
|
|
|
// Edge aligned with a edge of the SourceWidget
|
|
|
|
topWithLeftAligned,
|
|
|
|
topWithCenterAligned,
|
|
|
|
topWithRightAligned,
|
|
|
|
rightWithTopAligned,
|
|
|
|
rightWithCenterAligned,
|
|
|
|
rightWithBottomAligned,
|
|
|
|
bottomWithLeftAligned,
|
|
|
|
bottomWithCenterAligned,
|
|
|
|
bottomWithRightAligned,
|
|
|
|
leftWithTopAligned,
|
|
|
|
leftWithCenterAligned,
|
|
|
|
leftWithBottomAligned,
|
|
|
|
|
|
|
|
custom,
|
|
|
|
}
|
|
|
|
|
2022-08-30 12:51:59 +08:00
|
|
|
class Popover extends StatefulWidget {
|
|
|
|
final PopoverController? controller;
|
2022-09-01 15:28:16 +08:00
|
|
|
|
2022-08-29 14:00:27 +08:00
|
|
|
final Offset? offset;
|
2022-09-01 15:28:16 +08:00
|
|
|
|
2022-08-29 14:00:27 +08:00
|
|
|
final Decoration? maskDecoration;
|
2022-09-01 15:28:16 +08:00
|
|
|
|
|
|
|
/// The function used to build the popover.
|
2022-08-29 14:00:27 +08:00
|
|
|
final Widget Function(BuildContext context) popupBuilder;
|
2022-09-01 15:28:16 +08:00
|
|
|
|
2022-08-30 18:26:44 +08:00
|
|
|
final int triggerActions;
|
2022-09-01 15:28:16 +08:00
|
|
|
|
|
|
|
/// If multiple popovers are exclusive,
|
|
|
|
/// pass the same mutex to them.
|
2022-08-30 18:26:44 +08:00
|
|
|
final PopoverMutex? mutex;
|
2022-09-01 15:28:16 +08:00
|
|
|
|
|
|
|
/// The direction of the popover
|
2022-08-31 15:36:47 +08:00
|
|
|
final PopoverDirection direction;
|
2022-09-01 15:28:16 +08:00
|
|
|
|
2022-08-30 15:29:37 +08:00
|
|
|
final void Function()? onClose;
|
2022-08-29 14:00:27 +08:00
|
|
|
|
2022-09-01 15:28:16 +08:00
|
|
|
/// The content area of the popover.
|
|
|
|
final Widget child;
|
|
|
|
|
2022-08-30 12:51:59 +08:00
|
|
|
const Popover({
|
2022-08-29 14:00:27 +08:00
|
|
|
Key? key,
|
|
|
|
required this.child,
|
|
|
|
required this.popupBuilder,
|
|
|
|
this.controller,
|
|
|
|
this.offset,
|
|
|
|
this.maskDecoration,
|
2022-08-30 18:26:44 +08:00
|
|
|
this.triggerActions = 0,
|
2022-08-31 15:36:47 +08:00
|
|
|
this.direction = PopoverDirection.rightWithTopAligned,
|
2022-08-30 18:26:44 +08:00
|
|
|
this.mutex,
|
2022-08-30 15:29:37 +08:00
|
|
|
this.onClose,
|
2022-08-29 14:00:27 +08:00
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
2022-08-30 12:51:59 +08:00
|
|
|
State<Popover> createState() => PopoverState();
|
2022-08-29 14:00:27 +08:00
|
|
|
}
|
|
|
|
|
2022-08-30 12:51:59 +08:00
|
|
|
class PopoverState extends State<Popover> {
|
2022-08-31 15:36:47 +08:00
|
|
|
final PopoverLink popoverLink = PopoverLink();
|
2022-08-29 14:00:27 +08:00
|
|
|
OverlayEntry? _overlayEntry;
|
|
|
|
bool hasMask = true;
|
|
|
|
|
2022-08-30 12:51:59 +08:00
|
|
|
static PopoverState? _popoverWithMask;
|
2022-08-29 16:07:54 +08:00
|
|
|
|
2022-08-29 14:00:27 +08:00
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
widget.controller?.state = this;
|
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
|
|
|
showOverlay() {
|
2022-08-29 16:07:54 +08:00
|
|
|
close();
|
2022-08-29 14:00:27 +08:00
|
|
|
|
2022-08-30 18:26:44 +08:00
|
|
|
if (widget.mutex != null) {
|
|
|
|
if (widget.mutex!.state != null && widget.mutex!.state != this) {
|
|
|
|
widget.mutex!.state!.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
widget.mutex!.state = this;
|
|
|
|
}
|
|
|
|
|
2022-08-29 16:07:54 +08:00
|
|
|
if (_popoverWithMask == null) {
|
|
|
|
_popoverWithMask = this;
|
|
|
|
} else {
|
2022-08-29 14:00:27 +08:00
|
|
|
hasMask = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
final newEntry = OverlayEntry(builder: (context) {
|
|
|
|
final children = <Widget>[];
|
|
|
|
|
|
|
|
if (hasMask) {
|
|
|
|
children.add(_PopoverMask(
|
|
|
|
decoration: widget.maskDecoration,
|
2022-08-29 16:07:54 +08:00
|
|
|
onTap: () => close(),
|
|
|
|
onExit: () => close(),
|
2022-08-29 14:00:27 +08:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2022-08-31 18:06:41 +08:00
|
|
|
children.add(PopoverContainer(
|
|
|
|
direction: widget.direction,
|
|
|
|
popoverLink: popoverLink,
|
|
|
|
offset: widget.offset ?? Offset.zero,
|
|
|
|
popupBuilder: widget.popupBuilder,
|
|
|
|
onClose: () => close(),
|
2022-08-31 19:15:20 +08:00
|
|
|
onCloseAll: () => closeAll(),
|
2022-08-31 18:06:41 +08:00
|
|
|
));
|
2022-08-29 14:00:27 +08:00
|
|
|
|
|
|
|
return Stack(children: children);
|
|
|
|
});
|
|
|
|
|
|
|
|
_overlayEntry = newEntry;
|
|
|
|
|
|
|
|
Overlay.of(context)?.insert(newEntry);
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
2022-08-29 16:07:54 +08:00
|
|
|
if (_overlayEntry != null) {
|
|
|
|
_overlayEntry!.remove();
|
|
|
|
_overlayEntry = null;
|
|
|
|
if (_popoverWithMask == this) {
|
|
|
|
_popoverWithMask = null;
|
|
|
|
}
|
2022-08-30 15:29:37 +08:00
|
|
|
if (widget.onClose != null) {
|
|
|
|
widget.onClose!();
|
|
|
|
}
|
2022-08-29 14:00:27 +08:00
|
|
|
}
|
2022-08-30 18:26:44 +08:00
|
|
|
|
|
|
|
if (widget.mutex?.state == this) {
|
|
|
|
widget.mutex!.state = null;
|
|
|
|
}
|
2022-08-29 14:00:27 +08:00
|
|
|
}
|
|
|
|
|
2022-08-31 19:15:20 +08:00
|
|
|
closeAll() {
|
|
|
|
_popoverWithMask?.close();
|
|
|
|
}
|
|
|
|
|
2022-08-29 14:00:27 +08:00
|
|
|
@override
|
2022-08-29 16:07:54 +08:00
|
|
|
void deactivate() {
|
|
|
|
close();
|
|
|
|
super.deactivate();
|
2022-08-29 14:00:27 +08:00
|
|
|
}
|
|
|
|
|
2022-08-30 18:26:44 +08:00
|
|
|
_handleTargetPointerDown(PointerDownEvent event) {
|
|
|
|
if (widget.triggerActions & PopoverTriggerActionFlags.click != 0) {
|
|
|
|
showOverlay();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleTargetPointerEnter(PointerEnterEvent event) {
|
|
|
|
if (widget.triggerActions & PopoverTriggerActionFlags.hover != 0) {
|
|
|
|
showOverlay();
|
|
|
|
}
|
2022-08-30 12:51:59 +08:00
|
|
|
}
|
|
|
|
|
2022-09-01 15:28:16 +08:00
|
|
|
_buildContent(BuildContext context) {
|
|
|
|
if (widget.triggerActions == 0) {
|
|
|
|
return widget.child;
|
|
|
|
}
|
|
|
|
|
|
|
|
return MouseRegion(
|
|
|
|
onEnter: _handleTargetPointerEnter,
|
|
|
|
child: Listener(
|
|
|
|
onPointerDown: _handleTargetPointerDown,
|
|
|
|
child: widget.child,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-29 14:00:27 +08:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2022-08-31 15:36:47 +08:00
|
|
|
return PopoverTarget(
|
|
|
|
link: popoverLink,
|
2022-09-01 15:28:16 +08:00
|
|
|
child: _buildContent(context),
|
2022-08-30 18:26:44 +08:00
|
|
|
);
|
2022-08-29 14:00:27 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _PopoverMask extends StatefulWidget {
|
|
|
|
final void Function() onTap;
|
|
|
|
final void Function()? onExit;
|
|
|
|
final Decoration? decoration;
|
|
|
|
|
|
|
|
const _PopoverMask(
|
|
|
|
{Key? key, required this.onTap, this.onExit, this.decoration})
|
|
|
|
: super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() => _PopoverMaskState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _PopoverMaskState extends State<_PopoverMask> {
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);
|
|
|
|
super.initState();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _handleGlobalKeyEvent(KeyEvent event) {
|
|
|
|
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
|
|
if (widget.onExit != null) {
|
|
|
|
widget.onExit!();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2022-08-29 16:07:54 +08:00
|
|
|
void deactivate() {
|
2022-08-29 14:00:27 +08:00
|
|
|
HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
|
2022-08-29 16:07:54 +08:00
|
|
|
super.deactivate();
|
2022-08-29 14:00:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return GestureDetector(
|
|
|
|
onTap: widget.onTap,
|
|
|
|
child: Container(
|
|
|
|
// decoration: widget.decoration,
|
|
|
|
decoration: widget.decoration ??
|
|
|
|
const BoxDecoration(
|
|
|
|
color: Color.fromARGB(0, 244, 67, 54),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2022-08-29 13:56:16 +08:00
|
|
|
}
|
2022-08-31 18:06:41 +08:00
|
|
|
|
|
|
|
class PopoverContainer extends StatefulWidget {
|
|
|
|
final Widget Function(BuildContext context) popupBuilder;
|
|
|
|
final PopoverDirection direction;
|
|
|
|
final PopoverLink popoverLink;
|
|
|
|
final Offset offset;
|
|
|
|
final void Function() onClose;
|
2022-08-31 19:15:20 +08:00
|
|
|
final void Function() onCloseAll;
|
2022-08-31 18:06:41 +08:00
|
|
|
|
|
|
|
const PopoverContainer({
|
|
|
|
Key? key,
|
|
|
|
required this.popupBuilder,
|
|
|
|
required this.direction,
|
|
|
|
required this.popoverLink,
|
|
|
|
required this.offset,
|
|
|
|
required this.onClose,
|
2022-08-31 19:15:20 +08:00
|
|
|
required this.onCloseAll,
|
2022-08-31 18:06:41 +08:00
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() => PopoverContainerState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class PopoverContainerState extends State<PopoverContainer> {
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return CustomSingleChildLayout(
|
|
|
|
delegate: PopoverLayoutDelegate(
|
|
|
|
direction: widget.direction,
|
|
|
|
link: widget.popoverLink,
|
|
|
|
offset: widget.offset,
|
|
|
|
),
|
|
|
|
child: widget.popupBuilder(context),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-31 19:15:20 +08:00
|
|
|
close() => widget.onClose();
|
|
|
|
|
|
|
|
closeAll() => widget.onCloseAll();
|
2022-08-31 18:06:41 +08:00
|
|
|
|
|
|
|
static PopoverContainerState of(BuildContext context) {
|
|
|
|
if (context is StatefulElement && context.state is PopoverContainerState) {
|
|
|
|
return context.state as PopoverContainerState;
|
|
|
|
}
|
|
|
|
final PopoverContainerState? result =
|
|
|
|
context.findAncestorStateOfType<PopoverContainerState>();
|
|
|
|
return result!;
|
|
|
|
}
|
|
|
|
}
|