feat: add seating plan drag and quick assessment
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.SeatAssessmentDialog"
|
||||
x:DataType="vm:SeatAssessmentViewModel"
|
||||
Title="Sitzplatz-Schnelleingabe" Width="660" Height="700"
|
||||
MinWidth="560" MinHeight="560" CanResize="True"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Window.Styles>
|
||||
<Style Selector="Button.choice">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="8,5"/>
|
||||
</Style>
|
||||
<Style Selector="Button.choice.selected">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight2}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||
<Setter Property="BorderThickness" Value="2"/>
|
||||
</Style>
|
||||
<Style Selector="Border.aspectrow">
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
<Style Selector="Border.aspectrow.active">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight3}"/>
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="20">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding StudentName}" FontSize="22" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding SessionDisplay}" FontSize="12" Opacity="0.55"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Schließen" Click="OnClose" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<StackPanel Spacing="16" IsEnabled="{Binding CanEdit}">
|
||||
<Border Background="#FFF3CD" CornerRadius="6" Padding="10"
|
||||
IsVisible="{Binding CanEdit, Converter={x:Static BoolConverters.Not}}">
|
||||
<TextBlock Text="{Binding ReadOnlyHint}" Foreground="#92400E" TextWrapping="Wrap"/>
|
||||
</Border>
|
||||
|
||||
<StackPanel Spacing="7">
|
||||
<TextBlock Text="MITARBEIT" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||
<ItemsControl ItemsSource="{Binding AspectRows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SeatAssessmentAspectRow">
|
||||
<Border Classes="aspectrow" Classes.active="{Binding IsActive}"
|
||||
CornerRadius="7" Padding="9" Margin="0,3">
|
||||
<Grid ColumnDefinitions="150,*">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold">
|
||||
<Run Text="["/><Run Text="{Binding Shortcut}"/><Run Text="] "/><Run Text="{Binding Label}"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding DisplayValue}" FontSize="11" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
<WrapPanel Grid.Column="1" HorizontalAlignment="Right">
|
||||
<ItemsControl ItemsSource="{Binding Choices}">
|
||||
<ItemsControl.ItemsPanel><ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SeatRatingChoice">
|
||||
<Button Classes="choice" Classes.selected="{Binding IsSelected}"
|
||||
Command="{Binding ApplyCommand}" Margin="2">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Label}" HorizontalAlignment="Center" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Shortcut}" HorizontalAlignment="Center" FontSize="9" Opacity="0.5"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Classes="choice" Content="−" Command="{Binding DecrementCommand}" Margin="2"/>
|
||||
<Button Classes="choice" Content="+" Command="{Binding IncrementCommand}" Margin="2"/>
|
||||
<Button Classes="choice" Content="Löschen" Command="{Binding ClearCommand}" Margin="2"/>
|
||||
</WrapPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="7">
|
||||
<TextBlock Text="ANWESENHEIT" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||
<TextBlock Text="{Binding AttendanceLabel}" FontSize="12"/>
|
||||
<ItemsControl ItemsSource="{Binding AttendanceChoices}">
|
||||
<ItemsControl.ItemsPanel><ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SeatAttendanceChoice">
|
||||
<Button Classes="choice" Classes.selected="{Binding IsSelected}"
|
||||
Command="{Binding ApplyCommand}" Margin="2">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<TextBlock Text="{Binding Symbol}" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding Label}"/>
|
||||
<TextBlock Text="{Binding Shortcut}" FontSize="9" Opacity="0.5" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="7">
|
||||
<TextBlock Text="HAUSAUFGABEN" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||
<TextBlock Text="{Binding HomeworkLabel}" FontSize="12"/>
|
||||
<ItemsControl ItemsSource="{Binding HomeworkChoices}">
|
||||
<ItemsControl.ItemsPanel><ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SeatHomeworkChoice">
|
||||
<Button Classes="choice" Classes.selected="{Binding IsSelected}"
|
||||
Command="{Binding ApplyCommand}" Margin="2">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<TextBlock Text="{Binding Symbol}" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding Label}"/>
|
||||
<TextBlock Text="{Binding Shortcut}" FontSize="9" Opacity="0.5" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock Grid.Row="2" Margin="0,12,0,0" FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
||||
Text="Mitarbeit: Q/W/E/R/T Aspekt · 1–5 Bewertung · +/− anpassen · Backspace löschen · ←/→ Aspekt | Anwesenheit: Strg+1/2/5/7/9/0, Strg+X | Hausaufgaben: ⌥+1/3/4/5/7/8/0, ⌥+X | Esc schließen"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,80 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class SeatAssessmentDialog : Window
|
||||
{
|
||||
public SeatAssessmentDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||
}
|
||||
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
Focus();
|
||||
}
|
||||
|
||||
private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (DataContext is not SeatAssessmentViewModel vm) return;
|
||||
var digit = DigitFromEvent(e);
|
||||
var clear = e.Key == Key.X || e.PhysicalKey == PhysicalKey.X;
|
||||
if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||
{
|
||||
if (clear || digit is not null) vm.ApplyAttendanceShortcut(digit, clear);
|
||||
e.Handled = clear || digit is not null;
|
||||
return;
|
||||
}
|
||||
if (e.KeyModifiers.HasFlag(KeyModifiers.Alt))
|
||||
{
|
||||
if (clear || digit is not null) vm.ApplyHomeworkShortcut(digit, clear);
|
||||
e.Handled = clear || digit is not null;
|
||||
return;
|
||||
}
|
||||
if (digit is not null)
|
||||
{
|
||||
vm.SetRatingByNumber(digit.Value);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Q: vm.SelectAspect(0); e.Handled = true; break;
|
||||
case Key.W: vm.SelectAspect(1); e.Handled = true; break;
|
||||
case Key.E: vm.SelectAspect(2); e.Handled = true; break;
|
||||
case Key.R: vm.SelectAspect(3); e.Handled = true; break;
|
||||
case Key.T: vm.SelectAspect(4); e.Handled = true; break;
|
||||
case Key.Left: vm.MoveAspect(-1); e.Handled = true; break;
|
||||
case Key.Right: vm.MoveAspect(1); e.Handled = true; break;
|
||||
case Key.OemPlus or Key.Add: vm.AdjustCurrentRating(1); e.Handled = true; break;
|
||||
case Key.OemMinus or Key.Subtract: vm.AdjustCurrentRating(-1); e.Handled = true; break;
|
||||
case Key.Back: vm.ClearCurrentRating(); e.Handled = true; break;
|
||||
case Key.Escape: Close(); e.Handled = true; break;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
_ => e.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 OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -3,6 +3,21 @@
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.SeatingPlanTabView"
|
||||
x:DataType="vm:SeatingPlanTabViewModel">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Border.seat">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAltHighBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
</Style>
|
||||
<Style Selector="Border.seat.occupied">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||
</Style>
|
||||
<Style Selector="Border.seat.droptarget">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight2}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||
<Setter Property="BorderThickness" Value="2"/>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<Grid ColumnDefinitions="260,*">
|
||||
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,1,0" Padding="16">
|
||||
@@ -51,8 +66,11 @@
|
||||
<TextBlock Text="{Binding PlanTitle}" FontSize="22" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding PlanSubtitle}" Opacity="0.65"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1" Text="{Binding AssignmentSummary}" VerticalAlignment="Bottom"
|
||||
FontSize="12" Opacity="0.6"/>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Bottom">
|
||||
<TextBlock Text="{Binding AssignmentSummary}" HorizontalAlignment="Right" FontSize="12" Opacity="0.6"/>
|
||||
<TextBlock Text="Ziehen: Platz ändern · Klicken: bewerten" HorizontalAlignment="Right"
|
||||
FontSize="11" Opacity="0.5"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto"
|
||||
@@ -70,24 +88,60 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SeatCellViewModel">
|
||||
<Border Width="190" MinHeight="76" Margin="5" Padding="10"
|
||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="7">
|
||||
<Border Classes="seat" Classes.occupied="{Binding IsOccupied}"
|
||||
Classes.droptarget="{Binding IsDropTarget}"
|
||||
Width="190" MinHeight="76" Margin="5" Padding="10"
|
||||
CornerRadius="7"
|
||||
DragDrop.AllowDrop="True"
|
||||
DragDrop.DragEnter="OnSeatDragEnter"
|
||||
DragDrop.DragLeave="OnSeatDragLeave"
|
||||
DragDrop.DragOver="OnSeatDragOver"
|
||||
DragDrop.Drop="OnSeatDrop"
|
||||
PointerPressed="OnDragSourcePressed"
|
||||
PointerMoved="OnDragSourceMoved"
|
||||
PointerReleased="OnDragSourceReleased"
|
||||
Tapped="OnSeatTapped">
|
||||
<StackPanel Spacing="5">
|
||||
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
|
||||
<ComboBox ItemsSource="{Binding Options}" SelectedItem="{Binding SelectedOption}"
|
||||
IsEnabled="{Binding CanEdit}" HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:StudentSeatOption">
|
||||
<TextBlock Text="{Binding DisplayName}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
|
||||
<TextBlock Grid.Column="1" Text="⠿" Opacity="0.45" IsVisible="{Binding IsOccupied}"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border DragDrop.AllowDrop="True" DragDrop.DragOver="OnUnassignedDragOver"
|
||||
DragDrop.Drop="OnUnassignedDrop"
|
||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="7" Padding="12" Margin="5,8">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="NICHT ZUGEORDNET" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
|
||||
<TextBlock Text="Schüler hierher ziehen, um einen Platz zu leeren."
|
||||
FontSize="11" Opacity="0.5"/>
|
||||
<ItemsControl ItemsSource="{Binding UnassignedStudents}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:StudentSeatOption">
|
||||
<Border Background="{DynamicResource SystemAccentColorLight2}" CornerRadius="5"
|
||||
Padding="9,5" Margin="0,0,6,6"
|
||||
PointerPressed="OnDragSourcePressed"
|
||||
PointerMoved="OnDragSourceMoved"
|
||||
PointerReleased="OnDragSourceReleased">
|
||||
<TextBlock Text="{Binding DisplayName}" FontSize="12"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Alle Schüler sind zugeordnet." Classes="emptyhint"
|
||||
IsVisible="{Binding !UnassignedStudents.Count}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
@@ -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