using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; using Avalonia.VisualTree; using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.Views.Controls; using ClaudeDo.Ui.Views.MissionControl; using ClaudeDo.Ui.Views.Modals; namespace ClaudeDo.Ui.Views.Islands; public partial class TasksIslandView : UserControl { private readonly TaskDragController _drag = new(); // Custom-drag gesture state. The drag is ARMED on press and BEGINS once the pointer moves // past the threshold, so a plain click still selects the row. private const double DragThreshold = 4; private Point _pressPoint; private TaskRowViewModel? _pressRow; private Control? _pressControl; private bool _dragArmed; private bool _dragging; // The list row (in the Lists island) currently highlighted as a drop target while dragging. private ListNavItemViewModel? _hintedList; public TasksIslandView() { InitializeComponent(); AddHandler(PointerPressedEvent, OnTunnelPointerPressed, RoutingStrategies.Tunnel); AddHandler(PointerMovedEvent, OnPointerMovedDrag, RoutingStrategies.Tunnel); AddHandler(PointerReleasedEvent, OnPointerReleasedDrag, RoutingStrategies.Tunnel); AddHandler(PointerCaptureLostEvent, OnPointerCaptureLost); DataContextChanged += (_, _) => { if (DataContext is TasksIslandViewModel vm) { vm.FocusAddTaskRequested += (_, _) => AddTaskBox.Focus(); vm.ShowUnfinishedPlanningModal = async (modalVm) => { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) { modalVm.CancelCommand.Execute(null); return; } var modal = new UnfinishedPlanningModalView { DataContext = modalVm }; // Closing via the OS title-bar (if ever enabled) also resolves the TCS. modal.Closed += (_, _) => modalVm.CancelCommand.Execute(null); await modal.ShowDialog(owner); // ShowDialog completes once the window is closed (CloseAction or OS close). }; vm.ConfirmAsync = ShowConfirmAsync; vm.SelectionChanged += (_, _) => ScrollSelectedIntoView(); } }; } // Bring the selected row into view — a programmatic select (e.g. Mission Control's "Open in // app") can target a row that isn't currently realized. RowsListBox.ScrollIntoView is // virtualization-aware (it realizes the container as needed); a visual-tree search for an // existing Button, as this used to do, only finds rows already on screen. private void ScrollSelectedIntoView() { if (DataContext is not TasksIslandViewModel vm || vm.SelectedTask is not { } target) return; Dispatcher.UIThread.Post(() => RowsListBox.ScrollIntoView(target), DispatcherPriority.Background); } private async System.Threading.Tasks.Task ShowConfirmAsync(string message) { var owner = TopLevel.GetTopLevel(this) as Window; if (owner is null) return false; var tcs = new TaskCompletionSource(); var cancel = new Button { Content = "Cancel", MinWidth = 90 }; var confirm = new Button { Content = "Confirm", MinWidth = 90, Classes = { "danger" } }; var dialog = new Window { Title = "Confirm", Width = 380, SizeToContent = SizeToContent.Height, CanResize = false, WindowStartupLocation = WindowStartupLocation.CenterOwner, ShowInTaskbar = false, Background = this.FindResource("SurfaceBrush") as IBrush, Content = new StackPanel { Margin = new Thickness(20), Spacing = 16, Children = { new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap }, new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Spacing = 8, Children = { cancel, confirm }, }, }, }, }; cancel.Click += (_, _) => { tcs.TrySetResult(false); dialog.Close(); }; confirm.Click += (_, _) => { tcs.TrySetResult(true); dialog.Close(); }; dialog.Closed += (_, _) => tcs.TrySetResult(false); _ = dialog.ShowDialog(owner); return await tcs.Task; } // ── Custom ghost drag ──────────────────────────────────────────────────── // Replaces both the OLE DoDragDropAsync reorder and the OLE drop-to-queue path: a hand-built // drag (pointer capture + a transparent topmost ghost window) is the only way to get a // translucent follower that crosses from this window into the separate Mission Control window. private void OnTunnelPointerPressed(object? sender, PointerPressedEventArgs e) { ResetPressState(); if (DataContext is not TasksIslandViewModel) return; if (e.Source is not Visual src) return; var button = src as Button ?? src.FindAncestorOfType