diff --git a/lib/core/sdui/sdui_form_builder.dart b/lib/core/sdui/sdui_form_builder.dart index d34f3699..39632c96 100644 --- a/lib/core/sdui/sdui_form_builder.dart +++ b/lib/core/sdui/sdui_form_builder.dart @@ -162,15 +162,41 @@ class SduiFormBuilderState extends material.State { material.Widget _buildField(SduiFormField field) { switch (field.type) { case SduiFieldType.checkbox: - return material.CheckboxListTile( - contentPadding: material.EdgeInsets.zero, - title: Text(field.label), - value: _checkboxValues[field.id] ?? false, - controlAffinity: material.ListTileControlAffinity.leading, - onChanged: (v) { - setState(() => _checkboxValues[field.id] = v ?? false); - _notifyChanged(); - }, + // Avoid CheckboxListTile under opaque dialog DecoratedBox (Flutter 3.44+ + // ListTile ink assert — #492). + final checked = _checkboxValues[field.id] ?? false; + return material.Material( + type: material.MaterialType.transparency, + child: material.MergeSemantics( + child: material.InkWell( + onTap: () { + setState(() => _checkboxValues[field.id] = !checked); + _notifyChanged(); + }, + borderRadius: material.BorderRadius.circular(6), + child: material.Row( + crossAxisAlignment: material.CrossAxisAlignment.center, + children: [ + material.SizedBox( + width: 24, + height: 24, + child: material.Checkbox( + value: checked, + materialTapTargetSize: + material.MaterialTapTargetSize.shrinkWrap, + visualDensity: material.VisualDensity.compact, + onChanged: (v) { + setState(() => _checkboxValues[field.id] = v ?? false); + _notifyChanged(); + }, + ), + ), + const Gap(12), + material.Expanded(child: Text(field.label)), + ], + ), + ), + ), ); case SduiFieldType.select: return material.Column( diff --git a/lib/features/settings/preferences_controls.dart b/lib/features/settings/preferences_controls.dart index 2418fe6e..a897eaa2 100644 --- a/lib/features/settings/preferences_controls.dart +++ b/lib/features/settings/preferences_controls.dart @@ -28,6 +28,79 @@ class PreferencesHint extends StatelessWidget { } } +/// Leading checkbox + title/subtitle for Preferences (no [ListTile]). +/// +/// Avoids Flutter 3.44+ asserts when Preferences chrome uses an opaque +/// [DecoratedBox] above Material ink (#491). +class PreferencesCheckboxRow extends StatelessWidget { + const PreferencesCheckboxRow({ + super.key, + required this.value, + required this.onChanged, + required this.title, + this.subtitle, + }); + + final bool value; + final material.ValueChanged? onChanged; + final material.Widget title; + final material.Widget? subtitle; + + @override + material.Widget build(material.BuildContext context) { + final enabled = onChanged != null; + void toggle() { + if (enabled) onChanged!(!value); + } + + return material.Material( + type: material.MaterialType.transparency, + child: material.MergeSemantics( + child: material.InkWell( + onTap: enabled ? toggle : null, + borderRadius: material.BorderRadius.circular(6), + child: material.Padding( + padding: const material.EdgeInsets.symmetric(vertical: 4), + child: material.Row( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.SizedBox( + width: 24, + height: 24, + child: material.Checkbox( + value: value, + onChanged: enabled + ? (v) { + if (v != null) onChanged!(v); + } + : null, + materialTapTargetSize: + material.MaterialTapTargetSize.shrinkWrap, + visualDensity: material.VisualDensity.compact, + ), + ), + const material.SizedBox(width: 12), + material.Expanded( + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + title, + if (subtitle != null) ...[ + const material.SizedBox(height: 2), + subtitle!, + ], + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} + /// Label + full-width control row for Preferences (uniform dropdown width). class PreferencesFieldRow extends StatelessWidget { const PreferencesFieldRow({ diff --git a/lib/features/settings/preferences_dialog.dart b/lib/features/settings/preferences_dialog.dart index 3a3924ab..7302877d 100644 --- a/lib/features/settings/preferences_dialog.dart +++ b/lib/features/settings/preferences_dialog.dart @@ -152,21 +152,16 @@ class _PreferencesDialogContentState .small() .foreground(), const material.SizedBox(height: 8), - material.CheckboxListTile( - contentPadding: material.EdgeInsets.zero, - controlAffinity: - material.ListTileControlAffinity.leading, + PreferencesCheckboxRow( + value: _checkUpdatesOnStartup, title: const Text( 'Automatically check for updates on startup', ).small(), subtitle: const Text( 'Queries GitHub Releases silently when Querya starts.', ).muted().xSmall(), - value: _checkUpdatesOnStartup, onChanged: (v) { - if (v != null) { - unawaited(_setCheckUpdatesOnStartup(v)); - } + unawaited(_setCheckUpdatesOnStartup(v)); }, ), const material.SizedBox(height: 24), diff --git a/test/core/sdui/sdui_builders_test.dart b/test/core/sdui/sdui_builders_test.dart index 20e00f93..9bf1f381 100644 --- a/test/core/sdui/sdui_builders_test.dart +++ b/test/core/sdui/sdui_builders_test.dart @@ -118,6 +118,12 @@ void main() { expect(values['port'], 5432); expect(values['ssl'], isFalse); expect(key.currentState!.passwordFieldIds, ['password']); + expect(find.byType(material.CheckboxListTile), findsNothing); + expect(find.byType(material.Checkbox), findsOneWidget); + + await tester.tap(find.byType(material.Checkbox)); + await tester.pump(); + expect(key.currentState!.snapshotValues()['ssl'], isTrue); }); testWidgets('file_picker uses injectable picker', (tester) async { diff --git a/test/features/main_screen/workspace_homes_and_preferences_test.dart b/test/features/main_screen/workspace_homes_and_preferences_test.dart index 27f4d17f..dce57565 100644 --- a/test/features/main_screen/workspace_homes_and_preferences_test.dart +++ b/test/features/main_screen/workspace_homes_and_preferences_test.dart @@ -181,6 +181,8 @@ void main() { await tester.pump(const Duration(milliseconds: 400)); expect(find.text('Preferences'), findsOneWidget); + // Dialog chrome uses opaque DecoratedBox — must not host ListTile (#491). + expect(find.byType(material.CheckboxListTile), findsNothing); }); }); } diff --git a/test/features/settings/preferences_checkbox_row_test.dart b/test/features/settings/preferences_checkbox_row_test.dart new file mode 100644 index 00000000..412f90c3 --- /dev/null +++ b/test/features/settings/preferences_checkbox_row_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/features/settings/preferences_controls.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +import '../../support/querya_theme_test_shell.dart'; + +void main() { + testWidgets('PreferencesCheckboxRow toggles without CheckboxListTile', + (tester) async { + var value = false; + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Scaffold( + body: material.StatefulBuilder( + builder: (context, setState) { + return PreferencesCheckboxRow( + value: value, + title: const Text('Toggle me').small(), + subtitle: const Text('Hint').muted().xSmall(), + onChanged: (v) => setState(() => value = v), + ); + }, + ), + ), + ), + ); + + expect(find.byType(material.CheckboxListTile), findsNothing); + expect(find.byType(material.Checkbox), findsOneWidget); + expect(tester.widget(find.byType(material.Checkbox)).value, + isFalse); + + await tester.tap(find.text('Toggle me')); + await tester.pump(); + expect(value, isTrue); + + await tester.tap(find.byType(material.Checkbox)); + await tester.pump(); + expect(value, isFalse); + }); +}