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 createState() => _LayoutDialogState(); } class _LayoutDialogState extends State { 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()!.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 _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( 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( 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, ), ), ]), ), ], ); } }