File: Microsoft\Windows\Controls\Ribbon\Primitives\RibbonGalleryCategoriesPanel.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\System.Windows.Controls.Ribbon\System.Windows.Controls.Ribbon_dxtfdo3u_wpftmp.csproj (System.Windows.Controls.Ribbon)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
 
#region Using declarations
 
using System.ComponentModel;
using System.Windows.Controls.Primitives;
using System.Diagnostics;
using System.Windows.Media;
#if RIBBON_IN_FRAMEWORK
using Microsoft.Windows.Controls;
#else
    using Microsoft.Windows.Controls.Ribbon;
#endif
using MS.Internal;
 
#if RIBBON_IN_FRAMEWORK
namespace System.Windows.Controls.Ribbon.Primitives
#else
namespace Microsoft.Windows.Controls.Ribbon.Primitives
#endif
{
    #endregion
 
    public class RibbonGalleryCategoriesPanel : Panel, IProvideStarLayoutInfoBase, IContainsStarLayoutManager, IScrollInfo
    {
        #region Constructors
 
        public RibbonGalleryCategoriesPanel()
        {
            Unloaded += new RoutedEventHandler(OnRibbonGalleryCategoriesPanelUnloaded);
            Loaded += new RoutedEventHandler(OnRibbonGalleryCategoriesPanelLoaded);
        }
 
        #endregion
 
        #region Private Methods and Properties
 
        private void OnRibbonGalleryCategoriesPanelUnloaded(object sender, RoutedEventArgs e)
        {
            IContainsStarLayoutManager iContainsStarLayoutManager = (IContainsStarLayoutManager)this;
            if (iContainsStarLayoutManager.StarLayoutManager != null)
            {
                iContainsStarLayoutManager.StarLayoutManager.UnregisterStarLayoutProvider(this);
                iContainsStarLayoutManager.StarLayoutManager = null;
            }
        }
 
        private void OnRibbonGalleryCategoriesPanelLoaded(object sender, RoutedEventArgs e)
        {
            RibbonGallery gallery = this.Gallery;
            if (gallery != null)
            {
#if IN_RIBBON_GALLERY
                if (gallery.IsInInRibbonGalleryMode())
                {
                    return;
                }
#endif
                RibbonHelper.InitializeStarLayoutManager(this);
            }
        }
 
#if IN_RIBBON_GALLERY
        // PreComputing MaxItemHeight and MaxItemWidth as the Measure algorithms of parent panels of Items will need it already
        // If it is in InRibbonGalleryMode.
        private void PreComputeMaxRibbonGalleryItemWidthAndHeight()
        {
            UIElementCollection children = InternalChildren;
            Size childConstraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
            double maxItemHeight = 0;
            int childrenCount = children.Count;
            double maxColumnWidth = 0;
 
            for (int i = 0; i < childrenCount; i++)
            {
                RibbonGalleryCategory child = children[i] as RibbonGalleryCategory;
                RibbonGalleryItemsPanel itemPanel = child.ItemsHostSite as RibbonGalleryItemsPanel;
                if (itemPanel != null)
                {
                    int itemPanelChildrenCount = itemPanel.Children.Count;
                    for (int j = 0; j < itemPanelChildrenCount; j++)
                    {
                        RibbonGalleryItem item = (RibbonGalleryItem)itemPanel.Children[j];
                        item.Measure(childConstraint);
                        Size itemSize = item.DesiredSize;
                        maxColumnWidth = Math.Max(maxColumnWidth, itemSize.Width);
                        maxItemHeight = Math.Max(maxItemHeight, itemSize.Height);
                    }
                }
            }
 
            RibbonGallery gallery = this.Gallery;
            if (gallery != null)
            {
                gallery.MaxItemHeight = maxItemHeight;
                gallery.MaxColumnWidth = maxColumnWidth;
            }
        }
 
        internal bool InRibbonModeCanLineUp()
        {
            return !DoubleUtil.AreClose(VerticalOffset, 0.0);
        }
 
        internal bool InRibbonModeCanLineDown()
        {
            return DoubleUtil.GreaterThan(_scrollData._extent.Height, VerticalOffset + ViewportHeight);
        }
#endif
 
        #region Scrolling Helper
 
        private void EnsureScrollData()
        {
            if (_scrollData == null) { _scrollData = new ScrollData(); }
        }
 
        private static void ResetScrolling(RibbonGalleryCategoriesPanel element)
        {
            element.InvalidateMeasure();
 
            // Clear scrolling data.  Because of thrash (being disconnected & reconnected, &c...), we may
            if (element.IsScrolling)
            {
                element._scrollData.ClearLayout();
            }
        }
 
        // OnScrollChange is an override called whenever the IScrollInfo exposed scrolling state changes on this element.
        // At the time this method is called, scrolling state is in its new, valid state.
        private void OnScrollChange()
        {
            if (ScrollOwner != null) { ScrollOwner.InvalidateScrollInfo(); }
        }
 
        private void VerifyScrollingData(Size viewport, Size extent, Vector offset)
        {
            bool fValid = true;
 
            Debug.Assert(IsScrolling);
 
            fValid &= DoubleUtil.AreClose(viewport, _scrollData._viewport);
            fValid &= DoubleUtil.AreClose(extent, _scrollData._extent);
            fValid &= DoubleUtil.AreClose(offset, _scrollData._offset);
            _scrollData._offset = offset;
 
            if (!fValid)
            {
                _scrollData._viewport = viewport;
                _scrollData._extent = extent;
            }
            OnScrollChange();
        }
 
        static private double ComputeScrollOffsetWithMinimalScroll(
            double topView,
            double bottomView,
            double topChild,
            double bottomChild)
        {
            // # CHILD POSITION       CHILD SIZE      SCROLL      REMEDY
            // 1 Above viewport       <= viewport     Down        Align top edge of child & viewport
            // 2 Above viewport       > viewport      Down        Align bottom edge of child & viewport
            // 3 Below viewport       <= viewport     Up          Align bottom edge of child & viewport
            // 4 Below viewport       > viewport      Up          Align top edge of child & viewport
            // 5 Entirely within viewport             NA          No scroll.
            // 6 Spanning viewport                    NA          No scroll.
            //
            // Note: "Above viewport" = childTop above viewportTop, childBottom above viewportBottom
            //       "Below viewport" = childTop below viewportTop, childBottom below viewportBottom
            // These child thus may overlap with the viewport, but will scroll the same direction/
 
            bool fAbove = DoubleUtil.LessThan(topChild, topView) && DoubleUtil.LessThan(bottomChild, bottomView);
            bool fBelow = DoubleUtil.GreaterThan(bottomChild, bottomView) && DoubleUtil.GreaterThan(topChild, topView);
            bool fLarger = (bottomChild - topChild) > (bottomView - topView);
 
            // Handle Cases:  1 & 4 above
            if ((fAbove && !fLarger)
               || (fBelow && fLarger))
            {
                return topChild;
            }
 
            // Handle Cases: 2 & 3 above
            else if (fAbove || fBelow)
            {
                return (bottomChild - (bottomView - topView));
            }
 
            // Handle cases: 5 & 6 above.
            return topView;
        }
 
        // Returns an offset coerced into the [0, Extent - Viewport] range.
        static private double CoerceOffset(double offset, double extent, double viewport)
        {
            if (offset > extent - viewport) { offset = extent - viewport; }
            if (offset < 0) { offset = 0; }
            return offset;
        }
 
        private bool IsScrolling
        {
            get { return (_scrollData != null) && (_scrollData._scrollOwner != null); }
        }
 
        private bool CanMouseWheelVerticallyScroll
        {
            get { return (SystemParameters.WheelScrollLines > 0); }
        }
 
        #endregion Scrolling Helper
 
        #endregion
 
        #region Protected Methods
 
        /// <summary>
        /// In normal(Star) pass this panel behaves like a StackPanel but during Auto(non Star) pass
        /// It returns minimum Width and Height required to represent the children. There is another
        /// mode wherein it provides laying out mechanism for InRibbonGallery in INRibbon mode.
        /// </summary>
        /// <param name="availableSize"></param>
        /// <returns></returns>
        protected override Size MeasureOverride(Size availableSize)
        {
            RibbonGallery gallery = this.Gallery;
#if IN_RIBBON_GALLERY
            InRibbonGallery parentInRibbonGallery = gallery != null ? gallery.ParentInRibbonGallery : null;
            bool isInInRibbonMode = parentInRibbonGallery != null ? parentInRibbonGallery.IsInInRibbonMode : false;
 
            // For an InRibbonGallery rendering with IsDropDownOpen==true, we force gallery.ItemsPresenter's
            // MinWidth to be at least the value of IRG.ContentPresenter.ActualWidth.  This way, the IRG's popup
            // totally eclipses the IRG, which is required by the Office Fluent UI guidelines.
            if (gallery != null &&
                gallery.ItemsPresenter != null &&
                parentInRibbonGallery != null)
            {
                if (isInInRibbonMode && _irgIsConstrainingWidth)
                {
                    gallery.ItemsPresenter.MinWidth = _originalGalleryItemsPresenterMinWidth;
                    _irgIsConstrainingWidth = false;
                }
                else if (parentInRibbonGallery.IsDropDownOpen && !_irgIsConstrainingWidth)
                {
                    _originalGalleryItemsPresenterMinWidth = gallery.ItemsPresenter.MinWidth;
                    double minWidthFromParent = parentInRibbonGallery.CalculateGalleryItemsPresenterMinWidth();
                    gallery.ItemsPresenter.MinWidth = Math.Max(minWidthFromParent, _originalGalleryItemsPresenterMinWidth);
                    _irgIsConstrainingWidth = true;
                }
            }
 
            if (!isInInRibbonMode)
            {
#endif
                RibbonHelper.InitializeStarLayoutManager(this);
#if IN_RIBBON_GALLERY
            }
#endif
 
            IContainsStarLayoutManager iContainsStarLayoutManager = (IContainsStarLayoutManager)this;
            bool isStarLayoutPass = (iContainsStarLayoutManager.StarLayoutManager == null ? true : iContainsStarLayoutManager.StarLayoutManager.IsStarLayoutPass);
 
#if IN_RIBBON_GALLERY
            if (isInInRibbonMode)
            {
                PreComputeMaxRibbonGalleryItemWidthAndHeight();
                return InRibbonGalleryModeMeasureOverride(availableSize);
            }
            else
            {
#endif
                if (isStarLayoutPass)
                {
                    return RealMeasureOverride(availableSize);
                }
                else
                {
                    return AutoPassMeasureOverride();
                }
#if IN_RIBBON_GALLERY
            }
#endif
        }
 
#if IN_RIBBON_GALLERY
        // InRibbonGalleryMode where the gallery is shown within Ribbon via InRibbonGallery and Measure becomes responsibility
        // of InRibbonGalleryModeMeasureOverride.
        private Size InRibbonGalleryModeMeasureOverride(Size availableSize)
        {
            Size desiredSize = new Size();
            Size childConstraint = availableSize;
            childConstraint.Height = Double.PositiveInfinity;
            UIElementCollection children = InternalChildren;
            int childrenCount = children.Count;
            int galleryItemCount = 0;
            double galleryItemCumulativeHeight = 0.0;
            double maxChildWidth = 0.0;
            double maxChildHeight = 0.0;
            int rowOffset = 0;
            int colOffset = 0;
 
            // Measure the child and also sets start and end offsets of the categories to be used their ItemsPanel
            // (RibbonGalleryItemsPanel) in their Measure and Arrange algorithms when in InRibbonMode.
            for (int i = 0; i < childrenCount; i++)
            {
                RibbonGalleryCategory child = children[i] as RibbonGalleryCategory;
 
                if (child == null ||
                    child.Visibility == Visibility.Collapsed)
                {
                    continue;
                }
 
                child.RowOffset = rowOffset;
                child.ColumnOffset = colOffset;
                child.Measure(childConstraint);
                Size childSize = child.DesiredSize;
                maxChildWidth = Math.Max(maxChildWidth, childSize.Width);
                maxChildHeight = Math.Max(maxChildHeight, childSize.Height);
                galleryItemCount += child.averageItemHeightInfo.Count;
                galleryItemCumulativeHeight += child.averageItemHeightInfo.CumulativeHeight;
 
                int childFullRows = Math.Max(0, child.RowCount - 1);
                if (child.RowCount > 0 && child.ColumnEndOffSet == 0)
                    childFullRows++;
 
                rowOffset += childFullRows;
                colOffset = child.ColumnEndOffSet;
            }
 
            if (galleryItemCount == 0 || galleryItemCumulativeHeight == 0.0)
            {
                internalScrollDelta = 16.0;
            }
            else
            {
                internalScrollDelta = galleryItemCumulativeHeight/galleryItemCount;
            }
            desiredSize.Width = maxChildWidth;
            desiredSize.Height = maxChildHeight;
 
            if (IsScrolling)
            {
                UpdateScrollingData(availableSize, desiredSize);
            }
 
            return desiredSize;
        }
#endif
 
        private Size AutoPassMeasureOverride()
        {
            Size desiredSize = new Size();
            Size childConstraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
            IContainsStarLayoutManager iContainsStarLayoutManager = (IContainsStarLayoutManager)this;
 
            // This is Auto(non Star) pass
            UIElementCollection children = InternalChildren;
            int childrenCount, endCountForUpdate = children.Count;
            int galleryItemCount = 0;
            double galleryItemCumulativeHeight = 0.0;
            double maxChildWidth = 0.0;
            double maxChildHeight = 0.0;
            double maxColumnWidth = 0.0;
 
            // Since we support SharedColumnSizes across different categories, we need
            // to repeatedly measure them until their MaxColumnWidth has been synchronized.
            // The inner loop iterates over all of those categories that need an update.
            // Of course in the beginning all of them do. During that pass we detect if the
            // MaxColumnWidth has increased such that a previously measured category needs
            // an update. If so we make another pass over those categories once more. We
            // repeat this strategy until none of the categories need an update.
 
            while (endCountForUpdate > 0)
            {
                childrenCount = endCountForUpdate;
                endCountForUpdate = 0;
                for (int i = 0; i < childrenCount; i++)
                {
                    UIElement child = children[i] as UIElement;
                    child.Measure(childConstraint);
                    Size childSize = child.DesiredSize;
                    maxChildWidth = Math.Max(maxChildWidth, childSize.Width);
                    maxChildHeight = Math.Max(maxChildHeight, childSize.Height);
 
                    RibbonGalleryCategory category = child as RibbonGalleryCategory;
                    if (category != null)
                    {
                        galleryItemCount += category.averageItemHeightInfo.Count;
                        galleryItemCumulativeHeight += category.averageItemHeightInfo.CumulativeHeight;
 
                        // If the category is a ColumnSizeScope in itself it does not need
                        // to be synchronized with the gallery's scope and hence can be ignored.
 
                        if (!category.IsSharedColumnSizeScope && DoubleUtil.GreaterThan(category.MaxColumnWidth, maxColumnWidth))
                        {
                            maxColumnWidth = category.MaxColumnWidth;
                            endCountForUpdate = i;
                        }
                    }
                }
            }
 
            if (galleryItemCount == 0 || galleryItemCumulativeHeight == 0.0)
            {
                internalScrollDelta = 16.0;
            }
            else
            {
                internalScrollDelta = galleryItemCumulativeHeight/galleryItemCount;
            }
 
            desiredSize.Width = maxChildWidth;
            desiredSize.Height = maxChildHeight;
 
            return desiredSize;
        }
 
        /// <summary>
        /// General RibbonGalleryCategoriesPanel layout behavior is to grow unbounded in the "vertical" direction (Size To Content).
        /// Children in this dimension are encouraged to be as large as they like.  In the other dimension,
        /// RibbonGalleryCategoriesPanel will assume the maximum size of its children.
        /// </summary>
        /// <remarks>
        /// When scrolling, RibbonGalleryCategoriesPanel will not grow in layout size but effectively add the children on a z-plane which
        /// will probably be clipped by some parent (typically a ScrollContentPresenter) to Stack's size.
        /// </remarks>
        /// <param name="constraint">Constraint</param>
        /// <returns>Desired size</returns>
 
        private Size RealMeasureOverride(Size constraint)
        {
            UIElementCollection children = InternalChildren;
            Size stackDesiredSize = new Size();
            Size layoutSlotSize = constraint;
 
            //
            // Initialize child sizing and iterator data
            // Allow children as much size as they want along the stack.
            //
            layoutSlotSize.Height = Double.PositiveInfinity;
 
            //
            //  Iterate through children.
            //  While we still supported virtualization, this was hidden in a child iterator (see source history).
            //
            for (int i = 0, count = children.Count; i < count; ++i)
            {
                // Get next child.
                UIElement child = children[i];
 
                if (child == null) { continue; }
 
                // Measure the child.
                child.Measure(layoutSlotSize);
                Size childDesiredSize = child.DesiredSize;
 
                stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);
                stackDesiredSize.Height += childDesiredSize.Height;
            }
            if (IsScrolling)
            {
                UpdateScrollingData(constraint, stackDesiredSize);
            }
 
            // Since we can offset and clip our content, we never need to be larger than the parent suggestion.
            // If we returned the full size of the content, we would always be so big we didn't need to scroll.  :)
            stackDesiredSize.Width = Math.Min(stackDesiredSize.Width, constraint.Width);
            stackDesiredSize.Height = Math.Min(stackDesiredSize.Height, constraint.Height);
 
            return stackDesiredSize;
        }
 
        // Compute Scrolling stuff while in Measure.
        private void UpdateScrollingData(Size constraint, Size stackDesiredSize)
        {
            double viewportOffsetY = (IsScrolling) ? _scrollData._offset.Y : 0;
            double logicalVisibleSpace = constraint.Height;
 
            // Compute viewport and extent.
            Size viewport = constraint;
            Size extent = stackDesiredSize;
            Vector offset = _scrollData._offset;
 
            // If we or children have resized, it's possible that we can now display more content.
            // This is true if we started at a nonzero offeset and still have space remaining.
            // In this case, we move up as much as remaining space.
            logicalVisibleSpace = DoubleUtil.LessThanOrClose((viewportOffsetY + logicalVisibleSpace) - stackDesiredSize.Height, 0.0) ? 0.0 : Math.Min((viewportOffsetY + logicalVisibleSpace) - stackDesiredSize.Height, constraint.Height);
            viewportOffsetY = Math.Max(viewportOffsetY - logicalVisibleSpace, 0.0);
 
            offset.Y = viewportOffsetY;
            offset.X = Math.Max(0, Math.Min(offset.X, extent.Width - viewport.Width));
 
            // Verify Scroll Info, invalidate ScrollOwner if necessary.
            VerifyScrollingData(viewport, extent, offset);
        }
 
        /// <summary>
        /// Content arrangement.
        /// </summary>
        /// <param name="finalSize">Arrange size</param>
        protected override Size ArrangeOverride(Size finalSize)
        {
            RibbonGallery gallery = this.Gallery;
#if IN_RIBBON_GALLERY
            if (gallery != null &&
                gallery.IsInInRibbonGalleryMode())
            {
                return InRibbonGalleryModeArrangeOverride(finalSize);
            }
            else
            {
#endif
                return RealArrangeOverride(finalSize);
#if IN_RIBBON_GALLERY
            }
#endif
        }
 
#if IN_RIBBON_GALLERY
        // InRibbonGalleryMode where the gallery is shown within Ribbon via InRibbonGallery and Arrange
        // becomes responsibility of InRibbonGalleryModeArrangeOverride.
        private Size InRibbonGalleryModeArrangeOverride(Size finalSize)
        {
            UIElementCollection children = this.Children;
            Rect rcChild = new Rect(finalSize);
 
            //
            // Seed scroll offset into rcChild.
            //
            if (IsScrolling)
            {
                rcChild.X = -1.0 * _scrollData._offset.X;
                rcChild.Y = -1.0 * _scrollData._offset.Y;
            }
 
            // Arrange and Position Children. over each other as the items being arranged in child
            // in a way to respect the offset of where previous category children are ending.
            // It's merged wrapping.
            for (int i = 0, count = children.Count; i < count; ++i)
            {
                RibbonGalleryCategory child = children[i] as RibbonGalleryCategory;
 
                if (child == null ||
                    child.Visibility == Visibility.Collapsed)
                {
                    continue;
                }
 
                rcChild.Width = Math.Max(finalSize.Width, child.DesiredSize.Width);
                child.Arrange(rcChild);
            }
 
            // Refresh the IsEnabled state of the InRibbonGallery's LineUp & LineDown buttons.
            RibbonGallery gallery = Gallery;
            if (gallery != null)
            {
                InRibbonGallery irg = gallery.ParentInRibbonGallery;
                if (irg != null &&
                    irg.IsInInRibbonMode)
                {
                    irg.CoerceValue(InRibbonGallery.CanLineUpProperty);
                    irg.CoerceValue(InRibbonGallery.CanLineDownProperty);
                }
            }
 
            return DesiredSize;
        }
#endif
 
        private Size RealArrangeOverride(Size finalSize)
        {
            UIElementCollection children = this.Children;
            Rect rcChild = new Rect(finalSize);
            double previousChildSize = 0.0;
 
            //
            // Compute scroll offset and seed it into rcChild.
            //
            if (IsScrolling)
            {
                rcChild.X = -1.0 * _scrollData._offset.X;
                rcChild.Y = -1.0 * _scrollData._offset.Y;
            }
 
            //
            // Arrange and Position Children.
            //
            for (int i = 0, count = children.Count; i < count; ++i)
            {
                UIElement child = (UIElement)children[i];
 
                if (child == null) { continue; }
 
                rcChild.Y += previousChildSize;
                previousChildSize = child.DesiredSize.Height;
                rcChild.Height = previousChildSize;
                rcChild.Width = Math.Max(finalSize.Width, child.DesiredSize.Width);
 
                child.Arrange(rcChild);
            }
 
            return finalSize;
        }
 
 
        #endregion Protected Methods
 
        #region IProvideStarLayoutInfoBase Members
 
        public void OnInitializeLayout()
        {
            IContainsStarLayoutManager iContainsStarLayoutManager = (IContainsStarLayoutManager)this;
            if (iContainsStarLayoutManager.StarLayoutManager != null)
            {
                TreeHelper.InvalidateMeasureForVisualAncestorPath(this, RibbonHelper.IsISupportStarLayout);
                RibbonGallery gallery = this.Gallery;
                if (gallery != null)
                {
                    gallery.InvalidateMeasureOnAllCategoriesPanel();
                }
            }
        }
 
        public UIElement TargetElement
        {
            get
            {
                return this.Gallery;
            }
        }
 
        #endregion
 
        #region IContainsStarLayoutManager Members
 
        ISupportStarLayout IContainsStarLayoutManager.StarLayoutManager
        {
            get;
            set;
        }
 
        #endregion
 
        #region IScrollInfo Methods
 
        /// <summary>
        /// Scroll content by one line to the top.
        /// </summary>
        public void LineUp()
        {
            SetVerticalOffset(VerticalOffset - internalScrollDelta);
        }
 
        /// <summary>
        /// Scroll content by one line to the bottom.
        /// </summary>
        public void LineDown()
        {
            SetVerticalOffset(VerticalOffset + internalScrollDelta);
        }
 
        /// <summary>
        /// Scroll content by one line to the left.
        /// </summary>
        public void LineLeft()
        {
            SetHorizontalOffset(HorizontalOffset - _scrollLineDelta);
        }
 
        /// <summary>
        /// Scroll content by one line to the right.
        /// </summary>
        public void LineRight()
        {
            SetHorizontalOffset(HorizontalOffset + _scrollLineDelta);
        }
 
        /// <summary>
        /// Scroll content by one page to the top.
        /// </summary>
        public void PageUp()
        {
            SetVerticalOffset(VerticalOffset - ViewportHeight);
        }
 
        /// <summary>
        /// Scroll content by one page to the bottom.
        /// </summary>
        public void PageDown()
        {
            SetVerticalOffset(VerticalOffset + ViewportHeight);
        }
 
        /// <summary>
        /// Scroll content by one page to the left.
        /// </summary>
        public void PageLeft()
        {
            SetHorizontalOffset(HorizontalOffset - ViewportWidth);
        }
 
        /// <summary>
        /// Scroll content by one page to the right.
        /// </summary>
        public void PageRight()
        {
            SetHorizontalOffset(HorizontalOffset + ViewportWidth);
        }
 
        /// <summary>
        /// Scroll content by SystemParameters.WheelScrollLines lines to the top.
        /// </summary>
        public void MouseWheelUp()
        {
            if (CanMouseWheelVerticallyScroll)
            {
                SetVerticalOffset(VerticalOffset - SystemParameters.WheelScrollLines * internalScrollDelta);
            }
            else
            {
                PageUp();
            }
        }
 
        /// <summary>
        /// Scroll content by SystemParameters.WheelScrollLines lines to the bottom.
        /// </summary>
        public void MouseWheelDown()
        {
            if (CanMouseWheelVerticallyScroll)
            {
                SetVerticalOffset(VerticalOffset + SystemParameters.WheelScrollLines * internalScrollDelta);
            }
            else
            {
                PageDown();
            }
        }
 
        /// <summary>
        /// Scroll content a little to the left.
        /// </summary>
        public void MouseWheelLeft()
        {
            SetHorizontalOffset(HorizontalOffset - 3.0 * _scrollLineDelta);
        }
 
        /// <summary>
        /// Scroll content a little to the right.
        /// </summary>
        public void MouseWheelRight()
        {
            SetHorizontalOffset(HorizontalOffset + 3.0 * _scrollLineDelta);
        }
 
        /// <summary>
        /// Set the HorizontalOffset to the passed value.
        /// </summary>
        public void SetHorizontalOffset(double offset)
        {
            EnsureScrollData();
            double scrollX = CoerceOffset(offset, _scrollData._extent.Width, _scrollData._viewport.Width);
            if (!DoubleUtil.AreClose(scrollX, _scrollData._offset.X))
            {
                _scrollData._offset.X = scrollX;
                InvalidateMeasure();
            }
        }
 
        /// <summary>
        /// Set the VerticalOffset to the passed value.
        /// </summary>
        public void SetVerticalOffset(double offset)
        {
            EnsureScrollData();
            double scrollY = CoerceOffset(offset, _scrollData._extent.Height, _scrollData._viewport.Height);
            if (!DoubleUtil.AreClose(scrollY, _scrollData._offset.Y))
            {
                _scrollData._offset.Y = scrollY;
                InvalidateMeasure();
            }
        }
 
        /// <summary>
        /// RibbonGalleryCategoriesPanel implementation of <seealso cref="IScrollInfo.MakeVisible" />.
        /// </summary>
        /// <param name="visual">The Visual that should become visible</param>
        /// <param name="rectangle">A rectangle representing in the visual's coordinate space to make visible.</param>
        /// <returns>
        /// A rectangle in the IScrollInfo's coordinate space that has been made visible.
        /// Other ancestors to in turn make this new rectangle visible.
        /// The rectangle should generally be a transformed version of the input rectangle.  In some cases, like
        /// when the input rectangle cannot entirely fit in the viewport, the return value might be smaller.
        /// </returns>
        public Rect MakeVisible(Visual visual, Rect rectangle)
        {
            // We can only work on visuals that are us or children.
            // An empty rect has no size or position.  We can't meaningfully use it.
            if (rectangle.IsEmpty
                || visual == null
                || visual == (Visual)this
                || !this.IsAncestorOf(visual))
            {
                return Rect.Empty;
            }
#pragma warning disable 1634, 1691
#pragma warning disable 56506
            // Compute the child's rect relative to (0,0) in our coordinate space.
            // This is a false positive by PreSharp. visual cannot be null because of the 'if' check above
            GeneralTransform childTransform = visual.TransformToAncestor(this);
#pragma warning restore 56506
#pragma warning restore 1634, 1691
            rectangle = childTransform.TransformBounds(rectangle);
 
            // We can't do any work unless we're scrolling.
            if (!IsScrolling)
            {
                return rectangle;
            }
 
            // Initialize the viewport
            Rect viewport = new Rect(HorizontalOffset, VerticalOffset, ViewportWidth, ViewportHeight);
            rectangle.X += viewport.X;
            rectangle.Y += viewport.Y;
 
            // Compute the offsets required to minimally scroll the child maximally into view.
            double minX = ComputeScrollOffsetWithMinimalScroll(viewport.Left, viewport.Right, rectangle.Left, rectangle.Right);
            double minY = ComputeScrollOffsetWithMinimalScroll(viewport.Top, viewport.Bottom, rectangle.Top, rectangle.Bottom);
 
            // We have computed the scrolling offsets; scroll to them.
            SetHorizontalOffset(minX);
            SetVerticalOffset(minY);
 
            // Compute the visible rectangle of the child relative to the viewport.
            viewport.X = minX;
            viewport.Y = minY;
            rectangle.Intersect(viewport);
 
            if (!rectangle.IsEmpty)
            {
                rectangle.X -= viewport.X;
                rectangle.Y -= viewport.Y;
            }
 
            // Return the rectangle
            return rectangle;
        }
 
        #endregion
 
        #region IScrollInfo Properties
 
        /// <summary>
        /// For RibbonGalleryCategoriesPanel it is always false.
        /// </summary>
        [DefaultValue(false)]
        public bool CanHorizontallyScroll
        {
            get
            {
                return false;
            }
            set
            {
                EnsureScrollData();
                if (_scrollData._allowHorizontal != value)
                {
                    _scrollData._allowHorizontal = false;
                    InvalidateMeasure();
                }
            }
        }
 
        /// <summary>
        /// For RibbonGalleryCategoriesPanel it is always true.
        /// </summary>
        [DefaultValue(true)]
        public bool CanVerticallyScroll
        {
            get
            {
                return true;
            }
            set
            {
                EnsureScrollData();
                if (_scrollData._allowVertical != value)
                {
                    _scrollData._allowVertical = true;
                    InvalidateMeasure();
                }
            }
        }
 
        /// <summary>
        /// ExtentWidth contains the horizontal size of the scrolled content element in 1/96"
        /// </summary>
        public double ExtentWidth
        {
            get
            {
                if (_scrollData == null) { return 0.0; }
                return _scrollData._extent.Width;
            }
        }
 
        /// <summary>
        /// ExtentHeight contains the vertical size of the scrolled content element in 1/96"
        /// </summary>
        public double ExtentHeight
        {
            get
            {
                if (_scrollData == null) { return 0.0; }
                return _scrollData._extent.Height;
            }
        }
 
        /// <summary>
        /// ViewportWidth contains the horizontal size of content's visible range in 1/96"
        /// </summary>
        public double ViewportWidth
        {
            get
            {
                if (_scrollData == null) { return 0.0; }
                return _scrollData._viewport.Width;
            }
        }
 
        /// <summary>
        /// ViewportHeight contains the vertical size of content's visible range in 1/96"
        /// </summary>
        public double ViewportHeight
        {
            get
            {
                if (_scrollData == null) { return 0.0; }
                return _scrollData._viewport.Height;
            }
        }
 
        /// <summary>
        /// HorizontalOffset is the horizontal offset of the scrolled content in 1/96".
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public double HorizontalOffset
        {
            get
            {
                if (_scrollData == null) { return 0.0; }
                return _scrollData._offset.X;
            }
        }
 
        /// <summary>
        /// VerticalOffset is the vertical offset of the scrolled content in 1/96".
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public double VerticalOffset
        {
            get
            {
                if (_scrollData == null) { return 0.0; }
                return _scrollData._offset.Y;
            }
        }
 
        /// <summary>
        /// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependant
        /// on this IScrollInfo's properties.
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public ScrollViewer ScrollOwner
        {
            get
            {
                EnsureScrollData();
                return _scrollData._scrollOwner;
            }
            set
            {
                EnsureScrollData();
                if (value != _scrollData._scrollOwner)
                {
                    ResetScrolling(this);
                    _scrollData._scrollOwner = value;
                }
            }
        }
 
        #endregion IScrollInfo Properties
 
        #region Data
 
        private RibbonGallery Gallery
        {
            get
            {
                return (RibbonGallery)ItemsControl.GetItemsOwner(this);
            }
        }
 
        // Logical scrolling.
        private ScrollData _scrollData;
 
        internal double internalScrollDelta;
        internal const double _scrollLineDelta = 16.0;   // Default physical amount to scroll with one Up/Down/Left/Right key
 
#if IN_RIBBON_GALLERY
        // Flag indicating whether we are constraining IRG.FirstGallery.ItemPresenter.MinWidth to be
        // approximately the ActualWidth of the IRG while in in-Ribbon mode.
        private bool _irgIsConstrainingWidth;
        private double _originalGalleryItemsPresenterMinWidth;
#endif
 
        #endregion Data
 
        #region Private Structures Classes
 
        //-----------------------------------------------------------
        // ScrollData class
        //-----------------------------------------------------------
        #region ScrollData
 
        // Helper class to hold scrolling data.
        // This class exists to reduce working set when StackPanel is used outside a scrolling situation.
        // Standard "extra pointer always for less data sometimes" cache savings model:
        //      !Scroll [1xReference]
        //      Scroll  [1xReference] + [4xDouble + 1xReference]
        private class ScrollData
        {
            // Clears layout generated data.
            // Does not clear scrollOwner, because unless resetting due to a scrollOwner change, we won't get reattached.
            internal void ClearLayout()
            {
                _offset = new Vector();
                _viewport = _extent = new Size();
                _allowHorizontal = false;
                _allowVertical = true;
            }
 
            // For Stack/Flow, the two dimensions of properties are in different units:
            // 1. The "logically scrolling" dimension uses items as units.
            // 2. The other dimension physically scrolls.  Units are in Avalon pixels (1/96").
            internal bool _allowHorizontal;
            internal bool _allowVertical;
            internal Vector _offset;            // Scroll offset of content.  Positive corresponds to a visually upward offset.
            internal Size _viewport;            // ViewportSize is in {pixels x items} (or vice-versa).
            internal Size _extent;              // Extent is the total number of children (logical dimension) or physical size
            internal ScrollViewer _scrollOwner; // ScrollViewer to which we're attached.
        }
 
        #endregion ScrollData
 
        #endregion Private Structures Classes
    }
}