Show AI Scribe submenu on hover instead of on click
This commit is contained in:
@@ -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> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user