Skip to content

Feature: Added support for resizing columns in the Columns View #17380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions src/Files.App.Controls/BladeView/BladeItem.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using CommunityToolkit.WinUI;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;

namespace Files.App.Controls
{
Expand All @@ -11,11 +16,18 @@
[TemplatePart(Name = "CloseButton", Type = typeof(Button))]
public partial class BladeItem : ContentControl
{
private const double MINIMUM_WIDTH = 150;
private const double DEFAULT_WIDTH = 300; // Default width for the blade item

private Button _closeButton;
private Border _bladeResizer;
private bool _draggingSidebarResizer;
private double _preManipulationSidebarWidth = 0;

/// <summary>
/// Initializes a new instance of the <see cref="BladeItem"/> class.
/// </summary>
public BladeItem()

Check warning on line 30 in src/Files.App.Controls/BladeView/BladeItem.cs

View workflow job for this annotation

GitHub Actions / build (Release, x64)

Non-nullable field '_closeButton' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in src/Files.App.Controls/BladeView/BladeItem.cs

View workflow job for this annotation

GitHub Actions / build (Debug, arm64)

Non-nullable field '_bladeResizer' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 30 in src/Files.App.Controls/BladeView/BladeItem.cs

View workflow job for this annotation

GitHub Actions / build (Debug, arm64)

Non-nullable field '_closeButton' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
{
DefaultStyleKey = typeof(BladeItem);
}
Expand All @@ -36,7 +48,31 @@

_closeButton.Click -= CloseButton_Click;
_closeButton.Click += CloseButton_Click;

_bladeResizer = GetTemplateChild("BladeResizer") as Border;

if (_bladeResizer != null)
{
_bladeResizer.ManipulationStarted -= BladeResizer_ManipulationStarted;
_bladeResizer.ManipulationStarted += BladeResizer_ManipulationStarted;

_bladeResizer.ManipulationDelta -= BladeResizer_ManipulationDelta;
_bladeResizer.ManipulationDelta += BladeResizer_ManipulationDelta;

_bladeResizer.ManipulationCompleted -= BladeResizer_ManipulationCompleted;
_bladeResizer.ManipulationCompleted += BladeResizer_ManipulationCompleted;

_bladeResizer.PointerEntered -= BladeResizer_PointerEntered;
_bladeResizer.PointerEntered += BladeResizer_PointerEntered;

_bladeResizer.PointerExited -= BladeResizer_PointerExited;
_bladeResizer.PointerExited += BladeResizer_PointerExited;

_bladeResizer.DoubleTapped -= BladeResizer_DoubleTapped;
_bladeResizer.DoubleTapped += BladeResizer_DoubleTapped;
}
}

/// <summary>
/// Creates AutomationPeer (<see cref="UIElement.OnCreateAutomationPeer"/>)
/// </summary>
Expand All @@ -50,5 +86,166 @@
{
IsOpen = false;
}

private void BladeResizer_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
{
_draggingSidebarResizer = true;
_preManipulationSidebarWidth = ActualWidth;
VisualStateManager.GoToState(this, "ResizerPressed", true);
e.Handled = true;
}

private void BladeResizer_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
var newWidth = _preManipulationSidebarWidth + e.Cumulative.Translation.X;
if (newWidth < MINIMUM_WIDTH)
newWidth = MINIMUM_WIDTH;

Width = newWidth;
e.Handled = true;
}

private void BladeResizer_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
_draggingSidebarResizer = false;
VisualStateManager.GoToState(this, "ResizerNormal", true);
e.Handled = true;
}

private void BladeResizer_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
var optimalWidth = CalculateOptimalWidth();
if (optimalWidth > 0)
{
Width = Math.Max(optimalWidth, MINIMUM_WIDTH);
}
else
{
// Fallback to default width if calculation fails
Width = DEFAULT_WIDTH;
}

e.Handled = true;
}

private double CalculateOptimalWidth()
{
try
{
// Look for any ListView within this BladeItem that contains text content
var listView = this.FindDescendant<ListView>();
if (listView?.Items == null || !listView.Items.Any())
return 0;

// Calculate the maximum width needed by measuring text content
var maxTextWidth = MeasureContentWidth(listView);

// Add padding for icon, margins, and other UI elements
// Icon width (32) + margins (24) + padding (24) + chevron/tags (40) = 120
var totalPadding = 120;

return maxTextWidth + totalPadding;
}
catch (Exception)
{
return 0;
}
}

private double MeasureContentWidth(ListView listView)
{
try
{
double maxWidth = 0;

// Find all TextBlocks in the ListView using visual tree walking
var textBlocks = GetTextBlocksFromVisualTree(listView);

if (textBlocks.Any())
{
// Measure each TextBlock and find the widest one
foreach (var textBlock in textBlocks)
{
if (string.IsNullOrEmpty(textBlock.Text))
continue;

// Create a measuring TextBlock with the same properties
var measuringBlock = new TextBlock
{
Text = textBlock.Text,
FontSize = textBlock.FontSize,
FontFamily = textBlock.FontFamily,
FontWeight = textBlock.FontWeight,
FontStyle = textBlock.FontStyle
};

measuringBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
maxWidth = Math.Max(maxWidth, measuringBlock.DesiredSize.Width);
}
}
else
{
// Fallback: estimate based on item count and average text width
var itemCount = listView.Items.Count;
if (itemCount > 0)
{
// Estimate average filename length and multiply by character width
var estimatedCharWidth = 8; // Approximate pixel width per character
var estimatedMaxLength = Math.Min(50, Math.Max(20, itemCount * 2)); // Heuristic
maxWidth = estimatedCharWidth * estimatedMaxLength;
}
}

return maxWidth;
}
catch (Exception)
{
// Fallback calculation
return 200; // Default reasonable width
}
}

private List<TextBlock> GetTextBlocksFromVisualTree(DependencyObject parent)
{
var textBlocks = new List<TextBlock>();

if (parent == null)
return textBlocks;

var childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);

if (child is TextBlock textBlock)
{
textBlocks.Add(textBlock);
}

// Recursively search child elements
textBlocks.AddRange(GetTextBlocksFromVisualTree(child));
}

return textBlocks;
}

private void BladeResizer_PointerEntered(object sender, PointerRoutedEventArgs e)
{
var sidebarResizer = (FrameworkElement)sender;
sidebarResizer.ChangeCursor(InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast));
VisualStateManager.GoToState(this, "ResizerPointerOver", true);
e.Handled = true;
}

private void BladeResizer_PointerExited(object sender, PointerRoutedEventArgs e)
{
if (_draggingSidebarResizer)
return;

var sidebarResizer = (FrameworkElement)sender;
sidebarResizer.ChangeCursor(InputSystemCursor.Create(InputSystemCursorShape.Arrow));
VisualStateManager.GoToState(this, "ResizerNormal", true);
e.Handled = true;
}
}
}
39 changes: 39 additions & 0 deletions src/Files.App.Controls/BladeView/BladeView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}" />

<!-- Resizer -->
<Border
x:Name="BladeResizer"
Width="4"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
AllowFocusOnInteraction="True"
Background="Transparent"
BorderBrush="Transparent"
CornerRadius="2"
ManipulationMode="TranslateX" />

<Button
Name="CloseButton"
HorizontalAlignment="Right"
Expand All @@ -127,6 +139,33 @@
Style="{StaticResource ButtonRevealStyle}"
TabIndex="0"
Visibility="{TemplateBinding CloseButtonVisibility}" />

<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ResizerStates">
<VisualState x:Name="ResizerNormal" />
<VisualState x:Name="ResizerPointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BladeResizer" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource CardStrokeColorDefaultSolidBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BladeResizer" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource CardStrokeColorDefaultSolidBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>

<VisualState x:Name="ResizerPressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BladeResizer" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SolidBackgroundFillColorSecondaryBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BladeResizer" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SolidBackgroundFillColorSecondaryBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
Expand Down
Loading