Merge claudedo/2a790a4416924ed6beda0880dcf16836

This commit is contained in:
mika kuns
2026-08-10 15:00:04 +02:00
7 changed files with 95 additions and 11 deletions
+3 -1
View File
@@ -365,7 +365,9 @@
"splitView": "Nebeneinander",
"wrapLines": "Zeilenumbruch",
"paneBase": "BASIS",
"paneWorktree": "WORKTREE"
"paneWorktree": "WORKTREE",
"maximize": "Maximieren",
"restore": "Wiederherstellen"
},
"worktreesOverview": {
"refresh": "Aktualisieren",
+3 -1
View File
@@ -365,7 +365,9 @@
"splitView": "Side by side",
"wrapLines": "Wrap lines",
"paneBase": "BASE",
"paneWorktree": "WORKTREE"
"paneWorktree": "WORKTREE",
"maximize": "Maximize",
"restore": "Restore"
},
"worktreesOverview": {
"refresh": "Refresh",
@@ -14,14 +14,19 @@
Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<Grid ColumnDefinitions="*,Auto,Auto" Margin="14,0">
<TextBlock Text="{TemplateBinding Title}"
FontFamily="{DynamicResource MonoFont}"
FontSize="{DynamicResource FontSizeMono}"
LetterSpacing="1.4"
Foreground="{DynamicResource TextBrush}"
VerticalAlignment="Center"/>
<Button Grid.Column="1" Classes="icon-btn" Content="✕"
<Button Name="PART_MaxButton" Grid.Column="1" Classes="icon-btn"
IsVisible="{TemplateBinding ShowMaximizeButton}"
VerticalAlignment="Center">
<PathIcon Name="PART_MaxIcon" Data="{StaticResource Icon.WinMax}" Width="10" Height="10"/>
</Button>
<Button Grid.Column="2" Classes="icon-btn" Content="✕"
FontSize="{DynamicResource FontSizeBody}"
Command="{TemplateBinding CloseCommand}"
VerticalAlignment="Center"/>
@@ -3,6 +3,8 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
namespace ClaudeDo.Ui.Views.Controls;
@@ -18,17 +20,29 @@ public class ModalShell : ContentControl
public static readonly StyledProperty<ICommand?> CloseCommandProperty =
AvaloniaProperty.Register<ModalShell, ICommand?>(nameof(CloseCommand));
// Opt-in: most modals are fixed-purpose dialogs that don't need maximise. Set true only
// on windows (e.g. the diff viewer) that benefit from a maximised full-screen layout.
public static readonly StyledProperty<bool> ShowMaximizeButtonProperty =
AvaloniaProperty.Register<ModalShell, bool>(nameof(ShowMaximizeButton));
public string? Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public object? Footer { get => GetValue(FooterProperty); set => SetValue(FooterProperty, value); }
public ICommand? CloseCommand { get => GetValue(CloseCommandProperty); set => SetValue(CloseCommandProperty, value); }
public bool ShowMaximizeButton { get => GetValue(ShowMaximizeButtonProperty); set => SetValue(ShowMaximizeButtonProperty, value); }
private Window? _window;
private PathIcon? _maxIcon;
private Button? _maxButton;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (e.NameScope.Find<Border>("PART_TitleBar") is { } bar)
bar.PointerPressed += OnTitleBarPressed;
_maxButton = e.NameScope.Find<Button>("PART_MaxButton");
if (_maxButton is not null) _maxButton.Click += OnMaxButtonClick;
_maxIcon = e.NameScope.Find<PathIcon>("PART_MaxIcon");
UpdateMaxIcon();
}
/// Mirrors what MainWindow does for itself: with an extended client area a maximised window
@@ -42,6 +56,7 @@ public class ModalShell : ContentControl
if (_window is null) return;
Margin = _window.OffScreenMargin;
_window.PropertyChanged += OnWindowPropertyChanged;
UpdateMaxIcon();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
@@ -55,6 +70,32 @@ public class ModalShell : ContentControl
{
if (e.Property == Window.OffScreenMarginProperty && _window is not null)
Margin = _window.OffScreenMargin;
if (e.Property == Window.WindowStateProperty)
UpdateMaxIcon();
}
private void UpdateMaxIcon()
{
if (_maxIcon is null || _window is null) return;
var maximized = _window.WindowState == WindowState.Maximized;
var key = maximized ? "Icon.WinRestore" : "Icon.WinMax";
if (this.TryFindResource(key, out var geometry) && geometry is Geometry g)
_maxIcon.Data = g;
if (_maxButton is not null)
{
var tipKey = maximized ? "modals.diff.restore" : "modals.diff.maximize";
ToolTip.SetTip(_maxButton, ClaudeDo.Ui.Localization.Loc.T(tipKey));
}
}
private void OnMaxButtonClick(object? sender, RoutedEventArgs e) => ToggleMaximize();
private void ToggleMaximize()
{
if (_window is null) return;
_window.WindowState = _window.WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
}
/// <summary>
@@ -74,6 +115,11 @@ public class ModalShell : ContentControl
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
if (ShowMaximizeButton && e.ClickCount == 2)
{
ToggleMaximize();
return;
}
(TopLevel.GetTopLevel(this) as Window)?.BeginMoveDrag(e);
}
}
@@ -18,6 +18,7 @@ public partial class DetailsIslandView : UserControl
// never dragged stay dynamic (Auto-sized description).
private readonly Dictionary<string, double> _descriptionHeights = new();
private DetailsIslandViewModel? _vm;
private DiffViewerView? _diffViewerWindow;
public DetailsIslandView()
{
@@ -142,12 +143,26 @@ public partial class DetailsIslandView : UserControl
vm.PropertyChanged += OnViewModelPropertyChanged;
ApplyResizeStateForCurrentTask();
vm.Merge.ShowDiffViewer = async (diffVm) =>
// Non-modal: the diff viewer must not block navigating the rest of the app.
// Reuse the one open window (swap its DataContext + bring to front) instead of
// stacking a second one when the user re-opens the diff for another task.
vm.Merge.ShowDiffViewer = (diffVm) =>
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner == null) return;
var modal = new DiffViewerView { DataContext = diffVm };
await modal.ShowDialog(owner);
if (owner == null) return System.Threading.Tasks.Task.CompletedTask;
if (_diffViewerWindow is { } existing)
{
existing.DataContext = diffVm;
if (existing.WindowState == WindowState.Minimized)
existing.WindowState = WindowState.Normal;
existing.Activate();
return System.Threading.Tasks.Task.CompletedTask;
}
var window = new DiffViewerView { DataContext = diffVm };
window.Closed += (_, _) => _diffViewerWindow = null;
_diffViewerWindow = window;
window.Show(owner);
return System.Threading.Tasks.Task.CompletedTask;
};
vm.Merge.ShowMergeModal = async (mergeVm) =>
@@ -18,7 +18,7 @@
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="{loc:Tr modals.diff.title}" CloseCommand="{Binding CloseCommand}">
<ctl:ModalShell Title="{loc:Tr modals.diff.title}" CloseCommand="{Binding CloseCommand}" ShowMaximizeButton="True">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
+16 -2
View File
@@ -27,6 +27,7 @@ public sealed class WindowDialogService : IDialogService
{
private readonly Window _owner;
private MissionControlWindow? _missionControl;
private DiffViewerView? _diffViewerWindow;
public WindowDialogService(Window owner) => _owner = owner;
@@ -108,10 +109,23 @@ public sealed class WindowDialogService : IDialogService
};
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();
_ = diffVm.LoadAsync();
_ = diffDlg.ShowDialog(_owner);
diffDlg.Closed += (_, _) => _diffViewerWindow = null;
_diffViewerWindow = diffDlg;
diffDlg.Show(_owner);
};
vm.ConfirmAction = ConfirmAsync;
if (Shell is { } shell)