//
// Copyright 2009, Adam Cataldo
// https://www.codeproject.com/Articles/32629/A-better-panel-for-data-binding-to-a-WrapPanel-in
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the “Software”), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace QtVsTools.Wizards.Common
{
internal class UniformWrapPanel : WrapPanel
{
#region Dependency Properties
///
/// Gets or sets a value indicating whether the child elements should be arranged uniformly
/// in both rows and columns.
///
public static readonly DependencyProperty IsAutoUniformProperty =
DependencyProperty.Register(
nameof(IsAutoUniform),
typeof(bool),
typeof(UniformWrapPanel),
new FrameworkPropertyMetadata(true, OnIsAutoUniformChanged)
);
///
/// Gets or sets a value indicating whether the child elements should be arranged uniformly
/// in both rows and columns.
///
public bool IsAutoUniform
{
get => (bool)GetValue(IsAutoUniformProperty);
set => SetValue(IsAutoUniformProperty, value);
}
private static void OnIsAutoUniformChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
if (sender is UniformWrapPanel panel)
panel.InvalidateVisual();
}
///
/// Gets or sets the desired number of rows in the UniformWrapPanel.
///
public static readonly DependencyProperty RowsProperty =
DependencyProperty.Register(
nameof(Rows),
typeof(int),
typeof(UniformWrapPanel),
new FrameworkPropertyMetadata(0, OnRowsChanged)
);
///
/// Gets or sets the desired number of rows in the UniformWrapPanel.
///
public int Rows
{
get => (int)GetValue(RowsProperty);
set => SetValue(RowsProperty, Math.Max(value, 1));
}
private static void OnRowsChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
if (sender is UniformWrapPanel panel)
panel.InvalidateVisual();
}
///
/// Gets or sets the desired number of columns in the UniformWrapPanel.
///
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register(
nameof(Columns),
typeof(int),
typeof(UniformWrapPanel),
new FrameworkPropertyMetadata(0, OnColumnsChanged)
);
///
/// Gets or sets the desired number of columns in the UniformWrapPanel.
///
public int Columns
{
get => (int)GetValue(ColumnsProperty);
set => SetValue(ColumnsProperty, Math.Max(value, 1));
}
private static void OnColumnsChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
if (sender is UniformWrapPanel panel)
panel.InvalidateVisual();
}
#endregion
///
/// Measures the child elements of the UniformWrapPanel in order to determine their size.
///
/// The available size provided by the layout system.
/// The size that this UniformWrapPanel determines it needs during layout.
protected override Size MeasureOverride(Size availableSize)
{
// If there are no child elements, just use the base implementation
if (Children.Count <= 0)
return base.MeasureOverride(availableSize);
// Convert child elements to a list for easy manipulation
var elements = Children.Cast().ToList();
if (Orientation == Orientation.Horizontal) {
// Measure the desired widths for the elements and calculate the desired width for
// the row
var desiredWidths = new List(Children.Count);
if (IsAutoUniform) {
// If using AutoUniform, find the maximum width of the child elements
ItemWidth = 0.0;
foreach (UIElement element in Children) {
element.Measure(availableSize);
var nextSize = element.DesiredSize;
if (!double.IsInfinity(nextSize.Width) && !double.IsNaN(nextSize.Width))
ItemWidth = Math.Max(nextSize.Width, ItemWidth);
}
// Set the desired width for each element to be the maximum width
desiredWidths.AddRange(Enumerable.Repeat(ItemWidth, Children.Count));
} else {
// If not using AutoUniform, set the desired width for each element based on
// Rows
if (Rows <= 0)
return base.MeasureOverride(availableSize);
desiredWidths.AddRange(elements.Select(element =>
GetDesiredWidth(element, availableSize)));
}
// Calculate the desired width for the entire UniformWrapPanel layout
var desiredWidth = CalculateDesiredRowOrColumnSize(desiredWidths, Rows);
availableSize = new Size(Math.Max(desiredWidth, availableSize.Width),
availableSize.Height);
} else {
// Measure the desired heights for the elements and calculate the desired height
// for the column
var desiredHeights = new List(Children.Count);
if (IsAutoUniform) {
// If using AutoUniform, find the maximum height of the child elements
ItemHeight = 0.0;
foreach (UIElement element in Children) {
element.Measure(availableSize);
var nextSize = element.DesiredSize;
if (!double.IsInfinity(nextSize.Height) && !double.IsNaN(nextSize.Height))
ItemHeight = Math.Max(nextSize.Height, ItemHeight);
}
// Set the desired height for each element to be the maximum height
desiredHeights.AddRange(Enumerable.Repeat(ItemHeight, Children.Count));
} else {
// If not using AutoUniform, set the desired height for each element based on
// Columns
if (Columns <= 0)
return base.MeasureOverride(availableSize);
desiredHeights = elements
.Select(element => GetDesiredHeight(element, availableSize)).ToList();
}
// Calculate the desired height for the entire UniformWrapPanel layout
var desiredHeight = CalculateDesiredRowOrColumnSize(desiredHeights, Columns);
availableSize = new Size(availableSize.Width,
Math.Max(desiredHeight, availableSize.Height));
}
// Call the base implementation of MeasureOverride with the updated available size
return base.MeasureOverride(availableSize);
}
///
/// Calculates the desired size for rows or columns based on the provided list of sizes and
/// the number of rows or columns.
///
/// The list of sizes for rows or columns.
/// The desired number of rows or
/// columns.
/// The calculated desired size for rows or columns.
private double CalculateDesiredRowOrColumnSize(List rowOrColumnSizes,
int targetRowCountOrColumnCount)
{
// Calculate the maximum size based on the provided sizes or default to 0 if no valid
// sizes are found
var maxSize = rowOrColumnSizes.Where(length => !double.IsNaN(length))
.DefaultIfEmpty(0.0).Max();
// If the maximum size is non-positive, return NaN as it indicates an invalid layout
if (maxSize <= 0.0)
return double.NaN;
// If not using AutoUniform, adjust sizes by setting NaN sizes to the maximum size
if (!IsAutoUniform) {
rowOrColumnSizes = rowOrColumnSizes
.Select(length => double.IsNaN(length) ? maxSize : length).ToList();
}
// Calculate the total desired size and the maximum count of rows/columns to consider
var totalDesiredSize = rowOrColumnSizes.Sum();
var maxCount = Math.Min(targetRowCountOrColumnCount, rowOrColumnSizes.Count);
// Calculate the suitable size for rows/columns based on total desired size and maximum
// count
var suitableSize = totalDesiredSize / maxCount;
// Adjust suitableSize to ensure it doesn't exceed the maximum count of rows/columns
while (CalculateRowCountOrColumnCountWithinLimit(rowOrColumnSizes, suitableSize,
out var nextLengthIncrement) > maxCount) {
suitableSize += nextLengthIncrement;
}
// Return the larger of the calculated suitable size and the maximum size in the list
return Math.Max(suitableSize, rowOrColumnSizes.Max());
}
///
/// Calculates the number of rows or columns that can fit within the specified length
/// limit, and calculates the next length increment required for proper layout.
///
/// The list of desired lengths for rows or columns.
/// The maximum length limit for a row or
/// column.
/// The calculated next length increment required for
/// proper layout.
/// The number of rows or columns that can fit within the specified length
/// limit.
private static int CalculateRowCountOrColumnCountWithinLimit(List desiredLengths,
double rowOrColumnLengthLimit, out double nextLengthIncrement)
{
var rowCountOrColumnCount = 1;
var currentCumulativeLength = 0.0;
var minimalIncrement = double.MaxValue;
// Iterate through the desired lengths to calculate the number of rows or columns that
// can fit within the specified length limit and calculate the next length increment
foreach (var desiredLength in desiredLengths) {
if (currentCumulativeLength + desiredLength > rowOrColumnLengthLimit) {
minimalIncrement = Math.Min(minimalIncrement,
currentCumulativeLength + desiredLength - rowOrColumnLengthLimit);
currentCumulativeLength = desiredLength;
rowCountOrColumnCount++;
} else {
currentCumulativeLength += desiredLength;
}
}
// If minimalIncrement is still at its initial value, set it to 1 as the default
// increment
nextLengthIncrement = Math.Abs(minimalIncrement - double.MaxValue) > 0.0
? minimalIncrement : 1;
return rowCountOrColumnCount;
}
///
/// Measures the desired width of the specified UI element within the given available size.
///
/// The UI element to measure.
/// The available size to constrain the measurement.
///
/// The desired width of the UI element. If the width is double.infinity or double.NaN,
/// return double.NaN to indicate an invalid value.
///
private static double GetDesiredWidth(UIElement element, Size availableSize)
{
element.Measure(availableSize);
var length = element.DesiredSize.Width;
if (double.IsInfinity(length) || double.IsNaN(length))
return double.NaN;
return length;
}
///
/// Measures the desired height of the specified UI element within the given available
/// size.
///
/// The UI element to measure.
/// The available size to constrain the measurement.
///
/// The desired height of the UI element. If the height is double.infinity or double.NaN,
/// return double.NaN to indicate an invalid value.
///
private static double GetDesiredHeight(UIElement element, Size availableSize)
{
element.Measure(availableSize);
var length = element.DesiredSize.Height;
if (double.IsInfinity(length) || double.IsNaN(length))
return double.NaN;
return length;
}
}
}