feat: add seating plan drag and quick assessment
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
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;
|
||||
@@ -7,6 +10,15 @@ 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)
|
||||
@@ -16,9 +28,128 @@ public partial class SeatingPlanTabView : UserControl
|
||||
{
|
||||
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<SeatingPlan?> ShowPlanDialog(SeatingPlan? plan)
|
||||
{
|
||||
if (DataContext is not SeatingPlanTabViewModel vm) return null;
|
||||
|
||||
Reference in New Issue
Block a user