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:flutter/material.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:scribe/scribe/ai/presentation/model/ai_scribe_menu_action.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 Map<AIScribeMenuCategory, GlobalKey> _categoryKeys = {};
final GlobalKey _menuKey = GlobalKey(); final GlobalKey _menuKey = GlobalKey();
OverlayEntry? _submenuOverlay; OverlayEntry? _submenuOverlay;
Timer? _closeTimer;
@override @override
void initState() { void initState() {
@@ -36,6 +39,7 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
@override @override
void dispose() { void dispose() {
_closeTimer?.cancel();
_removeSubmenu(); _removeSubmenu();
super.dispose(); super.dispose();
} }
@@ -63,9 +67,11 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
position: menuPosition, position: menuPosition,
parentSize: menuSize, parentSize: menuSize,
onActionSelected: (action) { onActionSelected: (action) {
_closeTimer?.cancel();
_removeSubmenu(); _removeSubmenu();
widget.onActionSelected(action); widget.onActionSelected(action);
}, },
onHover: _cancelClose,
onDismiss: () { onDismiss: () {
setState(() { setState(() {
_hoveredCategory = null; _hoveredCategory = null;
@@ -78,18 +84,33 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
Overlay.of(context).insert(_submenuOverlay!); Overlay.of(context).insert(_submenuOverlay!);
} }
void _handleCategoryClick(AIScribeMenuCategory category) { void _handleCategoryHover(AIScribeMenuCategory category, bool isHovering) {
if (isHovering) {
_closeTimer?.cancel();
_closeTimer = null;
setState(() { setState(() {
// Toggle submenu - close if already open, open if closed
if (_hoveredCategory == category) {
_hoveredCategory = null;
_removeSubmenu();
} else {
_hoveredCategory = category; _hoveredCategory = category;
_showSubmenu(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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -112,7 +133,7 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
label: category.getLabel(context), label: category.getLabel(context),
hasSubmenu: true, hasSubmenu: true,
isHovered: _hoveredCategory == category, isHovered: _hoveredCategory == category,
onTap: () => _handleCategoryClick(category), onHover: (isHovering) => _handleCategoryHover(category, isHovering),
), ),
); );
} else { } else {
@@ -130,12 +151,16 @@ class _AIScribeMenuContentState extends State<AIScribeMenu> {
Widget _buildMenuItem({ Widget _buildMenuItem({
required String label, required String label,
required VoidCallback onTap, VoidCallback? onTap,
void Function(bool)? onHover,
bool hasSubmenu = false, bool hasSubmenu = false,
bool isHovered = false bool isHovered = false
}) { }) {
return SizedBox( return SizedBox(
height: AIScribeSizes.menuItemHeight, height: AIScribeSizes.menuItemHeight,
child: MouseRegion(
onEnter: onHover != null ? (_) => onHover(true) : null,
onExit: onHover != null ? (_) => onHover(false) : null,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(AIScribeSizes.menuItemBorderRadius), 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 Offset position;
final Size parentSize; final Size parentSize;
final Function(AIScribeMenuAction) onActionSelected; final Function(AIScribeMenuAction) onActionSelected;
final VoidCallback onHover;
final VoidCallback onDismiss; final VoidCallback onDismiss;
const _SubmenuPanel({ const _SubmenuPanel({
@@ -177,6 +204,7 @@ class _SubmenuPanel extends StatefulWidget {
required this.position, required this.position,
required this.parentSize, required this.parentSize,
required this.onActionSelected, required this.onActionSelected,
required this.onHover,
required this.onDismiss, required this.onDismiss,
}); });
@@ -209,6 +237,9 @@ class _SubmenuPanelState extends State<_SubmenuPanel> {
Positioned( Positioned(
left: adjustedLeft, left: adjustedLeft,
top: adjustedTop, top: adjustedTop,
child: MouseRegion(
onEnter: (_) => widget.onHover(),
onExit: (_) => widget.onDismiss(),
child: PointerInterceptor( child: PointerInterceptor(
child: Material( child: Material(
color: Colors.white, color: Colors.white,
@@ -244,6 +275,7 @@ class _SubmenuPanelState extends State<_SubmenuPanel> {
), ),
), ),
), ),
),
], ],
); );
} }