showPopupMenu function

Future<String?> showPopupMenu(
  1. BuildContext context,
  2. PopupType type, {
  3. List<PopupMenuEntry<String>>? items,
  4. bool? isPin,
  5. bool? isMute,
  6. bool? canRecall,
  7. bool? canQuote,
  8. bool? canCopy,
  9. bool? canEdit,
  10. String editLabel = '编辑',
  11. bool? canSpeechToText,
  12. bool? canCancelSpeechToText,
  13. bool conversationIsSystem = false,
})

Implementation

Future<String?> showPopupMenu(BuildContext context, PopupType type,
    {List<PopupMenuEntry<String>>? items,
    bool? isPin,
    bool? isMute,
    bool? canRecall,
    bool? canQuote,
    bool? canCopy,
    bool? canEdit,
    String editLabel = '编辑',
    bool? canSpeechToText,
    bool? canCancelSpeechToText,
    bool conversationIsSystem = false}) {
// 默认菜单项列表
  final defaultItemsChat = [
    if (((canSpeechToText ?? false) || (canCancelSpeechToText ?? false)) &&
        !conversationIsSystem)
      PopupMenuItem<String>(
        value: 'speechToText',
        height: _popupMenuItemHeight,
        padding: EdgeInsets.zero,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ImageUtil.getImageWidget(
                  RCKThemeProvider().themeIcon.speechToText ?? '',
                  width: kPopupIconSize,
                  height: kPopupIconSize,
                  color: RCKThemeProvider().themeColor.textPrimary),
              Text(
                (canCancelSpeechToText ?? false) ? '取消转文字' : '转文字',
                style: TextStyle(
                    fontSize: convoPopupFontSize,
                    color: RCKThemeProvider().themeColor.textPrimary),
              ),
            ],
          ),
        ),
      ),
    if (canCopy ?? true)
      PopupMenuItem<String>(
        value: 'copy',
        height: _popupMenuItemHeight,
        padding: EdgeInsets.zero,
        child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                ImageUtil.getImageWidget(
                    RCKThemeProvider().themeIcon.copy ?? '',
                    width: kPopupIconSize,
                    height: kPopupIconSize),
                Text(
                  '复制',
                  style: TextStyle(
                      fontSize: convoPopupFontSize,
                      color: RCKThemeProvider().themeColor.textPrimary),
                ),
              ],
            )),
      ),
    if ((canEdit ?? false) && !conversationIsSystem)
      PopupMenuItem<String>(
        value: 'edit',
        height: _popupMenuItemHeight,
        padding: EdgeInsets.zero,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ImageUtil.getImageWidget(
                RCKThemeProvider().themeIcon.edit ?? '',
                width: kPopupIconSize,
                height: kPopupIconSize,
                color: RCKThemeProvider().themeColor.textPrimary,
              ),
              Text(
                editLabel,
                style: TextStyle(
                  fontSize: convoPopupFontSize,
                  color: RCKThemeProvider().themeColor.textPrimary,
                ),
              ),
            ],
          ),
        ),
      ),
    if (!conversationIsSystem)
      PopupMenuItem<String>(
        value: 'forward',
        height: _popupMenuItemHeight,
        padding: EdgeInsets.zero,
        child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                ImageUtil.getImageWidget(
                    RCKThemeProvider().themeIcon.forwardSingle ?? '',
                    width: kPopupIconSize,
                    height: kPopupIconSize),
                Text(
                  '转发',
                  style: TextStyle(
                      fontSize: convoPopupFontSize,
                      color: RCKThemeProvider().themeColor.textPrimary),
                ),
              ],
            )),
      ),
    PopupMenuItem<String>(
      value: 'delete',
      height: _popupMenuItemHeight,
      padding: EdgeInsets.zero,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            ImageUtil.getImageWidget(RCKThemeProvider().themeIcon.delete ?? '',
                width: kPopupIconSize, height: kPopupIconSize),
            Text(
              '删除',
              style: TextStyle(
                  fontSize: convoPopupFontSize,
                  color: RCKThemeProvider().themeColor.textPrimary),
            ),
          ],
        ),
      ),
    ),
    if (!conversationIsSystem)
      PopupMenuItem<String>(
        value: 'multi',
        height: _popupMenuItemHeight,
        padding: EdgeInsets.zero,
        child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                ImageUtil.getImageWidget(
                    RCKThemeProvider().themeIcon.multiSelect ?? '',
                    width: kPopupIconSize,
                    height: kPopupIconSize,
                    color: RCKThemeProvider().themeColor.textPrimary),
                Text(
                  '多选',
                  style: TextStyle(
                      fontSize: convoPopupFontSize,
                      color: RCKThemeProvider().themeColor.textPrimary),
                ),
              ],
            )),
      ),
    if ((canQuote ?? true) && !conversationIsSystem)
      PopupMenuItem<String>(
        value: 'quote',
        height: _popupMenuItemHeight,
        padding: EdgeInsets.zero,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ImageUtil.getImageWidget(RCKThemeProvider().themeIcon.reply ?? '',
                  width: kPopupIconSize,
                  height: kPopupIconSize,
                  color: RCKThemeProvider().themeColor.textPrimary),
              Text(
                '引用',
                style: TextStyle(
                    fontSize: convoPopupFontSize,
                    color: RCKThemeProvider().themeColor.textPrimary),
              ),
            ],
          ),
        ),
      ),
    if ((canRecall ?? false) && !conversationIsSystem)
      PopupMenuItem<String>(
        value: 'recall',
        height: _popupMenuItemHeight,
        padding: EdgeInsets.zero,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ImageUtil.getImageWidget(
                  RCKThemeProvider().themeIcon.recall ?? '',
                  width: kPopupIconSize,
                  height: kPopupIconSize,
                  color: RCKThemeProvider().themeColor.textPrimary),
              Text(
                '撤回',
                style: TextStyle(
                    fontSize: convoPopupFontSize,
                    color: RCKThemeProvider().themeColor.textPrimary),
              ),
            ],
          ),
        ),
      ),
  ];
  const chatMenuOrder = <String, int>{
    'speechToText': 0,
    'copy': 1,
    'delete': 2,
    'recall': 3,
    'quote': 4,
    'edit': 5,
    'forward': 6,
    'multi': 7,
  };
  defaultItemsChat.sort((left, right) {
    final leftOrder = chatMenuOrder[left.value] ?? chatMenuOrder.length;
    final rightOrder = chatMenuOrder[right.value] ?? chatMenuOrder.length;
    return leftOrder.compareTo(rightOrder);
  });

  // 附加气泡菜单项列表
  final defaultItemsBubbleAppend = [
    if ((canCancelSpeechToText ?? false) && !conversationIsSystem)
      PopupMenuItem<String>(
        value: 'speechToText',
        height: _popupMenuItemHeight,
        padding: EdgeInsets.zero,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ImageUtil.getImageWidget(
                  RCKThemeProvider().themeIcon.speechToText ?? '',
                  width: kPopupIconSize,
                  height: kPopupIconSize,
                  color: RCKThemeProvider().themeColor.textPrimary),
              Text(
                '取消转文字',
                style: TextStyle(
                    fontSize: convoPopupFontSize,
                    color: RCKThemeProvider().themeColor.textPrimary),
              ),
            ],
          ),
        ),
      ),
    PopupMenuItem<String>(
      value: 'copy',
      height: _popupMenuItemHeight,
      padding: EdgeInsets.zero,
      child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ImageUtil.getImageWidget(RCKThemeProvider().themeIcon.copy ?? '',
                  width: kPopupIconSize, height: kPopupIconSize),
              Text(
                '复制',
                style: TextStyle(
                    fontSize: convoPopupFontSize,
                    color: RCKThemeProvider().themeColor.textPrimary),
              ),
            ],
          )),
    ),
  ];

  // 默认菜单项列表
  final defaultItemsConvo = [
    PopupMenuItem<String>(
      value: 'pin',
      height: _popupMenuItemHeight,
      padding: EdgeInsets.zero,
      child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ImageUtil.getImageWidget(
                  isPin ?? false
                      ? RCKThemeProvider().themeIcon.unpin ?? ''
                      : RCKThemeProvider().themeIcon.pin ?? '',
                  width: kPopupIconSize,
                  height: kPopupIconSize,
                  color: RCKThemeProvider().themeColor.textPrimary),
              Text(
                isPin ?? false ? '取消置顶' : '置顶',
                style: TextStyle(
                    fontSize: convoPopupFontSize,
                    color: RCKThemeProvider().themeColor.textPrimary),
              ),
            ],
          )),
    ),
    PopupMenuItem<String>(
      value: 'mute',
      height: _popupMenuItemHeight,
      padding: EdgeInsets.zero,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            ImageUtil.getImageWidget(
                isMute ?? false
                    ? RCKThemeProvider().themeIcon.allowNotification ?? ''
                    : RCKThemeProvider().themeIcon.doNotDisturb1 ?? '',
                width: kPopupIconSize,
                height: kPopupIconSize),
            Text(
              isMute ?? false ? '允许消息通知' : '免打扰',
              style: TextStyle(
                  fontSize: convoPopupFontSize,
                  color: RCKThemeProvider().themeColor.textPrimary),
            ),
          ],
        ),
      ),
    ),
    PopupMenuItem<String>(
      value: 'delete',
      height: _popupMenuItemHeight,
      padding: EdgeInsets.zero,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            ImageUtil.getImageWidget(RCKThemeProvider().themeIcon.delete ?? '',
                width: kPopupIconSize, height: kPopupIconSize),
            Text(
              '删除',
              style: TextStyle(
                  fontSize: convoPopupFontSize,
                  color: RCKThemeProvider().themeColor.textPrimary),
            ),
          ],
        ),
      ),
    ),
  ];
  // 获取点击 widget 的全局坐标
  final RenderBox renderBox = context.findRenderObject() as RenderBox;
  final Offset offset =
      renderBox.localToGlobal(renderBox.size.center(Offset.zero));
  final RelativeRect position =
      RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, offset.dy);
  final popupItems = items ??
      (switch (type) {
        PopupType.convo => defaultItemsConvo,
        PopupType.chat => defaultItemsChat,
        PopupType.bubbleAppend => defaultItemsBubbleAppend,
      });

  // 如果是在对话详情页面,调整菜单位置以避免遮挡输入框
  RelativeRect menuPosition = position;
  double? menuMaxHeight;

  if (type == PopupType.chat || type == PopupType.bubbleAppend) {
    // 获取屏幕高度
    final double screenHeight = MediaQuery.of(context).size.height;
    final double screenWidth = MediaQuery.of(context).size.width;
    // 获取底部安全区域高度
    final double bottomSafeArea = MediaQuery.of(context).padding.bottom;
    // 键盘弹出时,viewInsets.bottom 才是内容实际可用区域的底边。
    final double bottomOcclusion = math.max(
      bottomSafeArea,
      MediaQuery.viewInsetsOf(context).bottom,
    );
    // 获取顶部安全区域高度(状态栏)
    final double topSafeArea = MediaQuery.of(context).padding.top;
    // 计算header高度(状态栏高度 + 标题栏高度)
    final double headerHeight = topSafeArea + kChatAppBarHeight;

    final messageInputProvider =
        Provider.of<RCKMessageInputProvider>(context, listen: false);

    // 底部按钮高度
    final double heightBottomBar =
        messageInputProvider.inputType == RCIMIWMessageInputType.emoji ||
                messageInputProvider.inputType == RCIMIWMessageInputType.more
            ? kInputExtentionHeight
            : 0;
    // 消息列表的实际底边就是输入区域顶部,可覆盖多行输入和引用预览。
    final messageListContext =
        Provider.of<RCKChatProvider>(context, listen: false)
            .messageListKey
            .currentContext;
    final messageListRenderObject = messageListContext?.findRenderObject();
    final messageListRenderBox =
        messageListRenderObject is RenderBox ? messageListRenderObject : null;
    final double fallbackInputFieldTop = screenHeight -
        heightBottomBar -
        kInputMultiSelectHeight -
        bottomOcclusion;
    final double inputFieldTop = messageListRenderBox?.hasSize == true
        ? messageListRenderBox!.localToGlobal(Offset.zero).dy +
            messageListRenderBox.size.height
        : fallbackInputFieldTop;

    // 给 PopupRoute 的屏幕内二次布局和输入区域留出余量。
    const double menuEdgeSpacing = 16;
    final double menuTopLimit = headerHeight + menuEdgeSpacing;
    final double menuBottomLimit = math.min(
          inputFieldTop,
          screenHeight - bottomOcclusion,
        ) -
        menuEdgeSpacing;
    final double availableMenuHeight =
        math.max(0, menuBottomLimit - menuTopLimit);
    final double desiredMenuHeight = popupItems.fold<double>(
      8,
      (height, item) => height + item.height,
    );
    final double menuHeight = math.min(desiredMenuHeight, availableMenuHeight);
    final double menuTop = math.max(
      menuTopLimit,
      math.min(offset.dy, menuBottomLimit - menuHeight),
    );

    // PopupMenuItem 的实际高度可能大于声明的 height,最大高度必须按
    // 当前菜单顶部到可用底边的剩余空间计算,才能形成不可越过的硬边界。
    menuMaxHeight = math.max(0, menuBottomLimit - menuTop);
    menuPosition = RelativeRect.fromLTRB(
      offset.dx,
      menuTop,
      math.max(0, screenWidth - offset.dx),
      math.max(0, screenHeight - menuBottomLimit),
    );
  }

  // 使用自定义主题和样式设置
  return showMenu<String>(
    menuPadding: EdgeInsets.zero,
    context: context,
    position: menuPosition,
    // 弹出消息菜单时保留输入框焦点,避免键盘被 PopupRoute 收起。
    requestFocus: false,
    elevation: 8.0, // 阴影高度
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(10), // 圆角为10
    ),
    constraints: BoxConstraints(
      minWidth: 160,
      maxWidth: 160, // 固定宽度为160
      maxHeight: menuMaxHeight ?? double.infinity,
    ),
    color: RCKThemeProvider().themeColor.textInverse, // 背景色
    items: [
      // 顶部填充
      const PopupMenuItem<String>(
        enabled: false,
        height: 4, // 顶部padding高度
        padding: EdgeInsets.zero,
        child: SizedBox(),
      ),
      // 真实菜单项
      ...popupItems,
      // 底部填充
      const PopupMenuItem<String>(
        enabled: false,
        height: 4, // 底部padding高度
        padding: EdgeInsets.zero,
        child: SizedBox(),
      ),
    ],
    // 自定义内边距和项目间距
    surfaceTintColor: Colors.transparent,
    shadowColor: const Color(0x33000000), // 阴影颜色对应 #00000033
  );
}