feat(ui): drag-reorder Mission Control panes by their header

This commit is contained in:
Mika Kuns
2026-06-25 16:36:09 +02:00
parent f63be285a2
commit f6ecfc995f
3 changed files with 60 additions and 2 deletions

View File

@@ -1,8 +1,46 @@
using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.MissionControl; namespace ClaudeDo.Ui.Views.MissionControl;
public partial class MissionControlView : UserControl public partial class MissionControlView : UserControl
{ {
public MissionControlView() => InitializeComponent(); // Shared with MonitorPaneView (the drag source).
public static readonly DataFormat<string> PaneFormat =
DataFormat.CreateStringApplicationFormat("claudedo-monitor-pane");
public MissionControlView()
{
InitializeComponent();
AddHandler(DragDrop.DragOverEvent, OnPaneDragOver);
AddHandler(DragDrop.DropEvent, OnPaneDrop);
}
private void OnPaneDragOver(object? sender, DragEventArgs e)
{
e.DragEffects = (e.DataTransfer?.Contains(PaneFormat) ?? false)
? DragDropEffects.Move
: DragDropEffects.None;
}
private void OnPaneDrop(object? sender, DragEventArgs e)
{
if (DataContext is not MissionControlViewModel vm) return;
var draggedId = e.DataTransfer?.TryGetValue(PaneFormat);
if (string.IsNullOrEmpty(draggedId)) return;
if (e.Source is not Avalonia.Visual src) return;
var targetPane = src.FindAncestorOfType<MonitorPaneView>();
if (targetPane?.DataContext is not TaskMonitorViewModel target) return;
var dragged = vm.Monitors.FirstOrDefault(m => m.SubscribedTaskId == draggedId);
if (dragged is null) return;
vm.MoveMonitor(dragged, target);
}
} }

View File

@@ -6,6 +6,7 @@
x:DataType="vm:TaskMonitorViewModel" x:DataType="vm:TaskMonitorViewModel"
x:Class="ClaudeDo.Ui.Views.MissionControl.MonitorPaneView"> x:Class="ClaudeDo.Ui.Views.MissionControl.MonitorPaneView">
<Border Classes="monitor-pane" <Border Classes="monitor-pane"
DragDrop.AllowDrop="True"
Classes.mon-review="{Binding IsWaitingForReview}" Classes.mon-review="{Binding IsWaitingForReview}"
Classes.mon-done="{Binding IsDone}" Classes.mon-done="{Binding IsDone}"
Classes.mon-roadblock="{Binding HasRoadblock}" Classes.mon-roadblock="{Binding HasRoadblock}"
@@ -17,7 +18,8 @@
<Border DockPanel.Dock="Top" <Border DockPanel.Dock="Top"
Background="{DynamicResource Surface2Brush}" Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource LineBrush}" BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1" Padding="6,3"> BorderThickness="0,0,0,1" Padding="6,3"
PointerPressed="OnHeaderPressed">
<StackPanel Orientation="Horizontal" Spacing="2" <StackPanel Orientation="Horizontal" Spacing="2"
HorizontalAlignment="Right" VerticalAlignment="Center"> HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="title-ctrl" <Button Classes="title-ctrl"

View File

@@ -1,8 +1,26 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.MissionControl; namespace ClaudeDo.Ui.Views.MissionControl;
public partial class MonitorPaneView : UserControl public partial class MonitorPaneView : UserControl
{ {
public MonitorPaneView() => InitializeComponent(); public MonitorPaneView() => InitializeComponent();
private async void OnHeaderPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is not TaskMonitorViewModel m || m.SubscribedTaskId is not { } id) return;
if (e.Source is not Avalonia.Visual src) return;
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
// Don't start a drag when the press landed on a header action button.
var button = src as Button ?? src.FindAncestorOfType<Button>();
if (button is not null) return;
var data = new DataTransfer();
data.Add(DataTransferItem.Create(MissionControlView.PaneFormat, id));
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move);
}
} }