diff --git a/lib/features/erd/widgets/erd_export_menu.dart b/lib/features/erd/widgets/erd_export_menu.dart index 7ca93168..d69aa4d1 100644 --- a/lib/features/erd/widgets/erd_export_menu.dart +++ b/lib/features/erd/widgets/erd_export_menu.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart' as material; -import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; -import 'package:querya_desktop/shared/widgets/querya_action_menu.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:querya_desktop/shared/widgets/querya_action_menu.dart' + show QueryaActionMenuItem; +import 'package:querya_desktop/shared/widgets/querya_menu_button.dart'; /// Entries of the diagram's Export menu. enum ErdExportAction { @@ -24,8 +24,8 @@ class ErdExportMenu extends material.StatelessWidget { @override material.Widget build(material.BuildContext context) { - final wb = context.workbench; - return QueryaActionMenu( + return QueryaMenuButton( + key: const material.ValueKey('erd_export'), items: const [ QueryaActionMenuItem( value: ErdExportAction.toggleTheme, @@ -74,23 +74,8 @@ class ErdExportMenu extends material.StatelessWidget { ), ], onSelected: onSelected, - child: material.Padding( - key: const material.ValueKey('erd_export'), - padding: - const material.EdgeInsets.symmetric(horizontal: 10, vertical: 6), - child: material.Row( - mainAxisSize: material.MainAxisSize.min, - children: [ - material.Icon(material.Icons.file_download_outlined, - size: 16, color: wb.mutedForeground), - const material.SizedBox(width: 6), - const Text('Export'), - const material.SizedBox(width: 4), - material.Icon(material.Icons.expand_more_rounded, - size: 16, color: wb.mutedForeground), - ], - ), - ), + label: 'Export', + icon: material.Icons.file_download_outlined, ); } } diff --git a/lib/features/erd/widgets/erd_views_menu.dart b/lib/features/erd/widgets/erd_views_menu.dart index 7764fad1..6b20ab24 100644 --- a/lib/features/erd/widgets/erd_views_menu.dart +++ b/lib/features/erd/widgets/erd_views_menu.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/erd/erd_saved_layout.dart'; -import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; -import 'package:querya_desktop/shared/widgets/querya_action_menu.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:querya_desktop/shared/widgets/querya_action_menu.dart' + show QueryaActionMenuItem; +import 'package:querya_desktop/shared/widgets/querya_menu_button.dart'; /// The toolbar's view switcher: All tables, the saved views, and the /// commands to save, rename and delete one. @@ -34,9 +34,9 @@ class ErdViewsMenu extends material.StatelessWidget { @override material.Widget build(material.BuildContext context) { - final wb = context.workbench; final current = active; - return QueryaActionMenu( + return QueryaMenuButton( + key: const material.ValueKey('erd_views'), items: [ // A mark on every row, so the labels line up. QueryaActionMenuItem( @@ -86,23 +86,8 @@ class ErdViewsMenu extends material.StatelessWidget { if (v.startsWith('view:')) onSwitch(v.substring(5)); } }, - child: material.Padding( - key: const material.ValueKey('erd_views'), - padding: - const material.EdgeInsets.symmetric(horizontal: 10, vertical: 6), - child: material.Row( - mainAxisSize: material.MainAxisSize.min, - children: [ - material.Icon(material.Icons.bookmarks_outlined, - size: 16, color: wb.mutedForeground), - const material.SizedBox(width: 6), - Text(current == null ? 'All tables' : shortName(current.name)), - const material.SizedBox(width: 4), - material.Icon(material.Icons.expand_more_rounded, - size: 16, color: wb.mutedForeground), - ], - ), - ), + label: current == null ? 'All tables' : shortName(current.name), + icon: material.Icons.bookmarks_outlined, ); } } diff --git a/lib/features/results/charts/quick_chart_view.dart b/lib/features/results/charts/quick_chart_view.dart index 07ff4772..145b2f24 100644 --- a/lib/features/results/charts/quick_chart_view.dart +++ b/lib/features/results/charts/quick_chart_view.dart @@ -13,8 +13,8 @@ import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/features/results/charts/chart_data.dart'; import 'package:querya_desktop/features/results/charts/chart_format.dart'; import 'package:querya_desktop/features/results/charts/chart_svg.dart'; -import 'package:querya_desktop/shared/widgets/querya_action_menu.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; +import 'package:querya_desktop/core/ui/querya_control_tokens.dart'; /// Formats of the chart export menu. enum _ChartExport { png, svg, toggleTheme } @@ -379,7 +379,8 @@ class _QuickChartViewState extends material.State { ), const material.SizedBox(width: 8), ], - QueryaActionMenu<_ChartExport>( + QueryaMenuButton<_ChartExport>( + key: const material.ValueKey('chart_export'), items: const [ QueryaActionMenuItem( value: _ChartExport.png, @@ -406,22 +407,10 @@ class _QuickChartViewState extends material.State { _exportSvg(); } }, - child: material.Padding( - key: const material.ValueKey('chart_export'), - padding: const material.EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - child: material.Row( - mainAxisSize: material.MainAxisSize.min, - children: [ - material.Icon(material.Icons.file_download_outlined, - size: 16, color: wb.mutedForeground), - const material.SizedBox(width: 4), - const Text('Export'), - material.Icon(material.Icons.expand_more_rounded, - size: 16, color: wb.mutedForeground), - ], - ), - ), + label: 'Export', + icon: material.Icons.file_download_outlined, + variant: QueryaButtonVariant.ghost, + size: QueryaControlSize.sm, ), ], ), diff --git a/lib/features/workspace/sql_workspace_toolbar.dart b/lib/features/workspace/sql_workspace_toolbar.dart index c058303e..c8c7de94 100644 --- a/lib/features/workspace/sql_workspace_toolbar.dart +++ b/lib/features/workspace/sql_workspace_toolbar.dart @@ -5,7 +5,6 @@ import 'package:querya_desktop/features/settings/preferences_dialog.dart'; import 'package:querya_desktop/features/settings/sql_statement_timeout_dropdown.dart'; import 'package:querya_desktop/features/workspace/sql_execution_delegate.dart'; import 'package:querya_desktop/features/workspace/sql_query_tab_session.dart'; -import 'package:querya_desktop/shared/widgets/querya_action_menu.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Choices for the session options dropdown. @@ -147,7 +146,6 @@ class SqlWorkspaceToolbar extends StatelessWidget { @override material.Widget build(material.BuildContext context) { - final accent = context.workbench.accent; return material.LayoutBuilder( builder: (context, constraints) { final compact = constraints.maxWidth < compactWidthBreakpoint; @@ -159,17 +157,15 @@ class SqlWorkspaceToolbar extends StatelessWidget { headerBadge!, const material.SizedBox(width: 12), ], - QueryaActionButton( - key: const material.ValueKey('run_script'), + QueryaSplitButton( + mainKey: const material.ValueKey('run_script'), + menuKey: const material.ValueKey('run_menu'), label: 'Execute (F5)', icon: material.Icons.play_arrow_rounded, compact: compact, loading: session.running, onPressed: () => onExecute(false), - ), - const material.SizedBox(width: 4), - QueryaActionMenu( - key: const material.ValueKey('run_menu'), + menuTooltip: 'Run options', items: [ QueryaActionMenuItem( value: true, @@ -187,10 +183,6 @@ class SqlWorkspaceToolbar extends StatelessWidget { onExecute(atCursor); } }, - child: const material.Icon( - material.Icons.expand_more_rounded, - size: 16, - ), ), if (delegate.supportsExplain) ...[ const material.SizedBox(width: 8), @@ -267,44 +259,15 @@ class SqlWorkspaceToolbar extends StatelessWidget { ], const material.SizedBox(width: 8), ], - material.IgnorePointer( - ignoring: session.running, - child: material.Opacity( - opacity: session.running ? 0.5 : 1, - child: QueryaActionMenu( - key: const material.ValueKey('session_menu'), - items: _sessionMenuItems(), - onSelected: (choice) => _onSessionChoice(context, choice), - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - child: material.Row( - mainAxisSize: material.MainAxisSize.min, - children: [ - material.Icon( - material.Icons.tune_rounded, - size: 16, - color: accent, - ), - const material.SizedBox(width: 6), - if (!compact) ...[ - Text(autocommit - ? 'Session · auto-commit' - : 'Session · manual'), - const material.SizedBox(width: 4), - ], - material.Icon( - material.Icons.expand_more_rounded, - size: 16, - color: accent, - ), - ], - ), - ), - ), - ), + QueryaMenuButton( + key: const material.ValueKey('session_menu'), + items: _sessionMenuItems(), + onSelected: (choice) => _onSessionChoice(context, choice), + enabled: !session.running, + icon: material.Icons.tune_rounded, + label: compact + ? null + : (autocommit ? 'Session · auto-commit' : 'Session · manual'), ), if (extraTrailing != null) ...[ const material.SizedBox(width: 8), diff --git a/lib/shared/widgets/export_menu_button.dart b/lib/shared/widgets/export_menu_button.dart index af5c7a86..89d02429 100644 --- a/lib/shared/widgets/export_menu_button.dart +++ b/lib/shared/widgets/export_menu_button.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/ui/querya_control_tokens.dart'; import 'package:querya_desktop/shared/services/data_export_service.dart'; -import 'package:querya_desktop/shared/widgets/querya_action_menu.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:querya_desktop/shared/widgets/querya_action_menu.dart' + show QueryaActionMenuItem; +import 'package:querya_desktop/shared/widgets/querya_menu_button.dart'; -class ExportMenuButton extends StatelessWidget { +/// The Export / Copy menu of result and table toolbars: CSV, JSON, Markdown +/// and SQL dump, saved to a file or copied ([isSave]). +class ExportMenuButton extends material.StatelessWidget { const ExportMenuButton({ super.key, required this.label, @@ -18,8 +22,11 @@ class ExportMenuButton extends StatelessWidget { final bool isSave; @override - Widget build(BuildContext context) { - return QueryaActionMenu( + material.Widget build(material.BuildContext context) { + return QueryaMenuButton( + label: label, + icon: icon, + size: QueryaControlSize.sm, onSelected: onSelected, items: [ QueryaActionMenuItem( @@ -43,14 +50,6 @@ class ExportMenuButton extends StatelessWidget { label: isSave ? 'SQL INSERT Dump (.sql)' : 'Copy as SQL Dump', ), ], - child: material.Row( - mainAxisSize: material.MainAxisSize.min, - children: [ - material.Icon(icon, size: 14), - const material.SizedBox(width: 6), - Text(label), - ], - ), ); } } diff --git a/lib/shared/widgets/querya_action_menu.dart b/lib/shared/widgets/querya_action_menu.dart index 0d32c025..c106b63f 100644 --- a/lib/shared/widgets/querya_action_menu.dart +++ b/lib/shared/widgets/querya_action_menu.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart' as material; -import 'package:querya_desktop/core/layout/ui_scale.dart'; -import 'package:querya_desktop/shared/widgets/querya_dropdown_tokens.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart'; -/// One command row in [QueryaActionMenu]. +/// One command row of a menu: [QueryaMenuButton] and [QueryaSplitButton] take a +/// list of these. class QueryaActionMenuItem { const QueryaActionMenuItem({ required this.value, @@ -15,95 +13,3 @@ class QueryaActionMenuItem { final String label; final material.IconData? icon; } - -/// "Button that opens a list of commands" (Export / Copy). -/// -/// Same popup surface as `QueryaDropdown` (border, radius, soft shadow, -/// compact rows, no ripple); trigger is a regular shadcn [OutlineButton]. -class QueryaActionMenu extends material.StatelessWidget { - const QueryaActionMenu({ - super.key, - required this.items, - required this.onSelected, - required this.child, - }); - - final List> items; - final material.ValueChanged onSelected; - - /// Trigger content (label / icon row). - final material.Widget child; - - @override - material.Widget build(material.BuildContext context) { - final cs = Theme.of(context).colorScheme; - final radius = context.scaled(QueryaDropdownTokens.menuBorderRadius); - return material.MenuAnchor( - alignmentOffset: material.Offset( - 0, - context.scaled(QueryaDropdownTokens.menuAlignmentOffset.dy), - ), - consumeOutsideTap: true, - style: material.MenuStyle( - backgroundColor: material.WidgetStatePropertyAll(cs.popover), - surfaceTintColor: material.WidgetStatePropertyAll(cs.popover), - elevation: const material.WidgetStatePropertyAll( - QueryaDropdownTokens.menuElevation, - ), - shadowColor: const material.WidgetStatePropertyAll( - QueryaDropdownTokens.menuShadowColor, - ), - padding: const material.WidgetStatePropertyAll( - QueryaDropdownTokens.menuPadding, - ), - shape: material.WidgetStatePropertyAll( - material.RoundedRectangleBorder( - borderRadius: material.BorderRadius.circular(radius), - side: material.BorderSide(color: cs.border), - ), - ), - ), - menuChildren: [ - for (final item in items) - material.MenuItemButton( - style: material.MenuItemButton.styleFrom( - minimumSize: material.Size( - 0, - QueryaDropdownTokens.scaledMenuItemHeight(context), - ), - padding: material.EdgeInsets.symmetric( - horizontal: context.scaled(8), - ), - foregroundColor: cs.popoverForeground, - overlayColor: cs.accent.withValues(alpha: 0.14), - shape: material.RoundedRectangleBorder( - borderRadius: material.BorderRadius.circular(radius), - ), - ), - leadingIcon: item.icon == null - ? null - : material.Icon( - item.icon, - size: context.scaled(16), - color: cs.mutedForeground, - ), - onPressed: () => onSelected(item.value), - child: material.Text( - item.label, - style: QueryaDropdownTokens.menuItemTextStyle( - context, - cs.popoverForeground, - selected: false, - ), - ), - ), - ], - builder: (context, controller, _) => OutlineButton( - size: ButtonSize.small, - onPressed: () => - controller.isOpen ? controller.close() : controller.open(), - child: child, - ), - ); - } -} diff --git a/lib/shared/widgets/querya_split_button.dart b/lib/shared/widgets/querya_split_button.dart index 54304136..5d2f44b6 100644 --- a/lib/shared/widgets/querya_split_button.dart +++ b/lib/shared/widgets/querya_split_button.dart @@ -34,6 +34,9 @@ class QueryaSplitButton extends material.StatelessWidget { this.tooltip, this.shortcutHint, this.menuTooltip = 'More actions', + this.compact = false, + this.mainKey, + this.menuKey, }); final String label; @@ -57,6 +60,14 @@ class QueryaSplitButton extends material.StatelessWidget { /// Tooltip and spoken label of the chevron half. final String menuTooltip; + /// Icon only on the main half (narrow toolbars); the label becomes its + /// tooltip. + final bool compact; + + /// Keys of the two halves (tests and shortcuts target them separately). + final material.Key? mainKey; + final material.Key? menuKey; + @override material.Widget build(material.BuildContext context) { final wb = context.workbenchOrDefault; @@ -74,7 +85,9 @@ class QueryaSplitButton extends material.StatelessWidget { final enabled = onPressed != null && !loading; final main = QueryaButton( + key: mainKey, variant: halfVariant, + compact: compact, label: label, icon: icon, onPressed: onPressed, @@ -91,6 +104,7 @@ class QueryaSplitButton extends material.StatelessWidget { style: queryaMenuStyle(context), menuChildren: queryaMenuItems(context, items, onSelected), builder: (context, controller, _) => QueryaButton( + key: menuKey, variant: halfVariant, icon: material.Icons.keyboard_arrow_down_rounded, semanticLabel: menuTooltip, diff --git a/test/guards/shared_does_not_import_shadcn_test.dart b/test/guards/shared_does_not_import_shadcn_test.dart index 3ce73b32..9d3a2cf2 100644 --- a/test/guards/shared_does_not_import_shadcn_test.dart +++ b/test/guards/shared_does_not_import_shadcn_test.dart @@ -12,8 +12,6 @@ import 'package:flutter_test/flutter_test.dart'; void main() { const allowed = { 'lib/shared/widgets/app_toast.dart', - 'lib/shared/widgets/export_menu_button.dart', - 'lib/shared/widgets/querya_action_menu.dart', 'lib/shared/widgets/widgets.dart', };