297 lines
11 KiB
C#
297 lines
11 KiB
C#
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<ParticipationStudentRow>((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<ParticipationStudentRow>((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();
|
||
}
|