using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using LehrerApp.Core.Models; using LehrerApp.Desktop.ViewModels.Groups; namespace LehrerApp.Desktop.Views.Groups; public partial class AttendanceHomeworkQuickInputDialog : Window { private bool _leftAltDown; private bool _rightAltDown; public AttendanceHomeworkQuickInputDialog() { InitializeComponent(); BuildColumns(); // Im Tunnel lauschen, bevor DataGrid/Button einen Modifier-Event konsumieren können. AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel, handledEventsToo: true); AddHandler(KeyUpEvent, OnPreviewKeyUp, RoutingStrategies.Tunnel, handledEventsToo: true); } protected override void OnOpened(EventArgs e) { base.OnOpened(e); StatusGrid.Focus(); if (ViewModel?.SelectedRow is { } row) StatusGrid.ScrollIntoView(row, null); } private void BuildColumns() { StatusGrid.Columns.Add(new DataGridTextColumn { Header = "Schüler", Binding = new Binding(nameof(ParticipationStudentRow.Name)), Width = new DataGridLength(1, DataGridLengthUnitType.Star), }); StatusGrid.Columns.Add(new DataGridTemplateColumn { Header = "Anwesenheit", Width = new DataGridLength(210, DataGridLengthUnitType.Pixel), CellTemplate = BuildAttendanceTemplate(), }); StatusGrid.Columns.Add(new DataGridTemplateColumn { Header = "Hausaufgaben", Width = new DataGridLength(270, DataGridLengthUnitType.Pixel), CellTemplate = BuildHomeworkTemplate(), }); } private IDataTemplate BuildAttendanceTemplate() => new FuncDataTemplate((row, _) => { if (row is null) return new TextBlock(); var button = StatusButton(185); var flyout = new MenuFlyout { Placement = PlacementMode.BottomEdgeAlignedLeft }; AddAttendanceItem(flyout, row, "[1] ✓ Anwesend", AttendanceStatus.Present); AddAttendanceItem(flyout, row, "[2] ? Entschuldigung offen", AttendanceStatus.ExcusePending); AddAttendanceItem(flyout, row, "[5] ⊘ Krank, entschuldigt", AttendanceStatus.Excused); AddAttendanceItem(flyout, row, "[7] ◇ Andere Schulveranstaltung", AttendanceStatus.OtherSchoolEvent); AddAttendanceItem(flyout, row, "[9] ✕ Geschwänzt", AttendanceStatus.Truant); AddAttendanceItem(flyout, row, "[0] ! Unentschuldigt", AttendanceStatus.Unexcused); flyout.Items.Add(new Separator()); AddAttendanceItem(flyout, row, "V Verspätet", AttendanceStatus.Late); AddAttendanceItem(flyout, row, "V! Erheblich verspätet", AttendanceStatus.SignificantlyLate); AddAttendanceItem(flyout, row, "A Während des Unterrichts abgängig", AttendanceStatus.LeftDuringClass); AddAttendanceItem(flyout, row, "L Lerninsel", AttendanceStatus.LearningIsland); AddAttendanceItem(flyout, row, "S Suspendiert", AttendanceStatus.Suspended); flyout.Items.Add(new Separator()); AddAttendanceItem(flyout, row, "[X] · Nicht kontrolliert", null); button.Flyout = flyout; void Refresh() { UpdateButton(button, string.IsNullOrEmpty(row.AttendanceLabel) ? "·" : row.AttendanceLabel, row.AttendanceTooltip, AttendanceDisplay.Color(row.Attendance)); } Refresh(); row.PropertyChanged += (_, args) => { if (args.PropertyName == nameof(ParticipationStudentRow.AttendanceLabel)) Refresh(); }; return Center(button); }); private IDataTemplate BuildHomeworkTemplate() => new FuncDataTemplate((row, _) => { if (row is null) return new TextBlock(); var button = StatusButton(245); var flyout = new MenuFlyout { Placement = PlacementMode.BottomEdgeAlignedLeft }; AddHomeworkItem(flyout, row, "[⌥1] ✓ Gemacht", HomeworkStatus.Completed); AddHomeworkItem(flyout, row, "[⌥3] ◐ Teilweise, Rest offen", HomeworkStatus.PartiallyCompleted); AddHomeworkItem(flyout, row, "[⌥4] ◕ Rest nachgereicht", HomeworkStatus.PartialSubmittedLate); AddHomeworkItem(flyout, row, "[⌥5] ◒ Rest nicht nachgereicht", HomeworkStatus.PartialMissingOverdue); AddHomeworkItem(flyout, row, "[⌥7] ! Nicht gemacht, Nachreichen offen", HomeworkStatus.MissingOpen); AddHomeworkItem(flyout, row, "[⌥8] ↺ Vollständig nachgereicht", HomeworkStatus.SubmittedLate); AddHomeworkItem(flyout, row, "[⌥0] ✕ Nicht mehr nachgereicht", HomeworkStatus.MissingOverdue); AddHomeworkItem(flyout, row, "[⌥X] · Keine Hausaufgabe aufgegeben", null); button.Flyout = flyout; void Refresh() => UpdateButton(button, row.HomeworkSymbol, row.HomeworkTooltip, HomeworkDisplay.Color(row.Homework)); Refresh(); row.PropertyChanged += (_, args) => { if (args.PropertyName == nameof(ParticipationStudentRow.HomeworkSymbol)) Refresh(); }; return Center(button); }); private void AddAttendanceItem(MenuFlyout flyout, ParticipationStudentRow row, string header, AttendanceStatus? status) { var item = new MenuItem { Header = header }; item.Click += (_, _) => ViewModel?.ApplyAttendance(row, status); flyout.Items.Add(item); } private void AddHomeworkItem(MenuFlyout flyout, ParticipationStudentRow row, string header, HomeworkStatus? status) { var item = new MenuItem { Header = header }; item.Click += (_, _) => ViewModel?.ApplyHomework(row, status); flyout.Items.Add(item); } private static Button StatusButton(double width) => new() { Width = width, Height = 29, Padding = new Thickness(8, 2), FontSize = 12, HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, }; private static Control Center(Control child) => new Grid { Margin = new Thickness(0, 2), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, Children = { child }, }; private static void UpdateButton(Button button, string symbol, string tooltip, string color) { button.Content = $"{symbol} {tooltip}"; ToolTip.SetTip(button, tooltip + " – klicken, um Status auszuwählen"); if (Color.TryParse(color, out var parsed)) { button.Background = new SolidColorBrush(parsed); button.Foreground = Brushes.White; button.Opacity = 1; } else { button.ClearValue(Button.BackgroundProperty); button.ClearValue(Button.ForegroundProperty); button.Opacity = 0.48; } } private void OnPreviewKeyDown(object? sender, KeyEventArgs e) { if (e.PhysicalKey == PhysicalKey.AltLeft) { _leftAltDown = true; return; } if (e.PhysicalKey == PhysicalKey.AltRight) { _rightAltDown = true; return; } var vm = ViewModel; var row = vm?.SelectedRow; if (vm is null || row is null) return; var digit = DigitFromEvent(e); var isX = e.Key == Key.X || e.PhysicalKey == PhysicalKey.X; var altDown = _leftAltDown || _rightAltDown || e.KeyModifiers.HasFlag(KeyModifiers.Alt); if (altDown) { HomeworkStatus? homework = isX ? null : digit switch { 1 => HomeworkStatus.Completed, 3 => HomeworkStatus.PartiallyCompleted, 4 => HomeworkStatus.PartialSubmittedLate, 5 => HomeworkStatus.PartialMissingOverdue, 7 => HomeworkStatus.MissingOpen, 8 => HomeworkStatus.SubmittedLate, 0 => HomeworkStatus.MissingOverdue, _ => null, }; var recognized = isX || digit is 0 or 1 or 3 or 4 or 5 or 7 or 8; if (recognized) { vm.ApplyHomework(row, homework); BringSelectionIntoView(); e.Handled = true; return; } // Eine mit Alt/Option gedrückte Ziffer darf niemals in den // Anwesenheitszweig durchfallen, auch wenn sie nicht belegt ist. if (digit is not null) { e.Handled = true; return; } } AttendanceStatus? attendance = isX ? null : digit switch { 1 => AttendanceStatus.Present, 2 => AttendanceStatus.ExcusePending, 5 => AttendanceStatus.Excused, 7 => AttendanceStatus.OtherSchoolEvent, 9 => AttendanceStatus.Truant, 0 => AttendanceStatus.Unexcused, _ => null, }; var attendanceRecognized = isX || digit is 0 or 1 or 2 or 5 or 7 or 9; if (attendanceRecognized) { vm.ApplyAttendance(row, attendance); BringSelectionIntoView(); e.Handled = true; return; } switch (e.Key) { case Key.Up: vm.MoveSelection(-1); BringSelectionIntoView(); e.Handled = true; break; case Key.Down or Key.Enter: vm.MoveSelection(1); BringSelectionIntoView(); e.Handled = true; break; case Key.Escape: Close(); e.Handled = true; break; default: break; } } private void OnPreviewKeyUp(object? sender, KeyEventArgs e) { if (e.PhysicalKey == PhysicalKey.AltLeft) _leftAltDown = false; if (e.PhysicalKey == PhysicalKey.AltRight) _rightAltDown = false; } private AttendanceHomeworkQuickInputViewModel? ViewModel => DataContext as AttendanceHomeworkQuickInputViewModel; private static int? DigitFromEvent(KeyEventArgs e) => e.PhysicalKey switch { PhysicalKey.Digit0 => 0, PhysicalKey.Digit1 => 1, PhysicalKey.Digit2 => 2, PhysicalKey.Digit3 => 3, PhysicalKey.Digit4 => 4, PhysicalKey.Digit5 => 5, PhysicalKey.Digit6 => 6, PhysicalKey.Digit7 => 7, PhysicalKey.Digit8 => 8, PhysicalKey.Digit9 => 9, _ => DigitFromKey(e.Key), }; private static int? DigitFromKey(Key key) => key switch { Key.D0 or Key.NumPad0 => 0, Key.D1 or Key.NumPad1 => 1, Key.D2 or Key.NumPad2 => 2, Key.D3 or Key.NumPad3 => 3, Key.D4 or Key.NumPad4 => 4, Key.D5 or Key.NumPad5 => 5, Key.D6 or Key.NumPad6 => 6, Key.D7 or Key.NumPad7 => 7, Key.D8 or Key.NumPad8 => 8, Key.D9 or Key.NumPad9 => 9, _ => null, }; private void BringSelectionIntoView() { if (ViewModel?.SelectedRow is { } row) StatusGrid.ScrollIntoView(row, null); } private void OnClose(object? sender, RoutedEventArgs e) => Close(); }