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/DockGroup.xaml" />
<ResourceDictionary Source="/View/Controls/Docking/DockPanel.xaml" /> <ResourceDictionary Source="/View/Controls/Docking/DockPanel.xaml" />
<ResourceDictionary Source="/View/Controls/Docking/DockRegionHighlight.xaml" /> <ResourceDictionary Source="/View/Controls/Docking/DockRegionHighlight.xaml" />
<ResourceDictionary Source="/View/Controls/Docking/DockDocument.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </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; var selectedDoc = _tabView.SelectedItem is TabViewItem selectedItem ? selectedItem.Tag as DockDocument : null;
// Remove tabs that are no longer in Children // Detach all existing tabs
for (int i = _tabView.TabItems.Count - 1; i >= 0; i--) 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; tabItem.Content = null;
_tabView.TabItems.RemoveAt(i);
}
} }
} }
_tabView.TabItems.Clear();
TabViewItem? newSelectedItem = null; TabViewItem? newSelectedItem = null;
// Add tabs that are in Children but not in TabItems, and ensure correct order foreach (var child in Children)
for (int i = 0; i < Children.Count; i++)
{ {
if (Children[i] is DockDocument doc) if (child is DockDocument doc)
{ {
TabViewItem? existingTab = null; var tabItem = new TabViewItem
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
{ {
Tag = doc Tag = doc
}; };
existingTab.SetBinding(TabViewItem.HeaderProperty, new Binding tabItem.SetBinding(TabViewItem.HeaderProperty, new Microsoft.UI.Xaml.Data.Binding
{ {
Source = doc, Source = doc,
Path = new PropertyPath(nameof(DockDocument.Title)), Path = new PropertyPath(nameof(DockDocument.Title)),
Mode = BindingMode.OneWay Mode = Microsoft.UI.Xaml.Data.BindingMode.OneWay
}); });
existingTab.SetBinding(ContentControl.ContentProperty, new Binding tabItem.Content = doc;
{
Source = doc,
Path = new PropertyPath(nameof(DockDocument.Content)),
Mode = BindingMode.OneWay
});
_tabView.TabItems.Insert(i, existingTab); _tabView.TabItems.Add(tabItem);
}
if (doc == selectedDoc) 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;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using CommunityToolkit.WinUI.Controls;
namespace Ghost.Editor.View.Controls.Docking; 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. /// A container that can host multiple dock modules with splitters.
/// </summary> /// </summary>
[TemplatePart(Name = PART_GRID, Type = typeof(Grid))] [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 string PART_GRID = "PART_Grid";
private const double SPLITTER_THICKNESS = 1; private const double SPLITTER_THICKNESS = 4;
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
nameof(Orientation), typeof(Orientation), typeof(DockPanel), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged)); nameof(Orientation), typeof(Orientation), typeof(DockPanel), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
@@ -74,123 +74,44 @@ public partial class DockPanel : DockContainer
{ {
if (_grid == null) return; if (_grid == null) return;
// Remove splitters and children that are no longer in the collection _grid.Children.Clear();
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.RowDefinitions.Clear(); _grid.RowDefinitions.Clear();
_grid.ColumnDefinitions.Clear(); _grid.ColumnDefinitions.Clear();
return;
} if (Children.Count == 0) return;
if (Orientation == Orientation.Horizontal) if (Orientation == Orientation.Horizontal)
{ {
_grid.RowDefinitions.Clear(); for (int i = 0; i < Children.Count; i++)
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)
{ {
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); _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]; var child = Children[i];
if (!_grid.Children.Contains(child)) Grid.SetColumn(child, i * 2);
{
_grid.Children.Add(child); _grid.Children.Add(child);
}
Grid.SetColumn(child, columnIndex);
Grid.SetRow(child, 0);
if (i < Children.Count - 1) if (i < Children.Count - 1)
{
int splitterIndex = i * 2 + 1;
if (splitterIndex >= _grid.ColumnDefinitions.Count)
{ {
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); _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 }; var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Columns, Width = SPLITTER_THICKNESS };
Grid.SetColumn(splitter, splitterIndex); Grid.SetColumn(splitter, i * 2 + 1);
Grid.SetRow(splitter, 0);
_grid.Children.Add(splitter); _grid.Children.Add(splitter);
} }
} }
} }
else else
{ {
_grid.ColumnDefinitions.Clear(); for (int i = 0; i < Children.Count; i++)
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)
{ {
_grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); _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]; var child = Children[i];
if (!_grid.Children.Contains(child)) Grid.SetRow(child, i * 2);
{
_grid.Children.Add(child); _grid.Children.Add(child);
}
Grid.SetRow(child, rowIndex);
Grid.SetColumn(child, 0);
if (i < Children.Count - 1) if (i < Children.Count - 1)
{
int splitterIndex = i * 2 + 1;
if (splitterIndex >= _grid.RowDefinitions.Count)
{ {
_grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); _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 }; var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Rows, Height = SPLITTER_THICKNESS };
Grid.SetRow(splitter, splitterIndex); Grid.SetRow(splitter, i * 2 + 1);
Grid.SetColumn(splitter, 0);
_grid.Children.Add(splitter); _grid.Children.Add(splitter);
} }
} }

View File

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

View File

@@ -17,9 +17,6 @@ internal sealed partial class ProjectBrowser : UserControl
private readonly IInspectorService _inspectorService; private readonly IInspectorService _inspectorService;
private bool _isUpdatingSelection = false; private bool _isUpdatingSelection = false;
private double _savedHorizontalOffset;
private double _savedVerticalOffset;
private ScrollViewer? _filesScrollViewer;
public ProjectBrowserViewModel ViewModel public ProjectBrowserViewModel ViewModel
{ {
@@ -52,43 +49,12 @@ internal sealed partial class ProjectBrowser : UserControl
private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e) private void ProjectBrowser_Loaded(object sender, RoutedEventArgs e)
{ {
_inspectorService.OnSelectionChanged += _inspectorService_OnSelectionChanged; _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) private void ProjectBrowser_Unloaded(object sender, RoutedEventArgs e)
{ {
_inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged; _inspectorService.OnSelectionChanged -= _inspectorService_OnSelectionChanged;
if (_filesScrollViewer == null)
{
_filesScrollViewer = FindVisualChild<ScrollViewer>(PART_FilesView);
}
if (_filesScrollViewer != null)
{
_savedHorizontalOffset = _filesScrollViewer.HorizontalOffset;
_savedVerticalOffset = _filesScrollViewer.VerticalOffset;
}
if (LastFocused == this) if (LastFocused == this)
{ {
LastFocused = null; 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) private void PART_DirectoriesView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
{ {
if (_isUpdatingSelection) if (_isUpdatingSelection)