The diff viewer used ShowDialog at both call sites (task details, worktrees overview), blocking navigation of the rest of the app while open. Switch both to Window.Show(owner) and reuse the one open window (swap DataContext, Activate()) instead of stacking a second one on re-open. Add an opt-in maximize/restore button + double-click-titlebar toggle to ModalShell (ShowMaximizeButton, default false so other modals are untouched), wired on for DiffViewerView only. Reuses the existing Icon.WinMax/WinRestore geometry and OffScreenMargin inset from MainWindow's own maximize handling. The review gate (Approve & Merge locked until the diff is opened) already fires DiffViewed before the dialog/window is shown, so it is unaffected by the modal-to-non-modal change.
230 lines
8.4 KiB
C#
230 lines
8.4 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using ClaudeDo.Ui.Services;
|
|
using ClaudeDo.Ui.ViewModels;
|
|
using ClaudeDo.Ui.ViewModels.Conflicts;
|
|
using ClaudeDo.Ui.ViewModels.Modals;
|
|
using ClaudeDo.Ui.Views.Conflicts;
|
|
using ClaudeDo.Ui.Views.MissionControl;
|
|
using ClaudeDo.Ui.Views.Modals;
|
|
|
|
namespace ClaudeDo.Ui.Views;
|
|
|
|
/// <summary>
|
|
/// Window-backed <see cref="IDialogService"/>. Owns every modal-view construction, the
|
|
/// shared Confirm/Error dialogs, and the worktrees-overview sub-callbacks that reach back
|
|
/// into the shell (jump-to-task, merge, conflict resolution). Created by <see cref="MainWindow"/>
|
|
/// with itself as the owner window; the shell resolves lazily from the owner's DataContext so
|
|
/// the service and shell don't form a construction cycle.
|
|
/// </summary>
|
|
public sealed class WindowDialogService : IDialogService
|
|
{
|
|
private readonly Window _owner;
|
|
private MissionControlWindow? _missionControl;
|
|
private DiffViewerView? _diffViewerWindow;
|
|
|
|
public WindowDialogService(Window owner) => _owner = owner;
|
|
|
|
private IslandsShellViewModel? Shell => _owner.DataContext as IslandsShellViewModel;
|
|
|
|
// Own modals to the window the user is actually looking at (e.g. the Mission Control
|
|
// window when Settings is opened from there), falling back to the main window.
|
|
private Window ActiveOwner()
|
|
{
|
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
{
|
|
var active = desktop.Windows.FirstOrDefault(w => w.IsActive);
|
|
if (active is not null) return active;
|
|
}
|
|
return _owner;
|
|
}
|
|
|
|
public async Task ShowAboutAsync(AboutModalViewModel vm)
|
|
{
|
|
var dlg = new AboutModalView { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
await dlg.ShowDialog(_owner);
|
|
}
|
|
|
|
public async Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm)
|
|
{
|
|
var dlg = new WeeklyReportModalView { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
await dlg.ShowDialog(_owner);
|
|
}
|
|
|
|
public async Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm)
|
|
{
|
|
var dlg = new UsageMonitorModalView { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
// The pill sits in both the footer and the Mission Control header, so own the dialog to
|
|
// whichever window is active — otherwise it opens behind Mission Control.
|
|
await dlg.ShowDialog(ActiveOwner());
|
|
}
|
|
|
|
public async Task ShowSettingsAsync(SettingsModalViewModel vm)
|
|
{
|
|
var dlg = new SettingsModalView { DataContext = vm };
|
|
await dlg.ShowDialog(ActiveOwner());
|
|
}
|
|
|
|
public async Task ShowListSettingsAsync(ListSettingsModalViewModel vm)
|
|
{
|
|
var dlg = new ListSettingsModalView { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
vm.ConfirmAsync = ConfirmAsync;
|
|
vm.ShowErrorAsync = ShowErrorAsync;
|
|
await dlg.ShowDialog(_owner);
|
|
}
|
|
|
|
public async Task ShowRepoImportAsync(RepoImportModalViewModel vm)
|
|
{
|
|
var dlg = new RepoImportModalView { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
await dlg.ShowDialog(_owner);
|
|
}
|
|
|
|
public async Task<System.Collections.Generic.IReadOnlyList<string>?> ShowMergeHelperSelectionAsync(MergeHelperSelectionModalViewModel vm)
|
|
{
|
|
var dlg = new MergeHelperSelectionModal { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
dlg.Closed += (_, _) => vm.Result.TrySetResult(null); // native close counts as cancel
|
|
await dlg.ShowDialog(_owner);
|
|
return await vm.Result.Task;
|
|
}
|
|
|
|
public async Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm)
|
|
{
|
|
var dlg = new WorktreesOverviewModalView { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
vm.JumpToTaskAction = (listId, taskId) =>
|
|
{
|
|
if (Shell is { } s) _ = JumpToTaskHelper.SelectAsync(s, listId, taskId);
|
|
};
|
|
vm.ShowDiffAction = diffVm =>
|
|
{
|
|
_ = diffVm.LoadAsync();
|
|
// Non-modal: must not block the (still-open) worktrees overview or the main
|
|
// window behind it. Reuse the one open window rather than stacking a second.
|
|
if (_diffViewerWindow is { } existing)
|
|
{
|
|
existing.DataContext = diffVm;
|
|
diffVm.CloseAction = () => existing.Close();
|
|
if (existing.WindowState == WindowState.Minimized)
|
|
existing.WindowState = WindowState.Normal;
|
|
existing.Activate();
|
|
return;
|
|
}
|
|
var diffDlg = new DiffViewerView { DataContext = diffVm };
|
|
diffVm.CloseAction = () => diffDlg.Close();
|
|
diffDlg.Closed += (_, _) => _diffViewerWindow = null;
|
|
_diffViewerWindow = diffDlg;
|
|
diffDlg.Show(_owner);
|
|
};
|
|
vm.ConfirmAction = ConfirmAsync;
|
|
if (Shell is { } shell)
|
|
{
|
|
vm.ResolveMergeVm = shell.ResolveMergeVm;
|
|
vm.ShowMergeAction = async mergeVm =>
|
|
{
|
|
var mergeDlg = new MergeModalView { DataContext = mergeVm };
|
|
await mergeDlg.ShowDialog(_owner);
|
|
};
|
|
}
|
|
await dlg.ShowDialog(_owner);
|
|
}
|
|
|
|
public async Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm)
|
|
{
|
|
var dlg = new WorkerConnectionModalView { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
await dlg.ShowDialog(_owner);
|
|
}
|
|
|
|
public async Task ShowConflictResolverAsync(ConflictResolverViewModel vm)
|
|
{
|
|
var dlg = new ConflictResolverView { DataContext = vm };
|
|
await dlg.ShowDialog(_owner);
|
|
}
|
|
|
|
public async Task ShowLogVisualizerAsync(LogVisualizerViewModel vm)
|
|
{
|
|
var dlg = new LogVisualizerView { DataContext = vm };
|
|
vm.CloseAction = () => dlg.Close();
|
|
await dlg.ShowDialog(_owner);
|
|
}
|
|
|
|
public void ShowMissionControl(MissionControlViewModel vm)
|
|
{
|
|
_missionControl ??= new MissionControlWindow { DataContext = vm };
|
|
if (!_missionControl.IsVisible)
|
|
_missionControl.Show(); // modeless, independent top-level window
|
|
_missionControl.Activate(); // bring to front / focus
|
|
}
|
|
|
|
public Task<bool> ConfirmAsync(string message)
|
|
{
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
var cancel = new Button { Content = "Cancel", MinWidth = 90 };
|
|
var confirm = new Button { Content = "Confirm", MinWidth = 90, Classes = { "danger" } };
|
|
|
|
var dialog = NoticeWindow("Confirm", 380, message,
|
|
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 tcs.Task;
|
|
}
|
|
|
|
public Task ShowErrorAsync(string message)
|
|
{
|
|
var ok = new Button { Content = "OK", MinWidth = 90 };
|
|
var dialog = NoticeWindow("Error", 360, message,
|
|
new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
Spacing = 8,
|
|
Children = { ok },
|
|
});
|
|
ok.Click += (_, _) => dialog.Close();
|
|
return dialog.ShowDialog(_owner);
|
|
}
|
|
|
|
private Window NoticeWindow(string title, double width, string message, StackPanel buttons) => new()
|
|
{
|
|
Title = title,
|
|
Width = width,
|
|
SizeToContent = SizeToContent.Height,
|
|
CanResize = false,
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
ShowInTaskbar = false,
|
|
Background = _owner.FindResource("SurfaceBrush") as IBrush,
|
|
Content = new StackPanel
|
|
{
|
|
Margin = new Thickness(20),
|
|
Spacing = 16,
|
|
Children =
|
|
{
|
|
new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
|
|
buttons,
|
|
},
|
|
},
|
|
};
|
|
}
|