Show AI Scribe submenu on hover instead of on click

This commit is contained in:
Théo Poizat
2025-12-11 10:37:32 +01:00
committed by Dat H. Pham
parent 2f3bbe3bbb
commit 0b8a814ad6
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:scribe/scribe/ai/presentation/model/ai_scribe_menu_action.dart';
@@ -24,6 +26,7 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
final Map<AIScribeMenuCategory, GlobalKey> _categoryKeys = {};
final GlobalKey _menuKey = GlobalKey();
OverlayEntry? _submenuOverlay;
Timer? _closeTimer;
@override
void initState() {
@@ -36,6 +39,7 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
@override
void dispose() {
_closeTimer?.cancel();
_removeSubmenu();
super.dispose();
}
@@ -63,9 +67,11 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
position: menuPosition,
parentSize: menuSize,
onActionSelected: (action) {
_closeTimer?.cancel();
_removeSubmenu();
widget.onActionSelected(action);
},
onHover: _cancelClose,
onDismiss: () {
setState(() {
_hoveredCategory = null;
@@ -78,18 +84,33 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
Overlay.of(context).insert(_submenuOverlay!);
}
void _handleCategoryClick(AIScribeMenuCategory category) {
void _handleCategoryHover(AIScribeMenuCategory category, bool isHovering) {
if (isHovering) {
_closeTimer?.cancel();
_closeTimer = null;
setState(() {
// Toggle submenu - close if already open, open if closed
if (_hoveredCategory == category) {
_hoveredCategory = null;
_removeSubmenu();
} else {
_hoveredCategory = category;
_showSubmenu(category);
});
} else {
// Delay closing to allow mouse to reach submenu
_closeTimer?.cancel();
_closeTimer = Timer(const Duration(milliseconds: 150), () {
if (mounted && _hoveredCategory == category) {
setState(() {
_hoveredCategory = null;
_removeSubmenu();
});
}
});
}
}
void _cancelClose() {
_closeTimer?.cancel();
_closeTimer = null;
}
@override
Widget build(BuildContext context) {
@@ -112,7 +133,7 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
label: category.getLabel(context),
hasSubmenu: true,
isHovered: _hoveredCategory == category,
onTap: () => _handleCategoryClick(category),
onHover: (isHovering) => _handleCategoryHover(category, isHovering),
),
);
} else {
@@ -130,12 +151,16 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
Widget _buildMenuItem({
required String label,
required VoidCallback onTap,
VoidCallback? onTap,
void Function(bool)? onHover,
bool hasSubmenu = false,
bool isHovered = false
}) {
return SizedBox(
height: AIScribeSizes.menuItemHeight,
child: MouseRegion(
onEnter: onHover != null ? (_) => onHover(true) : null,
onExit: onHover != null ? (_) => onHover(false) : null,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AIScribeSizes.menuItemBorderRadius),
@@ -160,6 +185,7 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
),
),
),
),
);
}
}
@@ -170,6 +196,7 @@ class _SubmenuPanel extends StatefulWidget {
final Offset position;
final Size parentSize;
final Function(AIScribeMenuAction) onActionSelected;
final VoidCallback onHover;
final VoidCallback onDismiss;
const _SubmenuPanel({
@@ -177,6 +204,7 @@ class _SubmenuPanel extends StatefulWidget {
required this.position,
required this.parentSize,
required this.onActionSelected,
required this.onHover,
required this.onDismiss,
});
@@ -209,6 +237,9 @@ class _SubmenuPanelState extends State<_SubmenuPanel> {
Positioned(
left: adjustedLeft,
top: adjustedTop,
child: MouseRegion(
onEnter: (_) => widget.onHover(),
onExit: (_) => widget.onDismiss(),
child: PointerInterceptor(
child: Material(
color: Colors.white,
@@ -244,6 +275,7 @@ class _SubmenuPanelState extends State<_SubmenuPanel> {
),
),
),
),
],
);
}