Files
TelemetryMonitor/lib/ui/dialogs/layout_dialog.dart

402 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../../config/app_config.dart';
import '../../layout/chart_config.dart';
import '../../layout/layout_controller.dart';
import '../app_scope.dart';
class LayoutDialog extends StatefulWidget {
const LayoutDialog({super.key});
@override
State<LayoutDialog> createState() => _LayoutDialogState();
}
class _LayoutDialogState extends State<LayoutDialog> {
late LayoutController _draft;
static const _gridOptions = [
(2, 2), (2, 3), (3, 3), (3, 4), (4, 4),
];
@override
void initState() {
super.initState();
final live = context.findAncestorWidgetOfExactType<AppScope>()!.layout;
_draft = live.clone();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Layout configuration'),
content: SizedBox(
width: 620,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_gridSection(),
const SizedBox(height: 16),
_channelsSection(),
const SizedBox(height: 16),
_statusNamesSection(),
],
),
),
),
actions: [
TextButton(
onPressed: () {
setState(() {
_draft.grid.autoAssign(
_draft.charts
.where((c) => c.enabled)
.map((c) => c.channel)
.toList(),
);
});
},
child: const Text('Auto-assign'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => _apply(context),
child: const Text('Apply'),
),
],
);
}
Future<void> _apply(BuildContext context) async {
final scope = AppScope.of(context);
scope.layout.copyFrom(_draft);
await scope.layout.save();
if (context.mounted) Navigator.of(context).pop();
}
Widget _gridSection() {
final enabled =
_draft.charts.where((c) => c.enabled).length;
final empty = _draft.grid.cellCount -
_draft.grid.cellChannels.where((c) => c != null).length;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Grid size',
style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
DropdownButton<(int, int)>(
value: _gridOptions.firstWhere(
(g) => g.$1 == _draft.grid.rows && g.$2 == _draft.grid.cols,
orElse: () => _gridOptions.first,
),
items: _gridOptions
.map((g) => DropdownMenuItem(
value: g,
child: Text('${g.$1} × ${g.$2}'),
))
.toList(),
onChanged: (g) {
if (g == null) return;
setState(() => _draft.grid.resize(g.$1, g.$2));
},
),
const SizedBox(height: 4),
Text(
'${_draft.grid.cellCount} cells · '
'$enabled enabled · $empty empty',
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
],
),
),
const SizedBox(width: 16),
Expanded(child: _gridPreview()),
],
);
}
Widget _gridPreview() {
final scheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _draft.grid.cols,
mainAxisSpacing: 6,
crossAxisSpacing: 6,
childAspectRatio: 16 / 9,
),
itemCount: _draft.grid.cellCount,
itemBuilder: (_, i) {
final cellNumber = i + 1;
final ch = _draft.grid.channelForCell(cellNumber);
if (ch == null) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: scheme.outlineVariant,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(4),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$cellNumber',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: scheme.onSurfaceVariant,
),
),
Text(
'empty',
style: TextStyle(
fontSize: 11,
color: scheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
],
),
),
);
}
final cfg = _draft.configFor(ch);
return Container(
decoration: BoxDecoration(
color: scheme.surface,
border: Border.all(color: scheme.outlineVariant),
borderRadius: BorderRadius.circular(4),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$cellNumber',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 10,
color: scheme.onSurfaceVariant,
),
),
Text(
'CH$ch · ${cfg.name}',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: scheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
);
},
),
);
}
Widget _channelsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 6),
child: Text('Channels',
style: TextStyle(fontWeight: FontWeight.w500)),
),
...List.generate(AppConfig.channelCount, (i) => _channelRow(i + 1)),
],
);
}
Widget _channelRow(int channel) {
final cfg = _draft.configFor(channel);
final assignedCell = () {
for (var i = 0; i < _draft.grid.cellChannels.length; i++) {
if (_draft.grid.cellChannels[i] == channel) return i + 1;
}
return 0; // 0 = unassigned
}();
return Opacity(
opacity: cfg.enabled ? 1.0 : 0.55,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Checkbox(
value: cfg.enabled,
onChanged: (v) => setState(() {
cfg.enabled = v ?? false;
if (!cfg.enabled) {
for (var i = 0; i < _draft.grid.cellChannels.length; i++) {
if (_draft.grid.cellChannels[i] == channel) {
_draft.grid.cellChannels[i] = null;
}
}
}
}),
),
SizedBox(
width: 40,
child: Text('CH$channel',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: Colors.grey)),
),
Expanded(
child: TextFormField(
initialValue: cfg.name,
enabled: cfg.enabled,
decoration: const InputDecoration(
isDense: true, contentPadding: EdgeInsets.all(6)),
onChanged: (v) => cfg.name = v,
),
),
const SizedBox(width: 6),
SizedBox(
width: 64,
child: DropdownButton<int>(
value: assignedCell,
isExpanded: true,
onChanged: cfg.enabled
? (v) => setState(() {
_draft.grid.assign(v ?? 0, v == 0 ? null : channel);
})
: null,
items: [
const DropdownMenuItem(value: 0, child: Text('')),
...List.generate(
_draft.grid.cellCount,
(i) => DropdownMenuItem(
value: i + 1,
child: Text('${i + 1}'),
),
),
],
),
),
const SizedBox(width: 6),
SizedBox(
width: 78,
child: DropdownButton<YMode>(
value:
cfg.yMode == YMode.userZoomed ? YMode.fixed : cfg.yMode,
isExpanded: true,
onChanged: cfg.enabled
? (m) => setState(() => cfg.yMode = m ?? YMode.auto)
: null,
items: const [
DropdownMenuItem(value: YMode.auto, child: Text('auto')),
DropdownMenuItem(value: YMode.fixed, child: Text('fixed')),
],
),
),
const SizedBox(width: 6),
SizedBox(
width: 70,
child: TextFormField(
initialValue: cfg.yMin.toString(),
enabled: cfg.enabled && cfg.yMode != YMode.auto,
style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
textAlign: TextAlign.right,
decoration: const InputDecoration(
isDense: true, contentPadding: EdgeInsets.all(6)),
onChanged: (v) =>
cfg.yMin = double.tryParse(v) ?? cfg.yMin,
),
),
const SizedBox(width: 4),
SizedBox(
width: 70,
child: TextFormField(
initialValue: cfg.yMax.toString(),
enabled: cfg.enabled && cfg.yMode != YMode.auto,
style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
textAlign: TextAlign.right,
decoration: const InputDecoration(
isDense: true, contentPadding: EdgeInsets.all(6)),
onChanged: (v) =>
cfg.yMax = double.tryParse(v) ?? cfg.yMax,
),
),
],
),
),
);
}
Widget _statusNamesSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 4),
child: Text('Status indicator names',
style: TextStyle(fontWeight: FontWeight.w500)),
),
const Padding(
padding: EdgeInsets.only(bottom: 8),
child: Text(
'Names shown on the status bar pills for the 8 status fields.',
style: TextStyle(fontSize: 11, color: Colors.grey),
),
),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 4,
crossAxisSpacing: 10,
childAspectRatio: 6,
),
itemCount: 8,
itemBuilder: (_, i) => Row(children: [
SizedBox(
width: 56,
child: Text('status${i + 1}',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: Colors.grey)),
),
Expanded(
child: TextFormField(
initialValue: _draft.statusNames[i],
decoration: const InputDecoration(
isDense: true, contentPadding: EdgeInsets.all(6)),
onChanged: (v) => _draft.statusNames[i] = v,
),
),
]),
),
],
);
}
}