Skip to content

Commit c18be52

Browse files
authored
Feature: Added support for resizing columns in the Columns View (#17380)
1 parent c3e5222 commit c18be52

File tree

2 files changed

+236
-0
lines changed

2 files changed

+236
-0
lines changed

src/Files.App.Controls/BladeView/BladeItem.cs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using CommunityToolkit.WinUI;
5+
using Microsoft.UI.Input;
46
using Microsoft.UI.Xaml.Automation.Peers;
7+
using Microsoft.UI.Xaml.Input;
8+
using Microsoft.UI.Xaml.Media;
9+
using Windows.Foundation;
510

611
namespace Files.App.Controls
712
{
@@ -11,7 +16,14 @@ namespace Files.App.Controls
1116
[TemplatePart(Name = "CloseButton", Type = typeof(Button))]
1217
public partial class BladeItem : ContentControl
1318
{
19+
private const double MINIMUM_WIDTH = 150;
20+
private const double DEFAULT_WIDTH = 300; // Default width for the blade item
21+
1422
private Button _closeButton;
23+
private Border _bladeResizer;
24+
private bool _draggingSidebarResizer;
25+
private double _preManipulationSidebarWidth = 0;
26+
1527
/// <summary>
1628
/// Initializes a new instance of the <see cref="BladeItem"/> class.
1729
/// </summary>
@@ -36,7 +48,31 @@ protected override void OnApplyTemplate()
3648

3749
_closeButton.Click -= CloseButton_Click;
3850
_closeButton.Click += CloseButton_Click;
51+
52+
_bladeResizer = GetTemplateChild("BladeResizer") as Border;
53+
54+
if (_bladeResizer != null)
55+
{
56+
_bladeResizer.ManipulationStarted -= BladeResizer_ManipulationStarted;
57+
_bladeResizer.ManipulationStarted += BladeResizer_ManipulationStarted;
58+
59+
_bladeResizer.ManipulationDelta -= BladeResizer_ManipulationDelta;
60+
_bladeResizer.ManipulationDelta += BladeResizer_ManipulationDelta;
61+
62+
_bladeResizer.ManipulationCompleted -= BladeResizer_ManipulationCompleted;
63+
_bladeResizer.ManipulationCompleted += BladeResizer_ManipulationCompleted;
64+
65+
_bladeResizer.PointerEntered -= BladeResizer_PointerEntered;
66+
_bladeResizer.PointerEntered += BladeResizer_PointerEntered;
67+
68+
_bladeResizer.PointerExited -= BladeResizer_PointerExited;
69+
_bladeResizer.PointerExited += BladeResizer_PointerExited;
70+
71+
_bladeResizer.DoubleTapped -= BladeResizer_DoubleTapped;
72+
_bladeResizer.DoubleTapped += BladeResizer_DoubleTapped;
73+
}
3974
}
75+
4076
/// <summary>
4177
/// Creates AutomationPeer (<see cref="UIElement.OnCreateAutomationPeer"/>)
4278
/// </summary>
@@ -50,5 +86,166 @@ private void CloseButton_Click(object sender, RoutedEventArgs e)
5086
{
5187
IsOpen = false;
5288
}
89+
90+
private void BladeResizer_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
91+
{
92+
_draggingSidebarResizer = true;
93+
_preManipulationSidebarWidth = ActualWidth;
94+
VisualStateManager.GoToState(this, "ResizerPressed", true);
95+
e.Handled = true;
96+
}
97+
98+
private void BladeResizer_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
99+
{
100+
var newWidth = _preManipulationSidebarWidth + e.Cumulative.Translation.X;
101+
if (newWidth < MINIMUM_WIDTH)
102+
newWidth = MINIMUM_WIDTH;
103+
104+
Width = newWidth;
105+
e.Handled = true;
106+
}
107+
108+
private void BladeResizer_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
109+
{
110+
_draggingSidebarResizer = false;
111+
VisualStateManager.GoToState(this, "ResizerNormal", true);
112+
e.Handled = true;
113+
}
114+
115+
private void BladeResizer_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
116+
{
117+
var optimalWidth = CalculateOptimalWidth();
118+
if (optimalWidth > 0)
119+
{
120+
Width = Math.Max(optimalWidth, MINIMUM_WIDTH);
121+
}
122+
else
123+
{
124+
// Fallback to default width if calculation fails
125+
Width = DEFAULT_WIDTH;
126+
}
127+
128+
e.Handled = true;
129+
}
130+
131+
private double CalculateOptimalWidth()
132+
{
133+
try
134+
{
135+
// Look for any ListView within this BladeItem that contains text content
136+
var listView = this.FindDescendant<ListView>();
137+
if (listView?.Items == null || !listView.Items.Any())
138+
return 0;
139+
140+
// Calculate the maximum width needed by measuring text content
141+
var maxTextWidth = MeasureContentWidth(listView);
142+
143+
// Add padding for icon, margins, and other UI elements
144+
// Icon width (32) + margins (24) + padding (24) + chevron/tags (40) = 120
145+
var totalPadding = 120;
146+
147+
return maxTextWidth + totalPadding;
148+
}
149+
catch (Exception)
150+
{
151+
return 0;
152+
}
153+
}
154+
155+
private double MeasureContentWidth(ListView listView)
156+
{
157+
try
158+
{
159+
double maxWidth = 0;
160+
161+
// Find all TextBlocks in the ListView using visual tree walking
162+
var textBlocks = GetTextBlocksFromVisualTree(listView);
163+
164+
if (textBlocks.Any())
165+
{
166+
// Measure each TextBlock and find the widest one
167+
foreach (var textBlock in textBlocks)
168+
{
169+
if (string.IsNullOrEmpty(textBlock.Text))
170+
continue;
171+
172+
// Create a measuring TextBlock with the same properties
173+
var measuringBlock = new TextBlock
174+
{
175+
Text = textBlock.Text,
176+
FontSize = textBlock.FontSize,
177+
FontFamily = textBlock.FontFamily,
178+
FontWeight = textBlock.FontWeight,
179+
FontStyle = textBlock.FontStyle
180+
};
181+
182+
measuringBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
183+
maxWidth = Math.Max(maxWidth, measuringBlock.DesiredSize.Width);
184+
}
185+
}
186+
else
187+
{
188+
// Fallback: estimate based on item count and average text width
189+
var itemCount = listView.Items.Count;
190+
if (itemCount > 0)
191+
{
192+
// Estimate average filename length and multiply by character width
193+
var estimatedCharWidth = 8; // Approximate pixel width per character
194+
var estimatedMaxLength = Math.Min(50, Math.Max(20, itemCount * 2)); // Heuristic
195+
maxWidth = estimatedCharWidth * estimatedMaxLength;
196+
}
197+
}
198+
199+
return maxWidth;
200+
}
201+
catch (Exception)
202+
{
203+
// Fallback calculation
204+
return 200; // Default reasonable width
205+
}
206+
}
207+
208+
private List<TextBlock> GetTextBlocksFromVisualTree(DependencyObject parent)
209+
{
210+
var textBlocks = new List<TextBlock>();
211+
212+
if (parent == null)
213+
return textBlocks;
214+
215+
var childrenCount = VisualTreeHelper.GetChildrenCount(parent);
216+
for (int i = 0; i < childrenCount; i++)
217+
{
218+
var child = VisualTreeHelper.GetChild(parent, i);
219+
220+
if (child is TextBlock textBlock)
221+
{
222+
textBlocks.Add(textBlock);
223+
}
224+
225+
// Recursively search child elements
226+
textBlocks.AddRange(GetTextBlocksFromVisualTree(child));
227+
}
228+
229+
return textBlocks;
230+
}
231+
232+
private void BladeResizer_PointerEntered(object sender, PointerRoutedEventArgs e)
233+
{
234+
var sidebarResizer = (FrameworkElement)sender;
235+
sidebarResizer.ChangeCursor(InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast));
236+
VisualStateManager.GoToState(this, "ResizerPointerOver", true);
237+
e.Handled = true;
238+
}
239+
240+
private void BladeResizer_PointerExited(object sender, PointerRoutedEventArgs e)
241+
{
242+
if (_draggingSidebarResizer)
243+
return;
244+
245+
var sidebarResizer = (FrameworkElement)sender;
246+
sidebarResizer.ChangeCursor(InputSystemCursor.Create(InputSystemCursorShape.Arrow));
247+
VisualStateManager.GoToState(this, "ResizerNormal", true);
248+
e.Handled = true;
249+
}
53250
}
54251
}

src/Files.App.Controls/BladeView/BladeView.xaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@
114114
VerticalAlignment="Stretch"
115115
Background="{TemplateBinding Background}" />
116116

117+
<!-- Resizer -->
118+
<Border
119+
x:Name="BladeResizer"
120+
Width="4"
121+
HorizontalAlignment="Right"
122+
VerticalAlignment="Stretch"
123+
AllowFocusOnInteraction="True"
124+
Background="Transparent"
125+
BorderBrush="Transparent"
126+
CornerRadius="2"
127+
ManipulationMode="TranslateX" />
128+
117129
<Button
118130
Name="CloseButton"
119131
HorizontalAlignment="Right"
@@ -127,6 +139,33 @@
127139
Style="{StaticResource ButtonRevealStyle}"
128140
TabIndex="0"
129141
Visibility="{TemplateBinding CloseButtonVisibility}" />
142+
143+
<VisualStateManager.VisualStateGroups>
144+
<VisualStateGroup x:Name="ResizerStates">
145+
<VisualState x:Name="ResizerNormal" />
146+
<VisualState x:Name="ResizerPointerOver">
147+
<Storyboard>
148+
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BladeResizer" Storyboard.TargetProperty="Background">
149+
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource CardStrokeColorDefaultSolidBrush}" />
150+
</ObjectAnimationUsingKeyFrames>
151+
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BladeResizer" Storyboard.TargetProperty="BorderBrush">
152+
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource CardStrokeColorDefaultSolidBrush}" />
153+
</ObjectAnimationUsingKeyFrames>
154+
</Storyboard>
155+
</VisualState>
156+
157+
<VisualState x:Name="ResizerPressed">
158+
<Storyboard>
159+
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BladeResizer" Storyboard.TargetProperty="Background">
160+
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SolidBackgroundFillColorSecondaryBrush}" />
161+
</ObjectAnimationUsingKeyFrames>
162+
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BladeResizer" Storyboard.TargetProperty="BorderBrush">
163+
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SolidBackgroundFillColorSecondaryBrush}" />
164+
</ObjectAnimationUsingKeyFrames>
165+
</Storyboard>
166+
</VisualState>
167+
</VisualStateGroup>
168+
</VisualStateManager.VisualStateGroups>
130169
</Grid>
131170
</ControlTemplate>
132171
</Setter.Value>

0 commit comments

Comments
 (0)