TF-4195 Fix comments of coderabbit

This commit is contained in:
dab246
2025-12-26 12:22:18 +07:00
committed by Dat H. Pham
parent 0076a7af57
commit e8b2c1d2a5
32 changed files with 235 additions and 146 deletions
@@ -0,0 +1,42 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
class HoverSubmenuController {
HoverSubmenuController({
this.exitDelay = const Duration(milliseconds: 120),
});
final Duration exitDelay;
final ValueNotifier<bool> isHovering = ValueNotifier(false);
int _hoverRefCount = 0;
Timer? _exitTimer;
void enter() {
_exitTimer?.cancel();
_hoverRefCount++;
if (!isHovering.value) {
isHovering.value = true;
}
}
void exit() {
_hoverRefCount = (_hoverRefCount - 1).clamp(0, 999);
if (_hoverRefCount == 0) {
_exitTimer?.cancel();
_exitTimer = Timer(exitDelay, () {
if (_hoverRefCount == 0) {
isHovering.value = false;
}
});
}
}
void dispose() {
_exitTimer?.cancel();
isHovering.dispose();
}
}
@@ -37,6 +37,7 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
child: PopupMenuItemActionWidget(
menuAction: menuAction,
menuActionClick: (menuAction) {
submenuController?.hide();
Navigator.pop(context);
onActionSelected(menuAction);
},
@@ -60,7 +61,11 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
],
];
return openPopupMenuAction(context, position, popupMenuItems);
try {
await openPopupMenuAction(context, position, popupMenuItems);
} finally {
submenuController?.hide();
}
}
void _showPopupSubmenu({
@@ -69,8 +74,9 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
required PopupSubmenuController submenuController,
required Widget submenu,
}) {
final renderBox = itemKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return;
final renderObject = itemKey.currentContext?.findRenderObject();
if (renderObject is! RenderBox) return;
final renderBox = renderObject;
final offset = renderBox.localToGlobal(Offset.zero);
final rect = offset & renderBox.size;
@@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:model/email/email_action_type.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
import 'package:tmail_ui_user/features/base/widget/popup_menu/hover_submenu_controller.dart';
import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart';
class PopupMenuItemActionWidget extends StatefulWidget {
@@ -28,14 +29,15 @@ class PopupMenuItemActionWidget extends StatefulWidget {
class _PopupMenuItemActionWidgetState extends State<PopupMenuItemActionWidget> {
GlobalKey? _itemKey;
ValueNotifier<bool>? _hoverItemNotifier;
HoverSubmenuController? _hoverController;
@override
void initState() {
super.initState();
if (widget.menuAction is PopupMenuItemEmailAction) {
if (widget.menuAction is PopupMenuItemEmailAction &&
widget.menuAction.action == EmailActionType.labelAs) {
_itemKey = GlobalKey();
_hoverItemNotifier = ValueNotifier(false);
_hoverController = HoverSubmenuController();
}
}
@@ -133,9 +135,8 @@ class _PopupMenuItemActionWidgetState extends State<PopupMenuItemActionWidget> {
return PointerInterceptor(
child: MouseRegion(
onEnter: (_) {
if (_hoverItemNotifier != null) {
_hoverItemNotifier?.value = true;
}
_hoverController?.enter();
if (_itemKey != null) {
widget.onHoverShowSubmenu?.call(_itemKey!);
} else {
@@ -143,9 +144,7 @@ class _PopupMenuItemActionWidgetState extends State<PopupMenuItemActionWidget> {
}
},
onExit: (_) {
if (_hoverItemNotifier != null) {
_hoverItemNotifier?.value = false;
}
_hoverController?.exit();
},
child: Material(
type: MaterialType.transparency,
@@ -172,26 +171,23 @@ class _PopupMenuItemActionWidgetState extends State<PopupMenuItemActionWidget> {
),
if (isSelected && selectedIconWidget != null)
selectedIconWidget,
if (_hoverItemNotifier != null)
if (_hoverController != null)
ValueListenableBuilder(
valueListenable: _hoverItemNotifier!,
builder: (_, value, __) {
if (value) {
return Padding(
padding:
const EdgeInsetsDirectional.only(start: 16),
child: SvgPicture.asset(
specificMenuAction.hoverIcon,
width: specificMenuAction.hoverIconSize,
height: specificMenuAction.hoverIconSize,
colorFilter: specificMenuAction.hoverIconColor
.asFilter(),
fit: BoxFit.fill,
),
);
} else {
return const SizedBox.shrink();
}
valueListenable: _hoverController!.isHovering,
builder: (_, isHovering, __) {
if (!isHovering) return const SizedBox.shrink();
return Padding(
padding:
const EdgeInsetsDirectional.only(start: 16),
child: SvgPicture.asset(
specificMenuAction.hoverIcon,
width: specificMenuAction.hoverIconSize,
height: specificMenuAction.hoverIconSize,
colorFilter: specificMenuAction.hoverIconColor
.asFilter(),
fit: BoxFit.fill,
),
);
},
)
],
@@ -210,9 +206,7 @@ class _PopupMenuItemActionWidgetState extends State<PopupMenuItemActionWidget> {
onTap: () => widget.menuAction.onClick(widget.menuActionClick),
hoverColor: AppColor.popupMenuItemHovered,
onHover: (_) {
if (_hoverItemNotifier != null) {
_hoverItemNotifier?.value = false;
}
_hoverController?.exit();
widget.onHoverOtherItem?.call();
},
child: Container(
@@ -245,9 +239,9 @@ class _PopupMenuItemActionWidgetState extends State<PopupMenuItemActionWidget> {
@override
void dispose() {
if (_hoverItemNotifier != null) {
_hoverItemNotifier?.dispose();
_hoverItemNotifier = null;
if (_hoverController != null) {
_hoverController?.dispose();
_hoverController = null;
}
super.dispose();
}
@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
enum SubmenuDirection { left, right, auto }
@@ -18,7 +20,12 @@ class PopupSubmenuController {
}) {
hide();
final screenWidth = MediaQuery.of(context).size.width;
final overlayState = Overlay.maybeOf(context, rootOverlay: true);
if (overlayState == null) return;
final mediaSize = MediaQuery.sizeOf(context);
final screenWidth = mediaSize.width;
final screenHeight = mediaSize.height;
final rightPosition = anchor.right + offset;
final leftPosition = anchor.left - submenuWidth - offset;
@@ -42,10 +49,16 @@ class PopupSubmenuController {
}
}
final clampedLeft = finalLeft
.clamp(0.0, math.max(0.0, screenWidth - submenuWidth))
.toDouble();
final availableHeight = math.max(0.0, screenHeight - anchor.top);
final finalHeight = math.min(submenuMaxHeight, availableHeight);
_submenuEntry = OverlayEntry(
builder: (_) {
return PositionedDirectional(
start: finalLeft,
start: clampedLeft,
top: anchor.top,
child: MouseRegion(
onExit: (_) => hide(),
@@ -57,7 +70,7 @@ class PopupSubmenuController {
),
child: SizedBox(
width: submenuWidth,
height: submenuMaxHeight,
height: finalHeight,
child: submenu,
),
),
@@ -66,7 +79,7 @@ class PopupSubmenuController {
},
);
Overlay.maybeOf(context)?.insert(_submenuEntry!);
overlayState.insert(_submenuEntry!);
}
void hide() {