Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions lib/core/sdui/sdui_form_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,41 @@ class SduiFormBuilderState extends material.State<SduiFormBuilder> {
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(
Expand Down
73 changes: 73 additions & 0 deletions lib/features/settings/preferences_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>? 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({
Expand Down
11 changes: 3 additions & 8 deletions lib/features/settings/preferences_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
6 changes: 6 additions & 0 deletions test/core/sdui/sdui_builders_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
}
42 changes: 42 additions & 0 deletions test/features/settings/preferences_checkbox_row_test.dart
Original file line number Diff line number Diff line change
@@ -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<material.Checkbox>(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);
});
}
Loading