TF-4195 Show submenu when hover Label As item
This commit is contained in:
@@ -3,16 +3,23 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
typedef OnPopupMenuActionClick = void Function(PopupMenuItemAction action);
|
typedef OnPopupMenuActionClick = void Function(PopupMenuItemAction action);
|
||||||
|
typedef OnHoverShowSubmenu = void Function(GlobalKey key);
|
||||||
|
|
||||||
abstract class PopupMenuItemAction<T> with EquatableMixin {
|
abstract class PopupMenuItemAction<T> with EquatableMixin {
|
||||||
final T action;
|
final T action;
|
||||||
final String? key;
|
final String? key;
|
||||||
final int category;
|
final int category;
|
||||||
|
final Widget? submenu;
|
||||||
|
|
||||||
PopupMenuItemAction(this.action, {this.key, this.category = -1});
|
PopupMenuItemAction(
|
||||||
|
this.action, {
|
||||||
|
this.key,
|
||||||
|
this.category = -1,
|
||||||
|
this.submenu,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [action, key, category];
|
List<Object?> get props => [action, key, category, submenu];
|
||||||
|
|
||||||
String get actionName;
|
String get actionName;
|
||||||
|
|
||||||
@@ -40,9 +47,22 @@ mixin OptionalPopupSelectedIcon<T> {
|
|||||||
double get selectedIconSize => 16.0;
|
double get selectedIconSize => 16.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mixin OptionalPopupHoverIcon {
|
||||||
|
String get hoverIcon;
|
||||||
|
|
||||||
|
Color get hoverIconColor => AppColor.steelGrayA540;
|
||||||
|
|
||||||
|
double get hoverIconSize => 16.0;
|
||||||
|
}
|
||||||
|
|
||||||
abstract class PopupMenuItemActionRequiredIcon<T> extends PopupMenuItemAction<T>
|
abstract class PopupMenuItemActionRequiredIcon<T> extends PopupMenuItemAction<T>
|
||||||
with OptionalPopupIcon {
|
with OptionalPopupIcon, OptionalPopupHoverIcon {
|
||||||
PopupMenuItemActionRequiredIcon(super.action, {super.key, super.category});
|
PopupMenuItemActionRequiredIcon(
|
||||||
|
super.action, {
|
||||||
|
super.key,
|
||||||
|
super.category,
|
||||||
|
super.submenu,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class PopupMenuItemActionRequiredSelectedIcon<T>
|
abstract class PopupMenuItemActionRequiredSelectedIcon<T>
|
||||||
@@ -54,6 +74,7 @@ abstract class PopupMenuItemActionRequiredSelectedIcon<T>
|
|||||||
this.selectedAction, {
|
this.selectedAction, {
|
||||||
super.key,
|
super.key,
|
||||||
super.category,
|
super.category,
|
||||||
|
super.submenu,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +87,7 @@ abstract class PopupMenuItemActionRequiredFull<T> extends PopupMenuItemAction<T>
|
|||||||
this.selectedAction, {
|
this.selectedAction, {
|
||||||
super.key,
|
super.key,
|
||||||
super.category,
|
super.category,
|
||||||
|
super.submenu,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,5 +101,6 @@ abstract class PopupMenuItemActionRequiredIconWithMultipleSelected<T>
|
|||||||
this.selectedActions, {
|
this.selectedActions, {
|
||||||
super.key,
|
super.key,
|
||||||
super.category,
|
super.category,
|
||||||
|
super.submenu,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:tmail_ui_user/features/base/extensions/popup_menu_action_list_ex
|
|||||||
import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart';
|
import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart';
|
||||||
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
|
import 'package:tmail_ui_user/features/base/model/popup_menu_item_action.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_item_action_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_submenu_controller.dart';
|
||||||
|
|
||||||
typedef OnPopupMenuActionSelected = void Function(PopupMenuItemAction action);
|
typedef OnPopupMenuActionSelected = void Function(PopupMenuItemAction action);
|
||||||
|
|
||||||
@@ -11,11 +12,13 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
|
|||||||
final List<PopupMenuItemAction> actions;
|
final List<PopupMenuItemAction> actions;
|
||||||
final OnPopupMenuActionSelected onActionSelected;
|
final OnPopupMenuActionSelected onActionSelected;
|
||||||
final double dividerOpacity;
|
final double dividerOpacity;
|
||||||
|
final PopupSubmenuController? submenuController;
|
||||||
|
|
||||||
const PopupMenuActionGroupWidget({
|
PopupMenuActionGroupWidget({
|
||||||
required this.actions,
|
required this.actions,
|
||||||
required this.onActionSelected,
|
required this.onActionSelected,
|
||||||
this.dividerOpacity = 0.12,
|
this.dividerOpacity = 0.12,
|
||||||
|
this.submenuController,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> show(
|
Future<void> show(
|
||||||
@@ -37,6 +40,15 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
onActionSelected(menuAction);
|
onActionSelected(menuAction);
|
||||||
},
|
},
|
||||||
|
onHoverShowSubmenu: submenuController != null && menuAction.submenu != null
|
||||||
|
? (itemKey) => _showPopupSubmenu(
|
||||||
|
context: context,
|
||||||
|
itemKey: itemKey,
|
||||||
|
submenuController: submenuController!,
|
||||||
|
submenu: menuAction.submenu!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onHoverOtherItem: submenuController?.hide,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -50,4 +62,23 @@ class PopupMenuActionGroupWidget with PopupContextMenuActionMixin {
|
|||||||
|
|
||||||
return openPopupMenuAction(context, position, popupMenuItems);
|
return openPopupMenuAction(context, position, popupMenuItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showPopupSubmenu({
|
||||||
|
required BuildContext context,
|
||||||
|
required GlobalKey itemKey,
|
||||||
|
required PopupSubmenuController submenuController,
|
||||||
|
required Widget submenu,
|
||||||
|
}) {
|
||||||
|
final renderBox = itemKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
if (renderBox == null) return;
|
||||||
|
|
||||||
|
final offset = renderBox.localToGlobal(Offset.zero);
|
||||||
|
final rect = offset & renderBox.size;
|
||||||
|
|
||||||
|
submenuController.show(
|
||||||
|
context: context,
|
||||||
|
anchor: rect,
|
||||||
|
submenu: submenu,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,52 @@ import 'package:core/presentation/extensions/color_extension.dart';
|
|||||||
import 'package:core/presentation/utils/theme_utils.dart';
|
import 'package:core/presentation/utils/theme_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:model/email/email_action_type.dart';
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.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/model/popup_menu_item_action.dart';
|
||||||
|
import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_email_action.dart';
|
||||||
|
|
||||||
class PopupMenuItemActionWidget extends StatelessWidget {
|
class PopupMenuItemActionWidget extends StatefulWidget {
|
||||||
final PopupMenuItemAction menuAction;
|
final PopupMenuItemAction menuAction;
|
||||||
final OnPopupMenuActionClick menuActionClick;
|
final OnPopupMenuActionClick menuActionClick;
|
||||||
|
final OnHoverShowSubmenu? onHoverShowSubmenu;
|
||||||
|
final VoidCallback? onHoverOtherItem;
|
||||||
|
|
||||||
const PopupMenuItemActionWidget({
|
const PopupMenuItemActionWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.menuAction,
|
required this.menuAction,
|
||||||
required this.menuActionClick,
|
required this.menuActionClick,
|
||||||
|
this.onHoverShowSubmenu,
|
||||||
|
this.onHoverOtherItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PopupMenuItemActionWidget> createState() =>
|
||||||
|
_PopupMenuItemActionWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopupMenuItemActionWidgetState extends State<PopupMenuItemActionWidget> {
|
||||||
|
GlobalKey? _itemKey;
|
||||||
|
ValueNotifier<bool>? _hoverItemNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.menuAction is PopupMenuItemEmailAction) {
|
||||||
|
_itemKey = GlobalKey();
|
||||||
|
_hoverItemNotifier = ValueNotifier(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget? iconWidget;
|
Widget? iconWidget;
|
||||||
Widget? selectedIconWidget;
|
Widget? selectedIconWidget;
|
||||||
bool isSelected = false;
|
bool isSelected = false;
|
||||||
|
|
||||||
if (menuAction is PopupMenuItemActionRequiredIcon) {
|
if (widget.menuAction is PopupMenuItemActionRequiredIcon) {
|
||||||
final specificMenuAction = menuAction as PopupMenuItemActionRequiredIcon;
|
final specificMenuAction =
|
||||||
|
widget.menuAction as PopupMenuItemActionRequiredIcon;
|
||||||
iconWidget = Padding(
|
iconWidget = Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
@@ -33,9 +58,9 @@ class PopupMenuItemActionWidget extends StatelessWidget {
|
|||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (menuAction is PopupMenuItemActionRequiredSelectedIcon) {
|
} else if (widget.menuAction is PopupMenuItemActionRequiredSelectedIcon) {
|
||||||
final specificMenuAction =
|
final specificMenuAction =
|
||||||
menuAction as PopupMenuItemActionRequiredSelectedIcon;
|
widget.menuAction as PopupMenuItemActionRequiredSelectedIcon;
|
||||||
selectedIconWidget = Padding(
|
selectedIconWidget = Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(start: 16),
|
padding: const EdgeInsetsDirectional.only(start: 16),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
@@ -46,34 +71,11 @@ class PopupMenuItemActionWidget extends StatelessWidget {
|
|||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
isSelected = specificMenuAction.selectedAction == menuAction.action;
|
isSelected =
|
||||||
} else if (menuAction is PopupMenuItemActionRequiredFull) {
|
specificMenuAction.selectedAction == widget.menuAction.action;
|
||||||
final specificMenuAction = menuAction as PopupMenuItemActionRequiredFull;
|
} else if (widget.menuAction is PopupMenuItemActionRequiredFull) {
|
||||||
iconWidget = Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
specificMenuAction.actionIcon,
|
|
||||||
width: specificMenuAction.actionIconSize,
|
|
||||||
height: specificMenuAction.actionIconSize,
|
|
||||||
colorFilter: specificMenuAction.actionIconColor.asFilter(),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
selectedIconWidget = Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.only(start: 16),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
specificMenuAction.selectedIcon,
|
|
||||||
width: specificMenuAction.selectedIconSize,
|
|
||||||
height: specificMenuAction.selectedIconSize,
|
|
||||||
colorFilter: specificMenuAction.selectedIconColor.asFilter(),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
isSelected = specificMenuAction.selectedAction == menuAction.action;
|
|
||||||
} else if (menuAction
|
|
||||||
is PopupMenuItemActionRequiredIconWithMultipleSelected) {
|
|
||||||
final specificMenuAction =
|
final specificMenuAction =
|
||||||
menuAction as PopupMenuItemActionRequiredIconWithMultipleSelected;
|
widget.menuAction as PopupMenuItemActionRequiredFull;
|
||||||
iconWidget = Padding(
|
iconWidget = Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
@@ -95,27 +97,137 @@ class PopupMenuItemActionWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
isSelected =
|
isSelected =
|
||||||
specificMenuAction.selectedActions.contains(menuAction.action);
|
specificMenuAction.selectedAction == widget.menuAction.action;
|
||||||
|
} else if (widget.menuAction
|
||||||
|
is PopupMenuItemActionRequiredIconWithMultipleSelected) {
|
||||||
|
final specificMenuAction = widget.menuAction
|
||||||
|
as PopupMenuItemActionRequiredIconWithMultipleSelected;
|
||||||
|
iconWidget = Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
specificMenuAction.actionIcon,
|
||||||
|
width: specificMenuAction.actionIconSize,
|
||||||
|
height: specificMenuAction.actionIconSize,
|
||||||
|
colorFilter: specificMenuAction.actionIconColor.asFilter(),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
selectedIconWidget = Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(start: 16),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
specificMenuAction.selectedIcon,
|
||||||
|
width: specificMenuAction.selectedIconSize,
|
||||||
|
height: specificMenuAction.selectedIconSize,
|
||||||
|
colorFilter: specificMenuAction.selectedIconColor.asFilter(),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
isSelected =
|
||||||
|
specificMenuAction.selectedActions.contains(widget.menuAction.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.menuAction is PopupMenuItemEmailAction &&
|
||||||
|
widget.menuAction.action == EmailActionType.labelAs) {
|
||||||
|
final specificMenuAction = widget.menuAction as PopupMenuItemEmailAction;
|
||||||
|
|
||||||
|
return PointerInterceptor(
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (_) {
|
||||||
|
if (_hoverItemNotifier != null) {
|
||||||
|
_hoverItemNotifier?.value = true;
|
||||||
|
}
|
||||||
|
if (_itemKey != null) {
|
||||||
|
widget.onHoverShowSubmenu?.call(_itemKey!);
|
||||||
|
} else {
|
||||||
|
widget.onHoverOtherItem?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onExit: (_) {
|
||||||
|
if (_hoverItemNotifier != null) {
|
||||||
|
_hoverItemNotifier?.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => specificMenuAction.onClick(widget.menuActionClick),
|
||||||
|
hoverColor: AppColor.popupMenuItemHovered,
|
||||||
|
child: Container(
|
||||||
|
key: _itemKey,
|
||||||
|
height: 48,
|
||||||
|
width: double.infinity,
|
||||||
|
padding: specificMenuAction.itemPadding,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (iconWidget != null) iconWidget,
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
specificMenuAction.actionName,
|
||||||
|
style: ThemeUtils.textStyleBodyBody3(
|
||||||
|
color: specificMenuAction.actionNameColor,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSelected && selectedIconWidget != null)
|
||||||
|
selectedIconWidget,
|
||||||
|
if (_hoverItemNotifier != 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PointerInterceptor(
|
return PointerInterceptor(
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => menuAction.onClick(menuActionClick),
|
onTap: () => widget.menuAction.onClick(widget.menuActionClick),
|
||||||
hoverColor: AppColor.popupMenuItemHovered,
|
hoverColor: AppColor.popupMenuItemHovered,
|
||||||
|
onHover: (_) {
|
||||||
|
if (_hoverItemNotifier != null) {
|
||||||
|
_hoverItemNotifier?.value = false;
|
||||||
|
}
|
||||||
|
widget.onHoverOtherItem?.call();
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
|
key: _itemKey,
|
||||||
height: 48,
|
height: 48,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: menuAction.itemPadding,
|
padding: widget.menuAction.itemPadding,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (iconWidget != null) iconWidget,
|
if (iconWidget != null) iconWidget,
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
menuAction.actionName,
|
widget.menuAction.actionName,
|
||||||
style: ThemeUtils.textStyleBodyBody3(
|
style: ThemeUtils.textStyleBodyBody3(
|
||||||
color: menuAction.actionNameColor,
|
color: widget.menuAction.actionNameColor,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -130,4 +242,13 @@ class PopupMenuItemActionWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_hoverItemNotifier != null) {
|
||||||
|
_hoverItemNotifier?.dispose();
|
||||||
|
_hoverItemNotifier = null;
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
enum SubmenuDirection { left, right, auto }
|
||||||
|
|
||||||
|
class PopupSubmenuController {
|
||||||
|
OverlayEntry? _submenuEntry;
|
||||||
|
|
||||||
|
bool get isShowing => _submenuEntry != null;
|
||||||
|
|
||||||
|
void show({
|
||||||
|
required BuildContext context,
|
||||||
|
required Rect anchor,
|
||||||
|
required Widget submenu,
|
||||||
|
SubmenuDirection direction = SubmenuDirection.auto,
|
||||||
|
double submenuWidth = 249,
|
||||||
|
double submenuMaxHeight = 400,
|
||||||
|
double offset = 0,
|
||||||
|
}) {
|
||||||
|
hide();
|
||||||
|
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
final rightPosition = anchor.right + offset;
|
||||||
|
final leftPosition = anchor.left - submenuWidth - offset;
|
||||||
|
|
||||||
|
bool canShowRight = rightPosition + submenuWidth <= screenWidth;
|
||||||
|
bool canShowLeft = leftPosition >= 0;
|
||||||
|
|
||||||
|
double? finalLeft;
|
||||||
|
|
||||||
|
if (direction == SubmenuDirection.right) {
|
||||||
|
finalLeft = rightPosition;
|
||||||
|
} else if (direction == SubmenuDirection.left) {
|
||||||
|
finalLeft = leftPosition;
|
||||||
|
} else {
|
||||||
|
if (canShowRight) {
|
||||||
|
finalLeft = rightPosition;
|
||||||
|
} else if (canShowLeft) {
|
||||||
|
finalLeft = leftPosition;
|
||||||
|
} else {
|
||||||
|
finalLeft = rightPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_submenuEntry = OverlayEntry(
|
||||||
|
builder: (_) {
|
||||||
|
return Positioned(
|
||||||
|
left: finalLeft,
|
||||||
|
top: anchor.top,
|
||||||
|
child: MouseRegion(
|
||||||
|
onExit: (_) => hide(),
|
||||||
|
child: Material(
|
||||||
|
elevation: 8,
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: submenuWidth,
|
||||||
|
height: submenuMaxHeight,
|
||||||
|
child: submenu,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Overlay.of(context).insert(_submenuEntry!);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hide() {
|
||||||
|
_submenuEntry?.remove();
|
||||||
|
_submenuEntry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
additionalActions: [],
|
additionalActions: [],
|
||||||
emailIsRead: presentationEmail.hasRead,
|
emailIsRead: presentationEmail.hasRead,
|
||||||
isLabelFeatureEnabled: controller.isLabelFeatureEnabled,
|
isLabelFeatureEnabled: controller.isLabelFeatureEnabled,
|
||||||
|
labels: controller.mailboxDashBoardController.labelController.labels,
|
||||||
openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu,
|
openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu,
|
||||||
openPopupMenu: controller.mailboxDashBoardController.openPopupMenuActionGroup,
|
openPopupMenu: controller.mailboxDashBoardController.openPopupMenuActionGroup,
|
||||||
);
|
);
|
||||||
@@ -309,6 +310,7 @@ class EmailView extends GetWidget<SingleEmailController> {
|
|||||||
],
|
],
|
||||||
emailIsRead: presentationEmail.hasRead,
|
emailIsRead: presentationEmail.hasRead,
|
||||||
isLabelFeatureEnabled: controller.isLabelFeatureEnabled,
|
isLabelFeatureEnabled: controller.isLabelFeatureEnabled,
|
||||||
|
labels: controller.mailboxDashBoardController.labelController.labels,
|
||||||
openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu,
|
openBottomSheetContextMenu: controller.mailboxDashBoardController.openBottomSheetContextMenu,
|
||||||
openPopupMenu: controller.mailboxDashBoardController.openPopupMenuActionGroup,
|
openPopupMenu: controller.mailboxDashBoardController.openPopupMenuActionGroup,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class PopupMenuItemEmailAction
|
|||||||
this.imagePaths, {
|
this.imagePaths, {
|
||||||
super.key,
|
super.key,
|
||||||
super.category,
|
super.category,
|
||||||
|
super.submenu,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,4 +30,7 @@ class PopupMenuItemEmailAction
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Color get actionNameColor => action.getPopupMenuTitleColor();
|
Color get actionNameColor => action.getPopupMenuTitleColor();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get hoverIcon => imagePaths.icThumbsUp;
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-1
@@ -20,6 +20,7 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
|||||||
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
|
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
|
||||||
|
import 'package:labels/model/label.dart';
|
||||||
import 'package:model/email/email_action_type.dart';
|
import 'package:model/email/email_action_type.dart';
|
||||||
import 'package:model/email/mark_star_action.dart';
|
import 'package:model/email/mark_star_action.dart';
|
||||||
import 'package:model/email/presentation_email.dart';
|
import 'package:model/email/presentation_email.dart';
|
||||||
@@ -32,6 +33,7 @@ import 'package:model/mailbox/presentation_mailbox.dart';
|
|||||||
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart';
|
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_manager.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/context_menu/context_menu_item_action.dart';
|
import 'package:tmail_ui_user/features/base/widget/context_menu/context_menu_item_action.dart';
|
||||||
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_action_group_widget.dart';
|
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_menu_action_group_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/base/widget/popup_menu/popup_submenu_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart';
|
import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart';
|
||||||
import 'package:tmail_ui_user/features/email/domain/model/email_print.dart';
|
import 'package:tmail_ui_user/features/email/domain/model/email_print.dart';
|
||||||
import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart';
|
import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart';
|
||||||
@@ -50,6 +52,7 @@ import 'package:tmail_ui_user/features/email/presentation/model/popup_menu_item_
|
|||||||
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart';
|
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart';
|
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart';
|
||||||
|
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_list_context_menu.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart';
|
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart';
|
||||||
import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart';
|
import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart';
|
||||||
import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart';
|
import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart';
|
||||||
@@ -486,6 +489,7 @@ class EmailActionReactor {
|
|||||||
required bool isLabelFeatureEnabled,
|
required bool isLabelFeatureEnabled,
|
||||||
required OpenBottomSheetContextMenuAction openBottomSheetContextMenu,
|
required OpenBottomSheetContextMenuAction openBottomSheetContextMenu,
|
||||||
required OpenPopupMenuActionGroup openPopupMenu,
|
required OpenPopupMenuActionGroup openPopupMenu,
|
||||||
|
List<Label>? labels,
|
||||||
}) {
|
}) {
|
||||||
if (currentContext == null) return;
|
if (currentContext == null) return;
|
||||||
|
|
||||||
@@ -566,20 +570,49 @@ class EmailActionReactor {
|
|||||||
imagePaths,
|
imagePaths,
|
||||||
key: '${actionType.name}_action',
|
key: '${actionType.name}_action',
|
||||||
category: actionType.category,
|
category: actionType.category,
|
||||||
|
submenu: _getEmailActionSubmenu(
|
||||||
|
actionType: actionType,
|
||||||
|
imagePaths: imagePaths,
|
||||||
|
presentationEmail: presentationEmail,
|
||||||
|
labels: labels,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
final submenuController = PopupSubmenuController();
|
||||||
|
|
||||||
final popupMenuWidget = PopupMenuActionGroupWidget(
|
final popupMenuWidget = PopupMenuActionGroupWidget(
|
||||||
actions: popupMenuItemEmailActions,
|
actions: popupMenuItemEmailActions,
|
||||||
|
submenuController: submenuController,
|
||||||
onActionSelected: (action) {
|
onActionSelected: (action) {
|
||||||
handleEmailAction(presentationEmail, action.action);
|
handleEmailAction(presentationEmail, action.action);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
openPopupMenu(currentContext!, position, popupMenuWidget);
|
openPopupMenu(
|
||||||
|
currentContext!,
|
||||||
|
position,
|
||||||
|
popupMenuWidget,
|
||||||
|
).whenComplete(submenuController.hide);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget? _getEmailActionSubmenu({
|
||||||
|
required EmailActionType actionType,
|
||||||
|
required ImagePaths imagePaths,
|
||||||
|
required PresentationEmail presentationEmail,
|
||||||
|
required List<Label>? labels,
|
||||||
|
}) {
|
||||||
|
if (actionType == EmailActionType.labelAs) {
|
||||||
|
return LabelListContextMenu(
|
||||||
|
labelList: labels ?? [],
|
||||||
|
presentationEmail: presentationEmail,
|
||||||
|
imagePaths: imagePaths,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
bool _canDeletePermanently(
|
bool _canDeletePermanently(
|
||||||
PresentationEmail email,
|
PresentationEmail email,
|
||||||
PresentationMailbox? mailboxContain,
|
PresentationMailbox? mailboxContain,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
|
import 'package:core/presentation/utils/theme_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:labels/extensions/label_extension.dart';
|
||||||
|
import 'package:labels/model/label.dart';
|
||||||
|
|
||||||
|
class LabelItemContextMenu extends StatelessWidget {
|
||||||
|
final Label label;
|
||||||
|
final ImagePaths imagePaths;
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
const LabelItemContextMenu({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.imagePaths,
|
||||||
|
required this.isSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {},
|
||||||
|
child: Container(
|
||||||
|
height: 48,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
isSelected
|
||||||
|
? imagePaths.icCheckboxSelected
|
||||||
|
: imagePaths.icCheckboxUnselected,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
colorFilter: isSelected
|
||||||
|
? AppColor.primaryMain.asFilter()
|
||||||
|
: AppColor.steelGrayA540.asFilter(),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label.safeDisplayName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: ThemeUtils.textStyleBodyBody3(color: Colors.black),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:labels/model/label.dart';
|
||||||
|
import 'package:model/email/presentation_email.dart';
|
||||||
|
import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart';
|
||||||
|
import 'package:tmail_ui_user/features/labels/presentation/widgets/label_item_context_menu.dart';
|
||||||
|
|
||||||
|
class LabelListContextMenu extends StatelessWidget {
|
||||||
|
final PresentationEmail presentationEmail;
|
||||||
|
final List<Label> labelList;
|
||||||
|
final ImagePaths imagePaths;
|
||||||
|
|
||||||
|
const LabelListContextMenu({
|
||||||
|
super.key,
|
||||||
|
required this.labelList,
|
||||||
|
required this.presentationEmail,
|
||||||
|
required this.imagePaths,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final emailLabels = presentationEmail.getLabelList(labelList);
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: labelList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final label = labelList[index];
|
||||||
|
final isSelected = emailLabels.contains(label);
|
||||||
|
return LabelItemContextMenu(
|
||||||
|
label: label,
|
||||||
|
imagePaths: imagePaths,
|
||||||
|
isSelected: isSelected,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,4 +26,7 @@ class PopupMenuItemMailboxAction
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Color get actionNameColor => action.getPopupMenuTitleColor();
|
Color get actionNameColor => action.getPopupMenuTitleColor();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get hoverIcon => imagePaths.icThumbsUp;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -27,4 +27,7 @@ class PopupMenuItemProfileSettingTypeAction
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
EdgeInsetsGeometry get itemPadding => const EdgeInsets.symmetric(horizontal: 16);
|
EdgeInsetsGeometry get itemPadding => const EdgeInsets.symmetric(horizontal: 16);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get hoverIcon => imagePaths.icThumbsUp;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user