feat(mission-control): make overview panes and queue strip individually resizable
Replace the UniformGrid (all tiles forced to equal size) with a real Grid + GridSplitters built in code-behind per pane count/column count, so panes resize individually. The queue strip's fixed 210px column becomes a drag-resizable Grid column (min 160px) that collapses to 0 when nothing is queued, replacing the old dock-based fixed width.
This commit is contained in:
@@ -51,85 +51,98 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Read-only queue strip — collapses when nothing is queued -->
|
<!-- Body: main content | queue splitter | queue strip (widths managed in code-behind
|
||||||
<Border DockPanel.Dock="Right"
|
so the queue column collapses to zero when nothing is queued). -->
|
||||||
IsVisible="{Binding HasQueued}"
|
<Grid x:Name="BodyGrid">
|
||||||
Width="210"
|
<Grid.ColumnDefinitions>
|
||||||
Background="{DynamicResource DeepBrush}"
|
<ColumnDefinition Width="*"/>
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
<ColumnDefinition Width="Auto"/>
|
||||||
BorderThickness="1,0,0,0">
|
<ColumnDefinition Width="0"/>
|
||||||
<DockPanel LastChildFill="True" Margin="10,10">
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock DockPanel.Dock="Top" Classes="eyebrow"
|
|
||||||
Text="{loc:Tr missionControl.queue}"
|
<!-- Grid / tabs / empty state -->
|
||||||
|
<Panel Grid.Column="0" Margin="6">
|
||||||
|
<!-- Overview mode: a real Grid built in code-behind so each pane is individually
|
||||||
|
resizable via GridSplitters, instead of a UniformGrid where every cell is forced
|
||||||
|
to the same size. -->
|
||||||
|
<Grid x:Name="OverviewGrid" IsVisible="{Binding !IsFocusMode}" />
|
||||||
|
|
||||||
|
<!-- Focus mode: one pane large, switched via tabs -->
|
||||||
|
<TabControl ItemsSource="{Binding Panes}"
|
||||||
|
SelectedItem="{Binding FocusedPane}"
|
||||||
|
IsVisible="{Binding IsFocusMode}">
|
||||||
|
<TabControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vmm:IMissionControlPane">
|
||||||
|
<TextBlock Text="{Binding DisplayTitle}" TextTrimming="CharacterEllipsis" MaxWidth="160" />
|
||||||
|
</DataTemplate>
|
||||||
|
</TabControl.ItemTemplate>
|
||||||
|
</TabControl>
|
||||||
|
|
||||||
|
<TextBlock IsVisible="{Binding !HasPanes}"
|
||||||
|
Text="{loc:Tr missionControl.empty}"
|
||||||
Foreground="{DynamicResource TextMuteBrush}"
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
LetterSpacing="1.4" Margin="0,0,0,8" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
<ScrollViewer>
|
</Panel>
|
||||||
<ItemsControl ItemsSource="{Binding Queued}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:QueuedTaskViewModel">
|
|
||||||
<Button Command="{Binding OpenInAppCommand}"
|
|
||||||
Padding="0" Margin="0,0,0,4"
|
|
||||||
Background="Transparent" BorderThickness="0"
|
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
|
||||||
Cursor="Hand">
|
|
||||||
<Panel>
|
|
||||||
<Border Background="{DynamicResource SurfaceBrush}"
|
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
|
||||||
BorderThickness="1" CornerRadius="6" />
|
|
||||||
<Border Background="{DynamicResource RunningTintBrush}"
|
|
||||||
BorderBrush="{DynamicResource RunningTintBorderBrush}"
|
|
||||||
BorderThickness="1" CornerRadius="6"
|
|
||||||
IsVisible="{Binding IsRunning}" />
|
|
||||||
<StackPanel Margin="8,6" Spacing="2">
|
|
||||||
<TextBlock Text="{Binding Title}"
|
|
||||||
TextTrimming="CharacterEllipsis"
|
|
||||||
ToolTip.Tip="{Binding Title}"
|
|
||||||
Foreground="{DynamicResource TextDimBrush}" />
|
|
||||||
<TextBlock Classes="meta"
|
|
||||||
Text="{loc:Tr missionControl.blocked}"
|
|
||||||
IsVisible="{Binding IsBlocked}"
|
|
||||||
Foreground="{DynamicResource AmberBrush}" />
|
|
||||||
<TextBlock Classes="meta"
|
|
||||||
Text="{loc:Tr missionControl.running}"
|
|
||||||
IsVisible="{Binding IsRunning}"
|
|
||||||
Foreground="{DynamicResource StatusRunningBrush}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Panel>
|
|
||||||
</Button>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Grid / tabs / empty state -->
|
<GridSplitter x:Name="QueueSplitter" Grid.Column="1"
|
||||||
<Panel Margin="6">
|
IsVisible="{Binding HasQueued}"
|
||||||
<!-- Overview mode: the existing UniformGrid, one tile per pane -->
|
Width="6"
|
||||||
<ItemsControl ItemsSource="{Binding Panes}" IsVisible="{Binding !IsFocusMode}">
|
Background="Transparent"
|
||||||
<ItemsControl.ItemsPanel>
|
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||||
<ItemsPanelTemplate>
|
ResizeDirection="Columns"
|
||||||
<UniformGrid x:CompileBindings="False" Columns="{Binding ColumnCount}" />
|
ResizeBehavior="PreviousAndNext"/>
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ItemsControl.ItemsPanel>
|
|
||||||
</ItemsControl>
|
|
||||||
|
|
||||||
<!-- Focus mode: one pane large, switched via tabs -->
|
<!-- Read-only queue strip — collapses when nothing is queued -->
|
||||||
<TabControl ItemsSource="{Binding Panes}"
|
<Border Grid.Column="2"
|
||||||
SelectedItem="{Binding FocusedPane}"
|
IsVisible="{Binding HasQueued}"
|
||||||
IsVisible="{Binding IsFocusMode}">
|
Background="{DynamicResource DeepBrush}"
|
||||||
<TabControl.ItemTemplate>
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
<DataTemplate x:DataType="vmm:IMissionControlPane">
|
BorderThickness="1,0,0,0">
|
||||||
<TextBlock Text="{Binding DisplayTitle}" TextTrimming="CharacterEllipsis" MaxWidth="160" />
|
<DockPanel LastChildFill="True" Margin="10,10">
|
||||||
</DataTemplate>
|
<TextBlock DockPanel.Dock="Top" Classes="eyebrow"
|
||||||
</TabControl.ItemTemplate>
|
Text="{loc:Tr missionControl.queue}"
|
||||||
</TabControl>
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
|
LetterSpacing="1.4" Margin="0,0,0,8" />
|
||||||
<TextBlock IsVisible="{Binding !HasPanes}"
|
<ScrollViewer>
|
||||||
Text="{loc:Tr missionControl.empty}"
|
<ItemsControl ItemsSource="{Binding Queued}">
|
||||||
Foreground="{DynamicResource TextMuteBrush}"
|
<ItemsControl.ItemTemplate>
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
<DataTemplate x:DataType="vm:QueuedTaskViewModel">
|
||||||
</Panel>
|
<Button Command="{Binding OpenInAppCommand}"
|
||||||
|
Padding="0" Margin="0,0,0,4"
|
||||||
|
Background="Transparent" BorderThickness="0"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||||
|
Cursor="Hand">
|
||||||
|
<Panel>
|
||||||
|
<Border Background="{DynamicResource SurfaceBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6" />
|
||||||
|
<Border Background="{DynamicResource RunningTintBrush}"
|
||||||
|
BorderBrush="{DynamicResource RunningTintBorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6"
|
||||||
|
IsVisible="{Binding IsRunning}" />
|
||||||
|
<StackPanel Margin="8,6" Spacing="2">
|
||||||
|
<TextBlock Text="{Binding Title}"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
ToolTip.Tip="{Binding Title}"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}" />
|
||||||
|
<TextBlock Classes="meta"
|
||||||
|
Text="{loc:Tr missionControl.blocked}"
|
||||||
|
IsVisible="{Binding IsBlocked}"
|
||||||
|
Foreground="{DynamicResource AmberBrush}" />
|
||||||
|
<TextBlock Classes="meta"
|
||||||
|
Text="{loc:Tr missionControl.running}"
|
||||||
|
IsVisible="{Binding IsRunning}"
|
||||||
|
Foreground="{DynamicResource StatusRunningBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Panel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
|
|
||||||
@@ -7,9 +12,20 @@ namespace ClaudeDo.Ui.Views.MissionControl;
|
|||||||
|
|
||||||
public partial class MissionControlView : UserControl
|
public partial class MissionControlView : UserControl
|
||||||
{
|
{
|
||||||
|
private const double SplitterSize = 6;
|
||||||
|
private const double DefaultQueueWidth = 210;
|
||||||
|
private const double MinQueueWidth = 160;
|
||||||
|
private const double MinPaneWidth = 220;
|
||||||
|
private const double MinPaneHeight = 140;
|
||||||
|
|
||||||
|
private MissionControlViewModel? _vm;
|
||||||
|
private GridLength _queueWidth = new(DefaultQueueWidth, GridUnitType.Pixel);
|
||||||
|
|
||||||
public MissionControlView()
|
public MissionControlView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
DataContextChanged += OnDataContextChanged;
|
||||||
|
QueueSplitter.DragCompleted += (_, _) => _queueWidth = BodyGrid.ColumnDefinitions[2].Width;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ad-hoc ConPTY session: the view owns the folder picker, the VM only takes the chosen path.
|
// Ad-hoc ConPTY session: the view owns the folder picker, the VM only takes the chosen path.
|
||||||
@@ -28,4 +44,127 @@ public partial class MissionControlView : UserControl
|
|||||||
|
|
||||||
await vm.OpenAdHocConPtySessionAsync(folders[0].Path.LocalPath);
|
await vm.OpenAdHocConPtySessionAsync(folders[0].Path.LocalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_vm is not null)
|
||||||
|
{
|
||||||
|
_vm.Panes.CollectionChanged -= OnPanesChanged;
|
||||||
|
_vm.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
_vm = DataContext as MissionControlViewModel;
|
||||||
|
|
||||||
|
if (_vm is not null)
|
||||||
|
{
|
||||||
|
_vm.Panes.CollectionChanged += OnPanesChanged;
|
||||||
|
_vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
RebuildOverviewGrid();
|
||||||
|
UpdateQueueColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanesChanged(object? sender, NotifyCollectionChangedEventArgs e) => RebuildOverviewGrid();
|
||||||
|
|
||||||
|
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(MissionControlViewModel.ColumnCount))
|
||||||
|
RebuildOverviewGrid();
|
||||||
|
else if (e.PropertyName == nameof(MissionControlViewModel.HasQueued))
|
||||||
|
UpdateQueueColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The queue column is Pixel-widthed (so the splitter can drag it) — when nothing is
|
||||||
|
// queued it collapses to 0 instead of leaving a blank gap, and remembers the last
|
||||||
|
// dragged width for the rest of this window's lifetime (not persisted beyond it).
|
||||||
|
private void UpdateQueueColumn()
|
||||||
|
{
|
||||||
|
var queueColumn = BodyGrid.ColumnDefinitions[2];
|
||||||
|
if (_vm?.HasQueued == true)
|
||||||
|
{
|
||||||
|
queueColumn.MinWidth = MinQueueWidth;
|
||||||
|
queueColumn.Width = _queueWidth;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
queueColumn.MinWidth = 0;
|
||||||
|
queueColumn.Width = new GridLength(0, GridUnitType.Pixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replaces the old UniformGrid with a real Grid + GridSplitters so panes resize
|
||||||
|
// individually. Column splitters span every row (resize that whole column vs. its
|
||||||
|
// neighbor); row splitters span every column. Rebuilt from scratch whenever the pane
|
||||||
|
// count or column count changes — per-pane sizes are intentionally not preserved across
|
||||||
|
// a rebuild (session-only resizing, reset to the default split when panes come/go).
|
||||||
|
private void RebuildOverviewGrid()
|
||||||
|
{
|
||||||
|
OverviewGrid.Children.Clear();
|
||||||
|
OverviewGrid.ColumnDefinitions.Clear();
|
||||||
|
OverviewGrid.RowDefinitions.Clear();
|
||||||
|
|
||||||
|
var panes = _vm?.Panes;
|
||||||
|
if (panes is not { Count: > 0 }) return;
|
||||||
|
|
||||||
|
var columns = Math.Max(1, _vm!.ColumnCount);
|
||||||
|
var rows = (int)Math.Ceiling(panes.Count / (double)columns);
|
||||||
|
|
||||||
|
for (var c = 0; c < columns; c++)
|
||||||
|
{
|
||||||
|
if (c > 0) OverviewGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||||
|
OverviewGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)) { MinWidth = MinPaneWidth });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var r = 0; r < rows; r++)
|
||||||
|
{
|
||||||
|
if (r > 0) OverviewGrid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||||
|
OverviewGrid.RowDefinitions.Add(new RowDefinition(new GridLength(1, GridUnitType.Star)) { MinHeight = MinPaneHeight });
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalGridRows = rows * 2 - 1;
|
||||||
|
var totalGridCols = columns * 2 - 1;
|
||||||
|
|
||||||
|
for (var c = 1; c < columns; c++)
|
||||||
|
{
|
||||||
|
var splitter = new GridSplitter
|
||||||
|
{
|
||||||
|
Width = SplitterSize,
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
|
ResizeDirection = GridResizeDirection.Columns,
|
||||||
|
ResizeBehavior = GridResizeBehavior.PreviousAndNext,
|
||||||
|
};
|
||||||
|
Grid.SetColumn(splitter, c * 2 - 1);
|
||||||
|
Grid.SetRow(splitter, 0);
|
||||||
|
Grid.SetRowSpan(splitter, totalGridRows);
|
||||||
|
OverviewGrid.Children.Add(splitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var r = 1; r < rows; r++)
|
||||||
|
{
|
||||||
|
var splitter = new GridSplitter
|
||||||
|
{
|
||||||
|
Height = SplitterSize,
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
|
ResizeDirection = GridResizeDirection.Rows,
|
||||||
|
ResizeBehavior = GridResizeBehavior.PreviousAndNext,
|
||||||
|
};
|
||||||
|
Grid.SetColumn(splitter, 0);
|
||||||
|
Grid.SetColumnSpan(splitter, totalGridCols);
|
||||||
|
Grid.SetRow(splitter, r * 2 - 1);
|
||||||
|
OverviewGrid.Children.Add(splitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < panes.Count; i++)
|
||||||
|
{
|
||||||
|
var presenter = new ContentControl { Content = panes[i] };
|
||||||
|
Grid.SetColumn(presenter, (i % columns) * 2);
|
||||||
|
Grid.SetRow(presenter, (i / columns) * 2);
|
||||||
|
OverviewGrid.Children.Add(presenter);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user