using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Threading; using LehrerApp.Core.Models; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.Views.Shared; namespace LehrerApp.Desktop.Views.Groups; public partial class SeatingPlanTabView : UserControl { private object? _dragCandidate; private PointerPressedEventArgs? _dragTrigger; private Avalonia.Point _pressPosition; private SeatCellViewModel? _draggedSeat; private StudentSeatOption? _draggedStudent; private SeatCellViewModel? _pendingDropTarget; private bool _pendingClearSeat; private DateTime _ignoreTapUntil; public SeatingPlanTabView() => InitializeComponent(); protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (DataContext is SeatingPlanTabViewModel vm) { vm.OnEditPlan = ShowPlanDialog; vm.OnConfirmDelete = ShowDeleteConfirmDialog; vm.OnAssessStudent = ShowAssessmentDialog; } } private void OnDragSourcePressed(object? sender, PointerPressedEventArgs e) { if (sender is not Control control || !e.GetCurrentPoint(control).Properties.IsLeftButtonPressed) return; var candidate = control.DataContext; if (candidate is SeatCellViewModel { IsOccupied: false } or null) return; if (DataContext is not SeatingPlanTabViewModel { IsEditable: true }) return; _dragCandidate = candidate; _dragTrigger = e; _pressPosition = e.GetPosition(this); } private async void OnDragSourceMoved(object? sender, PointerEventArgs e) { if (_dragCandidate is null || _dragTrigger is null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; var current = e.GetPosition(this); if (Math.Abs(current.X - _pressPosition.X) < 6 && Math.Abs(current.Y - _pressPosition.Y) < 6) return; _draggedSeat = _dragCandidate as SeatCellViewModel; _draggedStudent = _dragCandidate as StudentSeatOption; _pendingDropTarget = null; _pendingClearSeat = false; _dragCandidate = null; var trigger = _dragTrigger; _dragTrigger = null; var data = new DataTransfer(); data.Add(DataTransferItem.CreateText("LehrerApp-Sitzplatz")); var effect = await DragDrop.DoDragDropAsync(trigger, data, DragDropEffects.Move); var draggedSeat = _draggedSeat; var draggedStudent = _draggedStudent; var dropTarget = _pendingDropTarget; var clearSeat = _pendingClearSeat; _draggedSeat = null; _draggedStudent = null; _pendingDropTarget = null; _pendingClearSeat = false; _ignoreTapUntil = DateTime.UtcNow.AddMilliseconds(250); // Never mutate an ItemsControl while Avalonia is still processing its native // drop event. Assigning an unassigned student removes the dragged source item. if ((effect & DragDropEffects.Move) == 0 || DataContext is not SeatingPlanTabViewModel vm) return; Dispatcher.UIThread.Post(() => { if (clearSeat && draggedSeat is not null) vm.ClearSeat(draggedSeat); else if (dropTarget is not null && draggedSeat is not null) vm.MoveSeat(draggedSeat, dropTarget); else if (dropTarget is not null && draggedStudent is not null) vm.AssignStudent(draggedStudent, dropTarget); }, DispatcherPriority.Background); } private void OnDragSourceReleased(object? sender, PointerReleasedEventArgs e) { _dragCandidate = null; _dragTrigger = null; } private void OnSeatDragEnter(object? sender, DragEventArgs e) { if (sender is Border { DataContext: SeatCellViewModel target } && CanDropOn(target)) target.IsDropTarget = true; } private void OnSeatDragLeave(object? sender, DragEventArgs e) { if (sender is Border { DataContext: SeatCellViewModel target }) target.IsDropTarget = false; } private void OnSeatDragOver(object? sender, DragEventArgs e) { e.DragEffects = sender is Border { DataContext: SeatCellViewModel target } && CanDropOn(target) ? DragDropEffects.Move : DragDropEffects.None; } private void OnSeatDrop(object? sender, DragEventArgs e) { if (sender is not Border { DataContext: SeatCellViewModel target }) return; target.IsDropTarget = false; if (!CanDropOn(target)) { e.DragEffects = DragDropEffects.None; return; } _pendingDropTarget = target; _pendingClearSeat = false; e.DragEffects = DragDropEffects.Move; } private bool CanDropOn(SeatCellViewModel target) => DataContext is SeatingPlanTabViewModel { IsEditable: true } && ((_draggedSeat is not null && _draggedSeat != target) || _draggedStudent?.StudentId is not null); private void OnUnassignedDragOver(object? sender, DragEventArgs e) => e.DragEffects = _draggedSeat is not null ? DragDropEffects.Move : DragDropEffects.None; private void OnUnassignedDrop(object? sender, DragEventArgs e) { if (_draggedSeat is not null) { _pendingDropTarget = null; _pendingClearSeat = true; e.DragEffects = DragDropEffects.Move; } } private async void OnSeatTapped(object? sender, TappedEventArgs e) { if (DateTime.UtcNow < _ignoreTapUntil || sender is not Border { DataContext: SeatCellViewModel seat } || !seat.IsOccupied || DataContext is not SeatingPlanTabViewModel vm) return; await vm.AssessStudent(seat); } private async Task ShowAssessmentDialog(SeatAssessmentViewModel vm) { var dialog = new SeatAssessmentDialog { DataContext = vm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is not null) await dialog.ShowDialog(owner); } private async Task ShowPlanDialog(SeatingPlan? plan) { if (DataContext is not SeatingPlanTabViewModel vm) return null; var dialogVm = vm.CreateDialogViewModel(plan); var dialog = new SeatingPlanDialog { DataContext = dialogVm }; var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null || !await dialog.ShowDialog(owner)) return null; return dialogVm.Result; } private async Task ShowDeleteConfirmDialog(SeatingPlanSummary plan) { var dialog = new ConfirmDialog { DataContext = new ConfirmDialogInfo { Title = "Sitzplan löschen?", Message = $"Der Sitzplan „{plan.Name}“ und seine Zuordnungen werden gelöscht.", ConfirmText = "Löschen", }, }; var owner = TopLevel.GetTopLevel(this) as Window; return owner is not null && await dialog.ShowDialog(owner); } }