using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; using Avalonia.VisualTree; using LehrerApp.Desktop.ViewModels.Groups; using System.Collections; using System.Globalization; namespace LehrerApp.Desktop.Views.Groups; public partial class ExamGradingDialog : Window { private static readonly IBrush InvalidBackground = new SolidColorBrush(Color.Parse("#FFCDD2")); public ExamGradingDialog() => InitializeComponent(); protected override void OnOpened(EventArgs e) { base.OnOpened(e); if (DataContext is ExamGradingDialogViewModel vm) BuildColumns(vm); } private void BuildColumns(ExamGradingDialogViewModel vm) { var grid = this.FindControl("ResultGrid"); if (grid is null) return; grid.Columns.Clear(); grid.Columns.Add(new DataGridTextColumn { Header = "Schüler", Binding = new Binding("Name"), IsReadOnly = true, Width = new DataGridLength(170, DataGridLengthUnitType.Pixel), }); for (var i = 0; i < vm.Tasks.Count; i++) { var task = vm.Tasks[i]; grid.Columns.Add(new DataGridTemplateColumn { Header = string.IsNullOrWhiteSpace(task.Title) ? $"Aufg. {task.Nr}" : $"{task.Nr}. {task.Title}", Width = new DataGridLength(1, DataGridLengthUnitType.Star), CellTemplate = BuildPointsCellTemplate(i, grid), }); } grid.Columns.Add(new DataGridTextColumn { Header = "Summe", Binding = new Binding("TotalPointsDisplay"), IsReadOnly = true, Width = new DataGridLength(70, DataGridLengthUnitType.Pixel), }); grid.Columns.Add(new DataGridTextColumn { Header = "Note", Binding = new Binding("GradeDisplay"), IsReadOnly = true, Width = new DataGridLength(70, DataGridLengthUnitType.Pixel), }); grid.Columns.Add(new DataGridTemplateColumn { Header = "Abw.", Width = new DataGridLength(50, DataGridLengthUnitType.Pixel), CellTemplate = BuildAbsentCellTemplate(), }); grid.Columns.Add(new DataGridTemplateColumn { Header = "Kommentar", Width = new DataGridLength(180, DataGridLengthUnitType.Pixel), CellTemplate = BuildCommentCellTemplate(grid), }); } private IDataTemplate BuildPointsCellTemplate(int taskIndex, DataGrid grid) { var cellName = $"Pts_{taskIndex}"; return new FuncDataTemplate((row, _) => { if (row is null) return new TextBlock(); var cell = row.Cells.ElementAtOrDefault(taskIndex); if (cell is null) return new TextBlock(); var tb = new TextBox { Name = cellName, Text = FormatPoints(cell.Value), BorderThickness = new Thickness(0), Background = Brushes.Transparent, Padding = new Thickness(6, 4), HorizontalContentAlignment = HorizontalAlignment.Center, }; ToolTip.SetTip(tb, $"max. {FormatPoints(cell.MaxPoints)} Punkte"); UpdateInvalidVisual(tb, cell.IsInvalid); cell.PropertyChanged += (_, pe) => { if (pe.PropertyName == nameof(PointsCell.IsInvalid)) UpdateInvalidVisual(tb, cell.IsInvalid); }; tb.LostFocus += (_, _) => CommitPointsCell(tb, cell); tb.KeyDown += (_, e) => HandleCellKeyDown(e, grid, row, cellName); return tb; }); } private static void CommitPointsCell(TextBox tb, PointsCell cell) { var text = tb.Text?.Trim().Replace(',', '.'); double? parsed = string.IsNullOrEmpty(text) ? null : double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) ? v : cell.Value; cell.TrySetValue(parsed); tb.Text = FormatPoints(cell.Value); } private static void UpdateInvalidVisual(TextBox tb, bool invalid) => tb.Background = invalid ? InvalidBackground : Brushes.Transparent; private static string FormatPoints(double? value) => value?.ToString("0.##", CultureInfo.InvariantCulture) ?? ""; private IDataTemplate BuildAbsentCellTemplate() { return new FuncDataTemplate((row, _) => { if (row is null) return new TextBlock(); var cb = new CheckBox { IsChecked = row.Absent, HorizontalAlignment = HorizontalAlignment.Center }; cb.IsCheckedChanged += (_, _) => row.Absent = cb.IsChecked == true; return cb; }); } private IDataTemplate BuildCommentCellTemplate(DataGrid grid) { const string cellName = "Comment"; return new FuncDataTemplate((row, _) => { if (row is null) return new TextBlock(); var tb = new TextBox { Name = cellName, Text = row.Comment, BorderThickness = new Thickness(0), Background = Brushes.Transparent, Padding = new Thickness(6, 4), }; tb.LostFocus += (_, _) => row.Comment = tb.Text ?? ""; tb.KeyDown += (_, e) => HandleCellKeyDown(e, grid, row, cellName); return tb; }); } /// Enter/Pfeil-Hoch/Pfeil-Runter springen zur gleichen Spalte in der Nachbarzeile /// (Tab funktioniert bereits über die normale Fokus-Reihenfolge). private void HandleCellKeyDown(KeyEventArgs e, DataGrid grid, object rowItem, string cellName) { if (e.Key is not (Key.Down or Key.Up or Key.Enter)) return; e.Handled = true; if (grid.ItemsSource is not IList items) return; var idx = items.IndexOf(rowItem); if (idx < 0) return; var targetIdx = e.Key == Key.Up ? idx - 1 : idx + 1; if (targetIdx < 0 || targetIdx >= items.Count) return; var targetItem = items[targetIdx]!; grid.ScrollIntoView(targetItem, grid.CurrentColumn); Dispatcher.UIThread.Post(() => { var targetTb = grid.GetVisualDescendants().OfType() .FirstOrDefault(t => t.DataContext == targetItem && t.Name == cellName); if (targetTb is null) return; targetTb.Focus(); targetTb.SelectAll(); }, DispatcherPriority.Loaded); } private void OnClose(object? sender, RoutedEventArgs e) => Close(); }