refactor: adopt WinUI.Dock approach for layout updates and tab content hosting

This commit is contained in:
2026-03-29 15:16:08 +09:00
parent 70b7e56eb7
commit 15870ffe89
6 changed files with 60 additions and 201 deletions

View File

@@ -7,6 +7,7 @@
<ResourceDictionary Source="/View/Controls/Docking/DockGroup.xaml" />
<ResourceDictionary Source="/View/Controls/Docking/DockPanel.xaml" />
<ResourceDictionary Source="/View/Controls/Docking/DockRegionHighlight.xaml" />
<ResourceDictionary Source="/View/Controls/Docking/DockDocument.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,21 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.View.Controls.Docking">
<Style TargetType="local:DockDocument">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockDocument">
<Border Background="Transparent">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -115,70 +115,41 @@ public partial class DockGroup : DockContainer
var selectedDoc = _tabView.SelectedItem is TabViewItem selectedItem ? selectedItem.Tag as DockDocument : null;
// Remove tabs that are no longer in Children
for (int i = _tabView.TabItems.Count - 1; i >= 0; i--)
// Detach all existing tabs
foreach (var item in _tabView.TabItems)
{
if (_tabView.TabItems[i] is TabViewItem tabItem && tabItem.Tag is DockDocument doc)
if (item is TabViewItem tabItem)
{
if (!Children.Contains(doc))
{
tabItem.ClearValue(ContentControl.ContentProperty);
tabItem.Content = null;
_tabView.TabItems.RemoveAt(i);
}
}
}
_tabView.TabItems.Clear();
TabViewItem? newSelectedItem = null;
// Add tabs that are in Children but not in TabItems, and ensure correct order
for (int i = 0; i < Children.Count; i++)
foreach (var child in Children)
{
if (Children[i] is DockDocument doc)
if (child is DockDocument doc)
{
TabViewItem? existingTab = null;
for (int j = 0; j < _tabView.TabItems.Count; j++)
{
if (_tabView.TabItems[j] is TabViewItem tabItem && tabItem.Tag == doc)
{
existingTab = tabItem;
// Fix order if necessary
if (j != i)
{
_tabView.TabItems.RemoveAt(j);
_tabView.TabItems.Insert(i, existingTab);
}
break;
}
}
if (existingTab == null)
{
existingTab = new TabViewItem
var tabItem = new TabViewItem
{
Tag = doc
};
existingTab.SetBinding(TabViewItem.HeaderProperty, new Binding
tabItem.SetBinding(TabViewItem.HeaderProperty, new Microsoft.UI.Xaml.Data.Binding
{
Source = doc,
Path = new PropertyPath(nameof(DockDocument.Title)),
Mode = BindingMode.OneWay
Mode = Microsoft.UI.Xaml.Data.BindingMode.OneWay
});
existingTab.SetBinding(ContentControl.ContentProperty, new Binding
{
Source = doc,
Path = new PropertyPath(nameof(DockDocument.Content)),
Mode = BindingMode.OneWay
});
tabItem.Content = doc;
_tabView.TabItems.Insert(i, existingTab);
}
_tabView.TabItems.Add(tabItem);
if (doc == selectedDoc)
{
newSelectedItem = existingTab;
newSelectedItem = tabItem;
}
}
}

View File

@@ -1,6 +1,6 @@
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using CommunityToolkit.WinUI.Controls;
namespace Ghost.Editor.View.Controls.Docking;
@@ -8,10 +8,10 @@ namespace Ghost.Editor.View.Controls.Docking;
/// A container that can host multiple dock modules with splitters.
/// </summary>
[TemplatePart(Name = PART_GRID, Type = typeof(Grid))]
public partial class DockPanel : DockContainer
public class DockPanel : DockContainer
{
private const string PART_GRID = "PART_Grid";
private const double SPLITTER_THICKNESS = 1;
private const double SPLITTER_THICKNESS = 4;
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
nameof(Orientation), typeof(Orientation), typeof(DockPanel), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
@@ -74,123 +74,44 @@ public partial class DockPanel : DockContainer
{
if (_grid == null) return;
// Remove splitters and children that are no longer in the collection
for (int i = _grid.Children.Count - 1; i >= 0; i--)
{
var child = _grid.Children[i];
if (child is GridSplitter)
{
_grid.Children.RemoveAt(i);
}
else if (child is DockModule module && !Children.Contains(module))
{
_grid.Children.RemoveAt(i);
}
}
if (Children.Count == 0)
{
_grid.Children.Clear();
_grid.RowDefinitions.Clear();
_grid.ColumnDefinitions.Clear();
return;
}
if (Children.Count == 0) return;
if (Orientation == Orientation.Horizontal)
{
_grid.RowDefinitions.Clear();
int requiredColumns = (Children.Count * 2) - 1;
while (_grid.ColumnDefinitions.Count > requiredColumns)
{
_grid.ColumnDefinitions.RemoveAt(_grid.ColumnDefinitions.Count - 1);
}
for (var i = 0; i < Children.Count; i++)
{
int columnIndex = i * 2;
if (columnIndex >= _grid.ColumnDefinitions.Count)
for (int i = 0; i < Children.Count; i++)
{
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
}
else
{
_grid.ColumnDefinitions[columnIndex].Width = new GridLength(1, GridUnitType.Star);
}
var child = Children[i];
if (!_grid.Children.Contains(child))
{
Grid.SetColumn(child, i * 2);
_grid.Children.Add(child);
}
Grid.SetColumn(child, columnIndex);
Grid.SetRow(child, 0);
if (i < Children.Count - 1)
{
int splitterIndex = i * 2 + 1;
if (splitterIndex >= _grid.ColumnDefinitions.Count)
{
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
}
else
{
_grid.ColumnDefinitions[splitterIndex].Width = GridLength.Auto;
}
var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Columns, Width = SPLITTER_THICKNESS };
Grid.SetColumn(splitter, splitterIndex);
Grid.SetRow(splitter, 0);
Grid.SetColumn(splitter, i * 2 + 1);
_grid.Children.Add(splitter);
}
}
}
else
{
_grid.ColumnDefinitions.Clear();
int requiredRows = (Children.Count * 2) - 1;
while (_grid.RowDefinitions.Count > requiredRows)
{
_grid.RowDefinitions.RemoveAt(_grid.RowDefinitions.Count - 1);
}
for (var i = 0; i < Children.Count; i++)
{
int rowIndex = i * 2;
if (rowIndex >= _grid.RowDefinitions.Count)
for (int i = 0; i < Children.Count; i++)
{
_grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
}
else
{
_grid.RowDefinitions[rowIndex].Height = new GridLength(1, GridUnitType.Star);
}
var child = Children[i];
if (!_grid.Children.Contains(child))
{
Grid.SetRow(child, i * 2);
_grid.Children.Add(child);
}
Grid.SetRow(child, rowIndex);
Grid.SetColumn(child, 0);
if (i < Children.Count - 1)
{
int splitterIndex = i * 2 + 1;
if (splitterIndex >= _grid.RowDefinitions.Count)
{
_grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
}
else
{
_grid.RowDefinitions[splitterIndex].Height = GridLength.Auto;
}
var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Rows, Height = SPLITTER_THICKNESS };
Grid.SetRow(splitter, splitterIndex);
Grid.SetColumn(splitter, 0);
Grid.SetRow(splitter, i * 2 + 1);
_grid.Children.Add(splitter);
}
}

View File

@@ -152,7 +152,6 @@
<DataTemplate x:DataType="model:ExplorerItem">
<ItemContainer>
<Grid
Width="72"
Padding="8"
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -192,7 +191,7 @@
</ItemsView.ItemTemplate>
<ItemsView.Layout>
<UniformGridLayout
ItemsStretch="None"
ItemsStretch="Fill"
MinColumnSpacing="4"
MinItemWidth="72"
MinRowSpacing="4" />

View File

@@ -17,9 +17,6 @@ internal sealed partial class ProjectBrowser : UserControl
private readonly IInspectorService _inspectorService;
private bool _isUpdatingSelection = false;
private double _savedHorizontalOffset;
private double _savedVerticalOffset;
private ScrollViewer? _filesScrollViewer;
public ProjectBrowserViewModel ViewModel
{
@@ -52,43 +49,12 @@ internal sealed partial class ProjectBrowser : UserControl
private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e)
{
_inspectorService.OnSelectionChanged += _inspectorService_OnSelectionChanged;
PART_FilesView.UpdateLayout();
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
if (_filesScrollViewer == null)
{
_filesScrollViewer = FindVisualChild<ScrollViewer>(PART_FilesView);
}
if (_filesScrollViewer != null)
{
// Sometimes resetting the ItemsSource helps clear corrupted virtualization state
var itemsSource = PART_FilesView.ItemsSource;
PART_FilesView.ItemsSource = null;
PART_FilesView.ItemsSource = itemsSource;
PART_FilesView.UpdateLayout();
_filesScrollViewer.ChangeView(_savedHorizontalOffset, _savedVerticalOffset, null, true);
}
});
}
private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e)
{
_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged;
if (_filesScrollViewer == null)
{
_filesScrollViewer = FindVisualChild<ScrollViewer>(PART_FilesView);
}
if (_filesScrollViewer != null)
{
_savedHorizontalOffset = _filesScrollViewer.HorizontalOffset;
_savedVerticalOffset = _filesScrollViewer.VerticalOffset;
}
if (LastFocused == this)
{
LastFocused = null;
@@ -104,26 +70,6 @@ internal sealed partial class ProjectBrowser : UserControl
}
}
private static T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(parent, i);
if (child is T t)
{
return t;
}
var result = FindVisualChild<T>(child);
if (result != null)
{
return result;
}
}
return null;
}
private void PART_DirectoriesView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
{
if (_isUpdatingSelection)