diff --git a/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs b/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs index 7a4af7cd..b6805938 100644 --- a/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs +++ b/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs @@ -45,7 +45,13 @@ public abstract class LayoutFloatingWindowControl : Window, ILayoutControl private DragService _dragService = null; private bool _internalCloseFlag = false; private bool _isClosing = false; - #endregion fields + + /// + /// Is false until the margins have been found once. + /// + /// + private bool _isTotalMarginSet = false; + #endregion fields #region Constructors @@ -181,6 +187,90 @@ protected override void OnStateChanged(EventArgs e) #endregion IsMaximized + #region TotalMargin + + private static readonly DependencyPropertyKey TotalMarginPropertyKey = + DependencyProperty.RegisterReadOnly(nameof(TotalMargin), + typeof(Thickness), + typeof(LayoutFloatingWindowControl), + new FrameworkPropertyMetadata(default(Thickness))); + + public static readonly DependencyProperty TotalMarginProperty = TotalMarginPropertyKey.DependencyProperty; + + /// + /// The total margin (including window chrome and title bar). + /// + /// The margin is queried from the visual tree the first time it is rendered, zero until the first call of FilterMessage(WM_ACTIVATE) + /// + public Thickness TotalMargin + { + get { return (Thickness) GetValue(TotalMarginProperty); } + protected set { SetValue(TotalMarginPropertyKey, value); } + } + + #endregion + + #region ContentMinHeight + public static readonly DependencyPropertyKey ContentMinHeightPropertyKey = DependencyProperty.RegisterReadOnly( + nameof(ContentMinHeight), typeof(double), typeof(LayoutFloatingWindowControl), new FrameworkPropertyMetadata(0.0)); + + public static readonly DependencyProperty ContentMinHeightProperty = + ContentMinHeightPropertyKey.DependencyProperty; + + /// + /// The MinHeight of the content of the window, will be 0 until the window has been rendered, or if the MinHeight is unset for the content + /// + public double ContentMinHeight + { + get { return (double) GetValue(ContentMinHeightProperty); } + set { SetValue(ContentMinHeightPropertyKey, value); } + } + #endregion + + #region ContentMinWidth + public static readonly DependencyPropertyKey ContentMinWidthPropertyKey = DependencyProperty.RegisterReadOnly( + nameof(ContentMinWidth), typeof(double), typeof(LayoutFloatingWindowControl), new FrameworkPropertyMetadata(0.0)); + + public static readonly DependencyProperty ContentMinWidthProperty = + ContentMinWidthPropertyKey.DependencyProperty; + + /// + /// The MinWidth ocf the content of the window, will be 0 until the window has been rendered, or if the MinWidth is unset for the content + /// + public double ContentMinWidth + { + get { return (double) GetValue(ContentMinWidthProperty); } + set { SetValue(ContentMinWidthPropertyKey, value); } + } + #endregion + + #region SetWindowSizeWhenOpened + + public static readonly DependencyProperty SetWindowSizeWhenOpenedProperty = DependencyProperty.Register( + nameof(SetWindowSizeWhenOpened), typeof(bool), typeof(LayoutFloatingWindowControl), new PropertyMetadata(false, + (sender, args) => + { + if (args.OldValue != args.NewValue && + sender is LayoutFloatingWindowControl control) + { + // This will resize the window when this property is set to true and the window is open + control._isTotalMarginSet = false; + } + })); + + /// + /// If true the MinHeight and MinWidth of the content will be used together with the margins to determine the initial size of the floating window + /// + /// + /// + /// + public bool SetWindowSizeWhenOpened + { + get { return (bool) GetValue(SetWindowSizeWhenOpenedProperty); } + set { SetValue(SetWindowSizeWhenOpenedProperty, value); } + } + + #endregion #endregion Properties #region Internal Methods @@ -248,7 +338,9 @@ protected virtual IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntP handled = true; } } - break; + UpdateWindowsSizeBasedOnMinSize(); + + break; case Win32Helper.WM_EXITSIZEMOVE: UpdatePositionAndSizeOfPanes(); @@ -283,7 +375,102 @@ protected virtual IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntP return IntPtr.Zero; } - internal void InternalClose(bool closeInitiatedByUser = false) + /// + /// Set the margins of the window control (including the borders of the floating window and the title bar). + /// The result will be stored in _totalMargin. + /// + /// If the control is not loaded _totalMargin will not be set. + private void UpdateMargins() + { + // The grid with window bar and content + var grid = this.GetChildrenRecursive() + .OfType() + .FirstOrDefault(g => g.RowDefinitions.Count > 0); + ContentPresenter contentControl = this.GetChildrenRecursive() + .OfType() + .FirstOrDefault(c => c.Content is LayoutContent); + if (contentControl == null) + return; + // The content control in the grid, this has a different tree to walk up + var layoutContent = (LayoutContent)contentControl.Content; + if (grid != null && layoutContent.Content is FrameworkElement content) + { + var parents = content.GetParents().ToArray(); + var children = this.GetChildrenRecursive() + .TakeWhile(c => c != grid) + .ToArray(); + var borders = children + .OfType() + .Concat(parents + .OfType()) + .ToArray(); + var controls = children + .OfType() + .Concat(parents + .OfType()) + .ToArray(); + var frameworkElements = children + .OfType() + .Concat(parents + .OfType()) + .ToArray(); + var padding = controls.Sum(b => b.Padding); + var border = borders.Sum(b => b.BorderThickness); + var margin = frameworkElements.Sum(f => f.Margin); + margin = margin.Add(padding).Add(border).Add(grid.Margin); + margin.Top = grid.RowDefinitions[0].MinHeight; + TotalMargin = margin; + _isTotalMarginSet = true; + } + } + + /// + /// Update the floating window size based on the MinHeight and MinWidth of the content of the control. + /// + /// This will only be run once, when the window is rendered the first time and _totalMargin is identified. + private void UpdateWindowsSizeBasedOnMinSize() + { + if (!_isTotalMarginSet) + { + UpdateMargins(); + if(_isTotalMarginSet) + { + // The LayoutAnchorableControl is bound via the ContentPresenter, hence it is best to do below in code and not in a style + // See https://github.com/Dirkster99/AvalonDock/pull/146#issuecomment-609974424 + var layoutContents = this.GetChildrenRecursive() + .OfType() + .Select(c => c.Content) + .OfType() + .Select(lc => lc.Content); + var contents = layoutContents.OfType(); + foreach (var content in contents) + { + ContentMinHeight = Math.Max(content.MinHeight, ContentMinHeight); + ContentMinWidth = Math.Max(content.MinWidth, ContentMinWidth); + if (SetWindowSizeWhenOpened) + { + var parent = content.GetParents() + .OfType() + .FirstOrDefault(); + // StackPanels among others have an ActualHeight larger than visible, hence we check the parent control as well + if (content.ActualHeight < content.MinHeight || + parent != null && parent.ActualHeight < content.MinHeight) + { + Height = content.MinHeight + TotalMargin.Top + TotalMargin.Bottom; + } + + if (content.ActualWidth < content.MinWidth || + parent != null && parent.ActualWidth < content.MinWidth) + { + Width = content.MinWidth + TotalMargin.Left + TotalMargin.Right; + } + } + } + } + } + } + + internal void InternalClose(bool closeInitiatedByUser = false) { _internalCloseFlag = !closeInitiatedByUser; if (_isClosing) return; diff --git a/source/Components/AvalonDock/Extensions.cs b/source/Components/AvalonDock/Extensions.cs index a861f6d9..b45fe5b5 100644 --- a/source/Components/AvalonDock/Extensions.cs +++ b/source/Components/AvalonDock/Extensions.cs @@ -8,37 +8,124 @@ This program is provided to you under the terms of the Microsoft Public ************************************************************************/ using System; -using System.Collections.Generic; using System.Collections; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media; namespace AvalonDock { - internal static class Extensions - { - public static bool Contains(this IEnumerable collection, object item) - { - foreach (var o in collection) - if (o == item) return true; - return false; - } - - - public static void ForEach(this IEnumerable collection, Action action) - { - foreach (var v in collection) action(v); - } - - - public static int IndexOf(this T[] array, T value) where T : class - { - for (var i = 0; i < array.Length; i++) - if (array[i] == value) return i; - return -1; - } - - public static V GetValueOrDefault(this WeakReference wr) - { - return wr == null || !wr.IsAlive ? default : (V) wr.Target; - } - } + internal static class Extensions + { + public static bool Contains(this IEnumerable collection, object item) + { + foreach (var o in collection) + if (o == item) return true; + return false; + } + + + public static void ForEach(this IEnumerable collection, Action action) + { + foreach (var v in collection) action(v); + } + + + public static int IndexOf(this T[] array, T value) where T : class + { + for (var i = 0; i < array.Length; i++) + if (array[i] == value) return i; + return -1; + } + + public static V GetValueOrDefault(this WeakReference wr) + { + return wr == null || !wr.IsAlive ? default : (V)wr.Target; + } + + /// + /// Recursively get the visual tree children of a dependency object. + /// + /// This function is recursive and will return all children. + /// The object to find the children of + /// An enumerable with all the children of the dependency object + public static IEnumerable GetChildrenRecursive(this DependencyObject dependencyObject) + { + var children = dependencyObject.GetChildren(); + foreach (var child in children) + { + yield return child; + foreach (var c in GetChildrenRecursive(child)) + { + yield return c; + } + } + } + + /// + /// Get the visual tree children of a dependency object. + /// + /// This function is not recursive and will only return the first level of children. + /// The object to find the children of + /// An enumerable with all the first level children of the dependency object + public static IEnumerable GetChildren(this DependencyObject dependencyObject) + { + int n = VisualTreeHelper.GetChildrenCount(dependencyObject); + for (int i = 0; i < n; i++) + { + yield return VisualTreeHelper.GetChild(dependencyObject, i); + } + } + + /// + /// Get the visual tree parents of a dependency object, + /// + /// The object to find the parents of + /// An enumerable with the parents of the dependencyObject, the first parent returned will be + /// the direct parent of dependencyObject + public static IEnumerable GetParents(this DependencyObject dependencyObject) + { + while (dependencyObject != null) + { + dependencyObject = VisualTreeHelper.GetParent(dependencyObject); + if (dependencyObject != null) + yield return dependencyObject; + } + } + + /// + /// Calculate the sum multiple thicknesses in an enumerable + /// + /// The type in the enumerable + /// The enumerable with thicknesses + /// A function returning the thickness from T + /// The total thickness + public static Thickness Sum(this IEnumerable enumerable, Func func) + { + double top = 0, bottom = 0, left = 0, right = 0; + foreach (var e in enumerable) + { + var t = func(e); + left = t.Left; + top += t.Top; + right = t.Right; + bottom += t.Bottom; + } + return new Thickness(left, top, right, bottom); + } + + /// + /// Add two thicknesses, each individual component will be added independently of the others. + /// + /// The first thickness + /// The second thickness + /// The total thickness + public static Thickness Add(this Thickness thickness, Thickness other) + { + return new Thickness(thickness.Left + other.Left, + thickness.Top + other.Top, + thickness.Right + other.Right, + thickness.Bottom + other.Bottom); + } + } } diff --git a/source/TestApp/MainWindow.xaml b/source/TestApp/MainWindow.xaml index ac4c440c..1df19a59 100644 --- a/source/TestApp/MainWindow.xaml +++ b/source/TestApp/MainWindow.xaml @@ -54,6 +54,7 @@ + @@ -108,7 +109,7 @@ Title="Tool Window 1" ContentId="toolWindow1" Hiding="OnToolWindow1Hiding"> - + diff --git a/source/TestApp/MainWindow.xaml.cs b/source/TestApp/MainWindow.xaml.cs index a8c559cf..dc8e1b21 100644 --- a/source/TestApp/MainWindow.xaml.cs +++ b/source/TestApp/MainWindow.xaml.cs @@ -236,5 +236,17 @@ private void OnShowHeader(object sender, RoutedEventArgs e) { //// LayoutDocumentPane.ShowHeader = !LayoutDocumentPane.ShowHeader; } - } + + private void OnNewFloatingWindow(object sender, RoutedEventArgs e) + { + var view = new TestUserControl(); + var anchorable = new LayoutAnchorable() + { + Title = "New floating window", + Content = view + }; + anchorable.AddToLayout(dockManager,AnchorableShowStrategy.Most); + anchorable.Float(); + } + } } diff --git a/source/TestApp/TestUserControl.xaml b/source/TestApp/TestUserControl.xaml index b164e968..cd1fe972 100644 --- a/source/TestApp/TestUserControl.xaml +++ b/source/TestApp/TestUserControl.xaml @@ -13,8 +13,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" - Height="300" Width="300"> + MinHeight="300" MinWidth="300"> - + + A label: + Some input +