File: src\Framework\System\Windows\Controls\Primitives\ToolBarPanel.cs
Project: wpf\PresentationFramework.csproj (PresentationFramework)
//---------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation.  All rights reserved.
//
//---------------------------------------------------------------------------
 
using MS.Internal;
using MS.Internal.KnownBoxes;
using MS.Internal.Telemetry.PresentationFramework;
using MS.Utility;
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Threading;
 
namespace System.Windows.Controls.Primitives
{
    /// <summary>
    ///     ToolBarPanel is used to arrange children within the ToolBar. It is used in the ToolBar style as items host.
    /// </summary>
    // There is no need for this panel to be a StackPanel.
    public class ToolBarPanel : StackPanel
    {
        static ToolBarPanel()
        {
            ControlsTraceLogger.AddControl(TelemetryControls.ToolBarPanel);
        }
 
        /// <summary>
        ///     Instantiates a new instance of this class.
        /// </summary>
        public ToolBarPanel() : base()
        {
        }
 
        #region Layout
 
        internal double MinLength
        {
            get;
            private set;
        }
 
        internal double MaxLength
        {
            get;
            private set;
        }
 
        private bool MeasureGeneratedItems(bool asNeededPass, Size constraint, bool horizontal, double maxExtent, ref Size panelDesiredSize, out double overflowExtent)
        {
            ToolBarOverflowPanel overflowPanel = ToolBarOverflowPanel;
            bool sendToOverflow = false; // Becomes true when the first AsNeeded does not fit
            bool hasOverflowItems = false;
            bool overflowNeedsInvalidation = false;
            overflowExtent = 0.0;
            UIElementCollection children = InternalChildren;
            int childrenCount = children.Count;
            int childrenIndex = 0;
 
            for (int i = 0; i < _generatedItemsCollection.Count; i++)
            {
                UIElement child = _generatedItemsCollection[i];
                OverflowMode overflowMode = ToolBar.GetOverflowMode(child);
                bool asNeededMode = overflowMode == OverflowMode.AsNeeded;
 
                // MeasureGeneratedItems is called twice to do a complete measure.
                // The first pass measures Always and Never items -- items that
                // are guaranteed to be or not to be in the overflow menu.
                // The second pass measures AsNeeded items and determines whether
                // there is enough room for them in the main bar or if they should
                // be placed in the overflow menu.
                // Check here whether the overflow mode matches a mode we should be
                // examining in this pass.
                if (asNeededMode == asNeededPass)
                {
                    DependencyObject visualParent = VisualTreeHelper.GetParent(child);
 
                    // In non-Always overflow modes, measure for main bar placement.
                    if ((overflowMode != OverflowMode.Always) && !sendToOverflow)
                    {
                        // Children may change their size depending on whether they are in the overflow
                        // menu or not. Ensure that when we measure, we are using the main bar size.
                        // If the item goes to overflow, this property will be updated later in this loop
                        // when it is removed from the visual tree.
                        ToolBar.SetIsOverflowItem(child, BooleanBoxes.FalseBox);
                        child.Measure(constraint);
                        Size childDesiredSize = child.DesiredSize;
 
                        // If the child is an AsNeeded, check if it fits. If it doesn't then
                        // this child and all subsequent AsNeeded children should be sent
                        // to the overflow menu.
                        if (asNeededMode)
                        {
                            double newExtent;
                            if (horizontal)
                            {
                                newExtent = childDesiredSize.Width + panelDesiredSize.Width;
                            }
                            else
                            {
                                newExtent = childDesiredSize.Height + panelDesiredSize.Height;
                            }
                            if (DoubleUtil.GreaterThan(newExtent, maxExtent))
                            {
                                // It doesn't fit, send to overflow
                                sendToOverflow = true;
                            }
                        }
 
                        // The child has been validated as belonging in the main bar.
                        // Update the panel desired size dimensions, and ensure the child
                        // is in the main bar's visual tree.
                        if (!sendToOverflow)
                        {
                            if (horizontal)
                            {
                                panelDesiredSize.Width += childDesiredSize.Width;
                                panelDesiredSize.Height = Math.Max(panelDesiredSize.Height, childDesiredSize.Height);
                            }
                            else
                            {
                                panelDesiredSize.Width = Math.Max(panelDesiredSize.Width, childDesiredSize.Width);
                                panelDesiredSize.Height += childDesiredSize.Height;
                            }
 
                            if (visualParent != this)
                            {
                                if ((visualParent == overflowPanel) && (overflowPanel != null))
                                {
                                    overflowPanel.Children.Remove(child);
                                }
 
                                if (childrenIndex < childrenCount)
                                {
                                    children.InsertInternal(childrenIndex, child);
                                }
                                else
                                {
                                    children.AddInternal(child);
                                }
                                childrenCount++;
                            }
 
                            Debug.Assert(children[childrenIndex] == child, "InternalChildren is out of sync with _generatedItemsCollection.");
                            childrenIndex++;
                        }
                    }
 
                    // The child should go to the overflow menu
                    if ((overflowMode == OverflowMode.Always) || sendToOverflow)
                    {
                        hasOverflowItems = true;
 
                        // If a child is in the overflow menu, we don't want to keep measuring.
                        // However, we need to calculate the MaxLength as well as set the desired height
                        // correctly. Thus, we will use the DesiredSize of the child. There is a problem
                        // that can occur if the child changes size while in the overflow menu and
                        // was recently displayed. It will be measure clean, yet its DesiredSize
                        // will not be accurate for the MaxLength calculation.
                        if (child.MeasureDirty)
                        {
                            // Set this temporarily in case the size is different while in the overflow area
                            ToolBar.SetIsOverflowItem(child, BooleanBoxes.FalseBox);
                            child.Measure(constraint);
                        }
 
                        // Even when in the overflow, we need two pieces of information:
                        // 1. We need to continue to track the maximum size of the non-logical direction
                        //    (i.e. height in horizontal bars). This way, ToolBars with everything in
                        //    the overflow will still have some height.
                        // 2. We want to track how much of the space we saved by placing the child in
                        //    the overflow menu. This is used to calculate MinLength and MaxLength.
                        Size childDesiredSize = child.DesiredSize;
                        if (horizontal)
                        {
                            overflowExtent += childDesiredSize.Width;
                            panelDesiredSize.Height = Math.Max(panelDesiredSize.Height, childDesiredSize.Height);
                        }
                        else
                        {
                            overflowExtent += childDesiredSize.Height;
                            panelDesiredSize.Width = Math.Max(panelDesiredSize.Width, childDesiredSize.Width);
                        }
 
                        // Set the flag to indicate that the child is in the overflow menu
                        ToolBar.SetIsOverflowItem(child, BooleanBoxes.TrueBox);
 
                        // If the child is in this panel's visual tree, remove it.
                        if (visualParent == this)
                        {
                            Debug.Assert(children[childrenIndex] == child, "InternalChildren is out of sync with _generatedItemsCollection.");
                            children.RemoveNoVerify(child);
                            childrenCount--;
                            overflowNeedsInvalidation = true;
                        }
                        // If the child isnt connected to the visual tree, notify the overflow panel to pick it up.
                        else if (visualParent == null)
                        {
                            overflowNeedsInvalidation = true;
                        }
                    }
                }
                else
                {
                    // We are not measure this child in this pass. Update the index into the
                    // visual children collection.
                    if ((childrenIndex < childrenCount) && (children[childrenIndex] == child))
                    {
                        childrenIndex++;
                    }
                }
            }
 
            // A child was added to the overflow panel, but since we don't add it
            // to the overflow panel's visual collection until that panel's measure
            // pass, we need to mark it as measure dirty.
            if (overflowNeedsInvalidation && (overflowPanel != null))
            {
                overflowPanel.InvalidateMeasure();
            }
 
            return hasOverflowItems;
        }
 
        /// <summary>
        /// Measure the content and store the desired size of the content
        /// </summary>
        /// <param name="constraint"></param>
        /// <returns></returns>
        protected override Size MeasureOverride(Size constraint)
        {
            Size stackDesiredSize = new Size();
 
            if (IsItemsHost)
            {
                Size layoutSlotSize = constraint;
                double maxExtent;
                double overflowExtent;
                bool horizontal = (Orientation == Orientation.Horizontal);
 
                if (horizontal)
                {
                    layoutSlotSize.Width = Double.PositiveInfinity;
                    maxExtent = constraint.Width;
                }
                else
                {
                    layoutSlotSize.Height = Double.PositiveInfinity;
                    maxExtent = constraint.Height;
                }
 
                // This first call will measure all of the non-AsNeeded elements (i.e. we know
                // whether they're going into the overflow or not.
                // overflowExtent will be the size of the Always elements, which is not actually
                // needed for subsequent calculations.
                bool hasAlwaysOverflowItems = MeasureGeneratedItems(/* asNeeded = */ false, layoutSlotSize, horizontal, maxExtent, ref stackDesiredSize, out overflowExtent);
 
                // At this point, the desired size is the minimum size of the ToolBar.
                MinLength = horizontal ? stackDesiredSize.Width : stackDesiredSize.Height;
 
                // This second call will measure all of the AsNeeded elements and place
                // them in the appropriate location.
                bool hasAsNeededOverflowItems = MeasureGeneratedItems(/* asNeeded = */ true, layoutSlotSize, horizontal, maxExtent, ref stackDesiredSize, out overflowExtent);
 
                // At this point, the desired size is complete. The desired size plus overflowExtent
                // is the maximum size of the ToolBar.
                MaxLength = (horizontal ? stackDesiredSize.Width : stackDesiredSize.Height) + overflowExtent;
 
                ToolBar toolbar = ToolBar;
                if (toolbar != null)
                {
                    toolbar.SetValue(ToolBar.HasOverflowItemsPropertyKey, hasAlwaysOverflowItems || hasAsNeededOverflowItems);
                }
            }
            else
            {
                stackDesiredSize = base.MeasureOverride(constraint);
            }
 
            return stackDesiredSize;
        }
 
        /// <summary>
        /// Content arrangement.
        /// </summary>
        /// <param name="arrangeSize">Arrange size</param>
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            UIElementCollection children = InternalChildren;
            bool fHorizontal = (Orientation == Orientation.Horizontal);
            Rect rcChild = new Rect(arrangeSize);
            double previousChildSize = 0.0d;
 
            //
            // Arrange and Position Children.
            //
            for (int i = 0, count = children.Count; i < count; ++i)
            {
                UIElement child = (UIElement)children[i];
 
                if (fHorizontal)
                {
                    rcChild.X += previousChildSize;
                    previousChildSize = child.DesiredSize.Width;
                    rcChild.Width = previousChildSize;
                    rcChild.Height = Math.Max(arrangeSize.Height, child.DesiredSize.Height);
                }
                else
                {
                    rcChild.Y += previousChildSize;
                    previousChildSize = child.DesiredSize.Height;
                    rcChild.Height = previousChildSize;
                    rcChild.Width = Math.Max(arrangeSize.Width, child.DesiredSize.Width);
                }
 
                child.Arrange(rcChild);
            }
 
            return arrangeSize;
        }
 
        /// <summary>
        /// ToolBarPanel sets bindings a on its Orientation property to its TemplatedParent if the
        /// property is not already set.
        /// </summary>
        internal override void OnPreApplyTemplate()
        {
            base.OnPreApplyTemplate();
 
            if (TemplatedParent is ToolBar && !HasNonDefaultValue(OrientationProperty))
            {
                Binding binding = new Binding();
                binding.RelativeSource = RelativeSource.TemplatedParent;
                binding.Path = new PropertyPath(ToolBar.OrientationProperty);
                SetBinding(OrientationProperty, binding);
            }
        }
 
        #endregion
 
        #region Item Generation
 
        internal override void GenerateChildren()
        {
            base.GenerateChildren();
 
            // This could re-enter InternalChildren, but after base.GenerateChildren, the collection
            // should be fully instantiated and ready to go.
            UIElementCollection children = InternalChildren;
            if (_generatedItemsCollection == null)
            {
                _generatedItemsCollection = new List<UIElement>(children.Count);
            }
            else
            {
                _generatedItemsCollection.Clear();
            }
 
            ToolBarOverflowPanel overflowPanel = ToolBarOverflowPanel;
            if (overflowPanel != null)
            {
                overflowPanel.Children.Clear();
                overflowPanel.InvalidateMeasure();
            }
 
            int childrenCount = children.Count;
            for (int i = 0; i < childrenCount; i++)
            {
                UIElement child = children[i];
 
                // Reset the overflow decision. This will be re-evaluated on the next measure
                // by ToolBarPanel.MeasureOverride.
                ToolBar.SetIsOverflowItem(child, BooleanBoxes.FalseBox);
 
                _generatedItemsCollection.Add(child);
            }
        }
 
        // This method returns a bool to indicate if or not the panel layout is affected by this collection change
        internal override bool OnItemsChangedInternal(object sender, ItemsChangedEventArgs args)
        {
            switch (args.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    AddChildren(args.Position, args.ItemCount);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    RemoveChildren(args.Position, args.ItemUICount);
                    break;
                case NotifyCollectionChangedAction.Replace:
                    ReplaceChildren(args.Position, args.ItemCount, args.ItemUICount);
                    break;
                case NotifyCollectionChangedAction.Move:
                    MoveChildren(args.OldPosition, args.Position, args.ItemUICount);
                    break;
 
                case NotifyCollectionChangedAction.Reset:
                    base.OnItemsChangedInternal(sender, args);
                    break;
            }
 
            return true;
        }
 
        private void AddChildren(GeneratorPosition pos, int itemCount)
        {
            IItemContainerGenerator generator = (IItemContainerGenerator)Generator;
            using (generator.StartAt(pos, GeneratorDirection.Forward))
            {
                for (int i = 0; i < itemCount; i++)
                {
                    UIElement e = generator.GenerateNext() as UIElement;
                    if (e != null)
                    {
                        _generatedItemsCollection.Insert(pos.Index + 1 + i, e);
                        generator.PrepareItemContainer(e);
                    }
                    else
                    {
                        ItemContainerGenerator icg = Generator as ItemContainerGenerator;
                        if (icg != null)
                        {
                            icg.Verify();
                        }
                    }
                }
            }
        }
 
        private void RemoveChild(UIElement child)
        {
            DependencyObject visualParent = VisualTreeHelper.GetParent(child);
 
            if (visualParent == this)
            {
                InternalChildren.RemoveInternal(child);
            }
            else
            {
                ToolBarOverflowPanel overflowPanel = ToolBarOverflowPanel;
                if ((visualParent == overflowPanel) && (overflowPanel != null))
                {
                    overflowPanel.Children.Remove(child);
                }
            }
        }
 
        private void RemoveChildren(GeneratorPosition pos, int containerCount)
        {
            for (int i = 0; i < containerCount; i++)
            {
                RemoveChild(_generatedItemsCollection[pos.Index + i]);
            }
 
            _generatedItemsCollection.RemoveRange(pos.Index, containerCount);
        }
 
        private void ReplaceChildren(GeneratorPosition pos, int itemCount, int containerCount)
        {
            Debug.Assert(itemCount == containerCount, "ToolBarPanel expects Replace to affect only realized containers");
 
            IItemContainerGenerator generator = (IItemContainerGenerator)Generator;
            using (generator.StartAt(pos, GeneratorDirection.Forward, true))
            {
                for (int i = 0; i < itemCount; i++)
                {
                    bool isNewlyRealized;
                    UIElement e = generator.GenerateNext(out isNewlyRealized) as UIElement;
 
                    Debug.Assert(e != null && !isNewlyRealized, "ToolBarPanel expects Replace to affect only realized containers");
                    if (e != null && !isNewlyRealized)
                    {
                        RemoveChild(_generatedItemsCollection[pos.Index + i]);
                        _generatedItemsCollection[pos.Index + i] = e;
                        generator.PrepareItemContainer(e);
                    }
                    else
                    {
                        ItemContainerGenerator icg = Generator as ItemContainerGenerator;
                        if (icg != null)
                        {
                            icg.Verify();
                        }
                    }
                }
            }
        }
 
        private void MoveChildren(GeneratorPosition fromPos, GeneratorPosition toPos, int containerCount)
        {
            if (fromPos == toPos)
                return;
 
            IItemContainerGenerator generator = (IItemContainerGenerator)Generator;
            int toIndex = generator.IndexFromGeneratorPosition(toPos);
 
            UIElement[] elements = new UIElement[containerCount];
 
            for (int i = 0; i < containerCount; i++)
            {
                UIElement child = _generatedItemsCollection[fromPos.Index + i];
                RemoveChild(child);
                elements[i] = child;
            }
 
            _generatedItemsCollection.RemoveRange(fromPos.Index, containerCount);
 
            for (int i = 0; i < containerCount; i++)
            {
                _generatedItemsCollection.Insert(toIndex + i, elements[i]);
            }
        }
 
        #endregion
 
        #region Helpers
 
        private ToolBar ToolBar
        {
            get { return TemplatedParent as ToolBar; }
        }
 
        private ToolBarOverflowPanel ToolBarOverflowPanel
        {
            get
            {
                ToolBar tb = ToolBar;
                return tb == null ? null : tb.ToolBarOverflowPanel;
            }
        }
 
        // Accessed by ToolBarOverflowPanel
        internal List<UIElement> GeneratedItemsCollection
        {
            get { return _generatedItemsCollection; }
        }
 
        #endregion
 
        #region Data
 
        private List<UIElement> _generatedItemsCollection;
 
        #endregion
 
    }
}