Code:
/ Net / Net / 3.5.50727.3053 / DEVDIV / depot / DevDiv / releases / Orcas / SP / wpf / src / Framework / MS / Internal / documents / DocumentGrid.cs / 1 / DocumentGrid.cs
//---------------------------------------------------------------------------- // //// Copyright (C) Microsoft Corporation. All rights reserved. // // // // Description: DocumentGrid displays DocumentPaginator content in a grid-like // arrangement and is used by DocumentViewer to display documents. // // History: // 10/21/04 - jdersch created for complete and utter overhaul for the new // DocumentViewer control. // //--------------------------------------------------------------------------- using MS.Internal; using MS.Internal.Media; using MS.Utility; using MS.Win32; using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; namespace MS.Internal.Documents { ////// DocumentGrid is an internal Avalon FrameworkElement that executes all the /// "heavy lifting" involved in loading and displaying an DocumentPaginator-based /// document inside of a DocumentViewer control. /// ///http://d2/DRX/default.aspx internal class DocumentGrid : FrameworkElement, IDocumentScrollInfo { //----------------------------------------------------- // // Constructors // //----------------------------------------------------- #region Constructors ////// Static constructor /// static DocumentGrid() { //Register for the RequestBringIntoView event so we can get BIV events for //TextEditor IP movements. EventManager.RegisterClassHandler(typeof(DocumentGrid), RequestBringIntoViewEvent, new RequestBringIntoViewEventHandler(OnRequestBringIntoView)); //Register the default ContextMenu DocumentGridContextMenu.RegisterClassHandler(); } ////// The constructor /// public DocumentGrid() : base() { Initialize(); } #endregion Constructors //------------------------------------------------------ // // Internal Methods // //----------------------------------------------------- #region Internal Methods ////// Hit-Test on the multi-page UI scope to return a DocumentPage /// that contains this point /// /// Point in pixel unit, relative to the UI Scope's coordinates ///A DocumentPage that is hit or null if no page is hit internal DocumentPage GetDocumentPageFromPoint(Point point) { DocumentPageView dp = GetDocumentPageViewFromPoint(point); // if we hit a DocumentPageView we can return its DocumentPage. if (dp != null) { return dp.DocumentPage; } //Nothing hit, return null. return null; } #endregion Internal Methods //------------------------------------------------------ // // Public Interfaces // //------------------------------------------------------ #region Interface Implementations #region IDocumentScrollInfo //----------------------------------------------------- // // IDocumentScrollInfo Methods // //------------------------------------------------------ ////// Scroll content by one line to the top. /// public void LineUp() { if (_canVerticallyScroll) { SetVerticalOffsetInternal(VerticalOffset - _verticalLineScrollAmount); } } ////// Scroll content by one line to the bottom. /// public void LineDown() { if (_canVerticallyScroll) { SetVerticalOffsetInternal(VerticalOffset + _verticalLineScrollAmount); //Perf Tracing - Mark LineDown Start if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXLINEDOWNGUID), MS.Utility.EventType.Info, (int)VerticalOffset ); } } } ////// Scroll content by one line to the left. /// public void LineLeft() { if (_canHorizontallyScroll) { SetHorizontalOffsetInternal(HorizontalOffset - _horizontalLineScrollAmount); } } ////// Scroll content by one line to the right. /// public void LineRight() { if (_canHorizontallyScroll) { SetHorizontalOffsetInternal(HorizontalOffset + _horizontalLineScrollAmount); } } ////// Scroll content by one viewport to the top. /// public void PageUp() { SetVerticalOffsetInternal(VerticalOffset - ViewportHeight); } ////// Scroll content by one viewport to the bottom. /// public void PageDown() { SetVerticalOffsetInternal(VerticalOffset + ViewportHeight); //Perf Tracing - Mark PageDown Start if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEDOWNGUID), MS.Utility.EventType.Info, (int)VerticalOffset); } } ////// Scroll content by one viewport to the left. /// public void PageLeft() { SetHorizontalOffsetInternal(HorizontalOffset - ViewportWidth); } ////// Scroll content by one viewport to the right. /// public void PageRight() { SetHorizontalOffsetInternal(HorizontalOffset + ViewportWidth); } ////// Scroll content up via the mousewheel. /// public void MouseWheelUp() { if (_canVerticallyScroll) { SetVerticalOffsetInternal(VerticalOffset - MouseWheelVerticalScrollAmount); } else { PageUp(); } } ////// Scroll content down via the mousewheel. /// public void MouseWheelDown() { if (_canVerticallyScroll) { SetVerticalOffsetInternal(VerticalOffset + MouseWheelVerticalScrollAmount); } else { PageDown(); } } ////// Scroll content left via the mousewheel. /// public void MouseWheelLeft() { if (_canHorizontallyScroll) { SetHorizontalOffsetInternal(HorizontalOffset - MouseWheelHorizontalScrollAmount); } else { PageLeft(); } } ////// Scroll content right via the mousewheel. /// public void MouseWheelRight() { if (_canHorizontallyScroll) { SetHorizontalOffsetInternal(HorizontalOffset + MouseWheelHorizontalScrollAmount); } else { PageRight(); } } ////// Ensures that the specified visual is made visible. /// ////// 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. /// public Rect MakeVisible(Visual v, Rect r) { if (Content != null && v != null) { ContentPosition cp = Content.GetObjectPosition(v); MakeContentPositionVisibleAsync(new MakeVisibleData(v, cp, r)); } return r; } ////// Ensures that the specified object is made visible, given that the page it lives on is already known. /// ////// 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. /// public Rect MakeVisible(object o, Rect r, int pageNumber) { MakeVisibleAsync(new MakeVisibleData(o as Visual, o as ContentPosition, r), pageNumber); return r; } ////// Scrolls the current selection into view. Requests for empty or /// invalid selections will do nothing. /// public void MakeSelectionVisible() { //We can only continue if we have a TextEditor attached... if (TextEditor != null && TextEditor.Selection != null) { //Get the TextPointer for the start of our selection. ITextPointer tp = TextEditor.Selection.Start; //Ensure that the TextPointer we use has gravity set to forwards (or into //the selection) so that the selected text is always displayed. tp = tp.CreatePointer(LogicalDirection.Forward); //If the TextPointer is also a ContentPosition, we can //make that ContentPosition visible. ContentPosition cp = tp as ContentPosition; MakeContentPositionVisibleAsync(new MakeVisibleData(null, cp, Rect.Empty)); } } ////// Scrolls the requested page into view. /// /// The page to make visible. public void MakePageVisible(int pageNumber) { //If we're moving more than one page then this is a "page jump" //and we should log the perf event. if (Math.Abs(pageNumber - _firstVisiblePageNumber) > 1) { //Perf Tracing - Mark Page Jump Start if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEJUMPGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, pageNumber); } } //Clip the offset into range for out-of-range page numbers if (pageNumber < 0 ) { //Clip to the top-left of the document SetVerticalOffsetInternal(0.0d); SetHorizontalOffsetInternal(0.0d); } else if (pageNumber >= _pageCache.PageCount || _rowCache.RowCount == 0) { //If the doc is done loading, then this page is out of range. if (_pageCache.IsPaginationCompleted && _rowCache.HasValidLayout) { //Clip to the bottom-right of the document SetVerticalOffsetInternal(ExtentHeight); SetHorizontalOffsetInternal(ExtentWidth); } else { //The doc is not done loading. //Wait for the page to be laid out and try again. _pageJumpAfterLayout = true; _pageJumpAfterLayoutPageNumber = pageNumber; } } else { //This page is valid, so scroll to it now. RowInfo scrolledRow = _rowCache.GetRowForPageNumber(pageNumber); SetVerticalOffsetInternal(scrolledRow.VerticalOffset); //Calculate the Horizontal offset of the page we're bringing into view: double horizontalOffset = GetHorizontalOffsetForPage(scrolledRow, pageNumber); SetHorizontalOffsetInternal(horizontalOffset); } } ////// Scrolls the next row of pages into view. This differs from /// IScrollInfo?s "PageDown" in that PageDown pages by Viewports /// which may not coincide with page dimensions, whereas /// ScrollToNextRow takes these dimensions into account so that /// precisely the next row of pages is displayed. /// public void ScrollToNextRow() { //We change our vertical offset to be the offset of the next row (if there is one). //If there isn't, we do nothing. int nextRow = _firstVisibleRow + 1; if (nextRow < _rowCache.RowCount) { //Get the next row. RowInfo row = _rowCache.GetRow(nextRow); SetVerticalOffsetInternal(row.VerticalOffset); } } /// Scrolls the previous row of pages into view. This differs from /// IScrollInfo?s "PageUp" in that PageUp pages by Viewports /// which may not coincide with page dimensions, whereas /// ScrollToPreviousRow takes these dimensions into account so that /// precisely the previously row of pages is displayed. public void ScrollToPreviousRow() { //We change our vertical offset to be the offset of the previous row (if there is one). int previousRow = _firstVisibleRow - 1; if (previousRow >= 0 && previousRow < _rowCache.RowCount) { //Get the previous row. RowInfo row = _rowCache.GetRow(previousRow); SetVerticalOffsetInternal(row.VerticalOffset); } } ////// Scrolls to the top of the document. /// public void ScrollToHome() { //We just set the VerticalOffset to 0. SetVerticalOffsetInternal(0); } ////// Scrolls to the bottom of the document. /// public void ScrollToEnd() { //We just set the VerticalOffset to our document's extent. SetVerticalOffsetInternal(ExtentHeight); } ////// Sets the scale factor applied to pages in the document, while /// keeping the "Active Focus" centered. /// /// public void SetScale(double scale) { if (!DoubleUtil.AreClose(scale, Scale)) { if (scale <= 0.0) { throw new ArgumentOutOfRangeException("scale"); } if (!Helper.IsDoubleValid(scale)) { throw new ArgumentOutOfRangeException("scale"); } QueueSetScale(scale); } } ////// Changes the view to the specified number of columns. /// /// public void SetColumns(int columns) { if (columns < 1) { throw new ArgumentOutOfRangeException("columns"); } //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout(new DocumentLayout(columns, ViewMode.SetColumns)); } ////// Changes the view to the specified number of columns. /// /// public void FitColumns(int columns) { if (columns < 1) { throw new ArgumentOutOfRangeException("columns"); } //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout(new DocumentLayout(columns, ViewMode.FitColumns)); } ////// Changes the view to a single page, scaled such that it is as wide as the Viewport. /// public void FitToPageWidth() { //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout( new DocumentLayout(1 /* one column */, ViewMode.PageWidth)); } ////// Changes the view to a single page, scaled such that it is as tall as the Viewport. /// public void FitToPageHeight() { //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout( new DocumentLayout(1 /* one column */, ViewMode.PageHeight)); } ////// Changes the view to ?thumbnail view? which will scale the document /// such that as many pages are visible at once as is possible. /// public void ViewThumbnails() { //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout( new DocumentLayout(1 /* one column, arbitrary */, ViewMode.Thumbnails)); } //----------------------------------------------------- // // IDocumentScrollInfo Properties // //----------------------------------------------------- ////// DocumentGrid always scrolls in both dimensions. /// public bool CanHorizontallyScroll { get { return _canHorizontallyScroll; } set { _canHorizontallyScroll = value; } } ////// DocumentGrid always scrolls in both dimensions. /// public bool CanVerticallyScroll { get { return _canVerticallyScroll; } set { _canVerticallyScroll = value; } } ////// ExtentWidth contains the full horizontal range of the scrolled content. /// public double ExtentWidth { get { return _rowCache.ExtentWidth; } } ////// ExtentHeight contains the full vertical range of the scrolled content. /// public double ExtentHeight { get { return _rowCache.ExtentHeight; } } ////// ViewportWidth contains the currently visible horizontal range of the scrolled content. /// public double ViewportWidth { get { return _viewportWidth; } } ////// ViewportHeight contains the currently visible vertical range of the scrolled content. /// public double ViewportHeight { get { return _viewportHeight; } } ////// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible. /// Valid values are inclusively between 0 and public double HorizontalOffset { get { //Clip HorizontalOffset into range. double clippedHorizontalOffset = Math.Min(_horizontalOffset, ExtentWidth - ViewportWidth); clippedHorizontalOffset = Math.Max(clippedHorizontalOffset, 0.0); return clippedHorizontalOffset; } } ///less . /// /// Set the HorizontalOffset. If there are pending layout delegates, then /// this will be processed by a delegate. /// /// public void SetHorizontalOffset(double offset) { if (!DoubleUtil.AreClose(_horizontalOffset,offset)) { if (Double.IsNaN(offset)) { throw new ArgumentOutOfRangeException("offset"); } // If there aren't any pending document layout delegates, then change // the HorizontalOffset immediately, otherwise schedule a delegate for it. if (_documentLayoutsPending == 0) { SetHorizontalOffsetInternal(offset); } else { QueueUpdateDocumentLayout(new DocumentLayout(offset, ViewMode.SetHorizontalOffset)); } } } ////// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible. /// Valid values are inclusively between 0 and public double VerticalOffset { get { //Clip VerticalOffset into range. double clippedVerticalOffset = Math.Min(_verticalOffset, ExtentHeight - ViewportHeight); clippedVerticalOffset = Math.Max(clippedVerticalOffset, 0.0); return clippedVerticalOffset; } } ///less . /// /// Set the VerticalOffset. If there are pending layout delegates, then /// this will be processed by a delegate. /// /// public void SetVerticalOffset(double offset) { if (!DoubleUtil.AreClose(_verticalOffset, offset)) { if (Double.IsNaN(offset)) { throw new ArgumentOutOfRangeException("offset"); } // If there aren't any pending document layout delegates, then change // the VerticalOffset immediately, otherwise schedule a delegate for it. if (_documentLayoutsPending == 0) { SetVerticalOffsetInternal(offset); } else { QueueUpdateDocumentLayout(new DocumentLayout(offset, ViewMode.SetVerticalOffset)); } } } ////// Provides the IDocumentScrollInfo implementer with a content /// tree to be paginated. Developers are free to modify this /// Content at any time (remove, add, modify pages, etc?) /// and the IDocumentScrollInfo implementer is responsible for /// noting the changes and updating as necessary. /// ///The DocumentPaginator to be assigned as the content public DynamicDocumentPaginator Content { get { //_pageCache is guaranteed to be non-null as it's created in the //Constructor. return _pageCache.Content; } set { //_pageCache is guaranteed to be non-null as it's created in the //Constructor. if (value != _pageCache.Content) { //Null out our TextContainer. It will be created as needed. _textContainer = null; //Remove our old events from the content if (_pageCache.Content != null) { _pageCache.Content.GetPageNumberCompleted -= new GetPageNumberCompletedEventHandler(OnGetPageNumberCompleted); } //Remove our ScrollChanged events from our ScrollViewer if (ScrollOwner != null) { ScrollOwner.ScrollChanged -= new ScrollChangedEventHandler(OnScrollChanged); _scrollChangedEventAttached = false; } //Assign the new content _pageCache.Content = value; if (_pageCache.Content != null) { //Add our new events to the content _pageCache.Content.GetPageNumberCompleted += new GetPageNumberCompletedEventHandler(OnGetPageNumberCompleted); } //Clear out our visual collection so that the old pages (pointing to old content) //will be replaced with new ones on the next Measure/Arrange pass. ResetVisualTree(false /*pruneOnly*/); ResetPageViewCollection(); //Reset our visible pages. _firstVisiblePageNumber = 0; _lastVisiblePageNumber = 0; // Perf Tracing - PageVisible Changed if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEVISIBLEGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, _lastVisiblePageNumber); } _lastRowChangeExtentWidth = 0.0; _lastRowChangeVerticalOffset = 0.0; //Cause the new content to be laid out in the same fashion as //the previous content. //If the view is Thumbnails we'll change it to SetColumns //so that the Column count will be maintained. This is done //because we're getting new content which initially has 0 //pages and a Thumbnail view only gives decent results after //the entire content has been loaded; since we don't want to provide a //"jarring" situation (where layout suddenly changes after the content's loaded) //we use SetColumns to maintain the same exact layout as the old content. //(This is consistent with DocumentViewer's overall behavior -- any view setting is //a "one time thing" and isn't recomputed if the content changes.) if (_documentLayout.ViewMode == ViewMode.Thumbnails) { _documentLayout.ViewMode = ViewMode.SetColumns; } QueueUpdateDocumentLayout(_documentLayout); //Invalidate Measure and our IDSI so that properties changed //by the content assignment will be properly updated. InvalidateMeasure(); InvalidateDocumentScrollInfo(); } } } ////// Indicates the number of pages currently in the document. /// ///public int PageCount { get { return _pageCache.PageCount; } } /// /// When queried, FirstVisiblePageNumber returns the first page visible onscreen. /// ///public int FirstVisiblePageNumber { get { return _firstVisiblePageNumber; } } /// /// Returns the current Scale factor applied to the pages given the current settings. /// ///public double Scale { get { return _rowCache.Scale; } } /// /// Returns the current number of Columns of pages displayed given the current settings. /// ///public int MaxPagesAcross { get { return _maxPagesAcross; } } /// /// Specifies the vertical gap between Pages when laid out, in pixel (1/96?) units. /// ///public double VerticalPageSpacing { get { return _rowCache.VerticalPageSpacing; } set { if (!Helper.IsDoubleValid(value)) { throw new ArgumentOutOfRangeException("value"); } _rowCache.VerticalPageSpacing = value; } } /// /// Specifies the horizontal gap between Pages when laid out, in pixel (1/96?) units. /// ///public double HorizontalPageSpacing { get { return _rowCache.HorizontalPageSpacing; } set { if (!Helper.IsDoubleValid(value)) { throw new ArgumentOutOfRangeException("value"); } _rowCache.HorizontalPageSpacing = value; } } /// /// Specifies whether each displayed page should be adorned with a ?Drop Shadow? border or not. /// ///public bool ShowPageBorders { get { return _showPageBorders; } set { if (_showPageBorders != value) { _showPageBorders = value; //Update our pages' ShowPageBorder properties. //Get the current Visual Collection, which contains our pages. int count = _childrenCollection.Count; for (int i = 0; i < count; i++) { DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage; if (dp != null) { dp.ShowPageBorders = _showPageBorders; } } } } } /// /// Specifies whether the last "view mode" related property change should be locked /// for resizing. /// ///public bool LockViewModes { get { return _lockViewModes; } set { _lockViewModes = value; } } /// /// Returns a TextContainer for current content /// ///The content's TextContainer, or null if there is none. public ITextContainer TextContainer { get { if (_textContainer == null) { if (Content != null) { IServiceProvider isp = Content as IServiceProvider; if (isp != null) { _textContainer = (ITextContainer)isp.GetService(typeof(ITextContainer)); } } } return _textContainer; } } ////// Returns the MultiPageTextView for the current content. /// ///public ITextView TextView { get { if (TextEditor != null) { return TextEditor.TextView; } else { return null; } } } /// /// The collection of currently-visible DocumentPageViews. /// public ReadOnlyCollectionPageViews { get { return _pageViews; } } /// /// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependent /// on this IScrollInfo's properties. Implementers of IScrollInfo should call InvalidateScrollInfo() /// on this object when related properties change. /// public ScrollViewer ScrollOwner { get { return _scrollOwner; } set { _scrollOwner = value; InvalidateDocumentScrollInfo(); } } ////// DocumentViewerOwner is the DocumentViewer Control and UI that hosts the IDocumentScrollInfo object. /// This control is dependent on this IDSI?s properties, so implementers of IDSI should call /// InvalidateDocumentScrollInfo() on this object when related properties change so that /// DocumentViewer?s UI will be kept in [....]. This property is analogous to IScrollInfo?s ScrollOwner /// property. /// ///public DocumentViewer DocumentViewerOwner { get { return _documentViewerOwner; } set { _documentViewerOwner = value; } } #endregion IDocumentScrollInfo #endregion Interface Implementations //----------------------------------------------------- // // Protected Methods // //------------------------------------------------------ #region Protected Methods /// /// Derived class must implement to support Visual children. The method must return /// the child at the specified index. Index must be between 0 and GetVisualChildrenCount-1. /// /// By default a Visual does not have any children. /// /// Remark: /// During this virtual call it is not valid to modify the Visual tree. /// protected override Visual GetVisualChild(int index) { if(_childrenCollection == null || index < 0 || index >= _childrenCollection.Count) { throw new ArgumentOutOfRangeException("index", index, SR.Get(SRID.Visual_ArgumentOutOfRange)); } return _childrenCollection[index]; } ////// Derived classes override this property to enable the Visual code to enumerate /// the Visual children. Derived classes need to return the number of children /// from this method. /// /// By default a Visual does not have any children. /// /// Remark: During this virtual method the Visual tree must not be modified. /// protected override int VisualChildrenCount { get { // _childrenCollection cannot be null since its initialized in the constructor return _childrenCollection.Count; } } ////// MeasureOverride is repsonsible for measuring any visible pages to their correct sizes. /// /// The upper bound for child sizes ///protected override Size MeasureOverride(Size constraint) { // If layoutSize is infinity, we need to return our absolute smallest size. // This might happen if we are inside an element which sizes-to-content. // For DocumentGrid, we use a hard coded constraint. if (double.IsInfinity(constraint.Width) || double.IsInfinity(constraint.Height)) { constraint = _defaultConstraint; } //Determine which pages are visible at the current offset given the current constraint. RecalculateVisualPages(VerticalOffset, constraint); //Get our visual children count... int count = _childrenCollection.Count; //Now go through our child collection and measure all the pages to their sizes. for(int i = 0; i < count; i++) { //This should be our background. if (i == _backgroundVisualIndex) { Border background = _childrenCollection[i] as Border; if (background == null) { throw new InvalidOperationException(SR.Get(SRID.DocumentGridVisualTreeContainsNonBorderAsFirstElement)); } //We measure this to the size of our constraint. background.Measure(constraint); } //Otherwise it's a page. else { //Ensure that this is actually a DocumentGridPage. If it is not, //Then someone's been mucking with our VisualTree, so we'll throw. DocumentGridPage page = _childrenCollection[i] as DocumentGridPage; if (page == null) { throw new InvalidOperationException(SR.Get(SRID.DocumentGridVisualTreeContainsNonDocumentGridPage)); } //Get the cached size of this page and scale it to our current scale factor. Size pageSize = _pageCache.GetPageSize(page.PageNumber); pageSize.Width *= Scale; pageSize.Height *= Scale; //Measure the page if necessary. if (!page.IsMeasureValid) { page.Measure(pageSize); //See if the cached size has changed since we Measured. //This can happen if in the course of Measuring the page //a GetPageAsync() calls back immediately with the real page size. //If this happens we need to re-measure the page before we finish here, //otherwise we'll end up with a page that's Measured to one size //and Arranged to another, which looks bad. Size newPageSize = _pageCache.GetPageSize(page.PageNumber); if (newPageSize != Size.Empty) { newPageSize.Width *= Scale; newPageSize.Height *= Scale; if (newPageSize.Width != pageSize.Width || newPageSize.Height != pageSize.Height) { //Measure again. page.Measure(newPageSize); } } } } } return constraint; } /// /// ArrangeOverride is responsible for arranging the previously measured pages in the right order. /// /// The final constraint, inside of which everything must live. ///protected override Size ArrangeOverride(Size arrangeSize) { if (_viewportHeight != arrangeSize.Height || _viewportWidth != arrangeSize.Width) { //Update our Viewport sizes _viewportWidth = arrangeSize.Width; _viewportHeight = arrangeSize.Height; if( LockViewModes && IsViewLoaded()) { if (_firstVisiblePageNumber < _pageCache.PageCount && _rowCache.HasValidLayout) { //If we’re locking the view modes and we have loaded content, then we need to re-apply //the last mode setting since our constraint has changed ApplyViewParameters(_rowCache.GetRowForPageNumber(_firstVisiblePageNumber)); MeasureOverride(arrangeSize); } } UpdateTextView(); } //If we have a non-zero viewport size, we should execute any //requests for layout that may have been made but were unable //to complete due to a zero viewport size. if (IsViewportNonzero) { if (ExecutePendingLayoutRequests()) { //We need to re-do layout (RowCache has changed), so call measure here //to ensure everything's updated accordingly. MeasureOverride(arrangeSize); } } //If our constraint size has changed then we need to //alert our parents so they can update their ViewportWidth/Height properties. if ( _previousConstraint != arrangeSize) { _previousConstraint = arrangeSize; InvalidateDocumentScrollInfo(); } //Now we go through the visible rows and arrange the pages within them. //Get our visual collection count int count = _childrenCollection.Count; //If we have no visual children, there's nothing to arrange //so quit now. if (count == 0) { return arrangeSize; } //Arrange the background first. This is always child 0. //The background takes up the entire constraint. UIElement background =_childrenCollection[_backgroundVisualIndex] as UIElement; background.Arrange(new Rect(new Point(0, 0), arrangeSize)); //The offsets for the current page being arranged. double xOffset; double yOffset; //The current visual child (aka DocumentGridPage) we're arranging. //The first child in our tree is always the background so we start at //1 which is our first page. int visualChild = _firstPageVisualIndex; //Now walk through the visible rows and arrange the pages therein. for (int row = _firstVisibleRow; row < _firstVisibleRow + _visibleRowCount; row++) { //Calculate the position for this row. CalculateRowOffsets(row, out xOffset, out yOffset); //Get the current row. RowInfo currentRow = _rowCache.GetRow(row); //Now we can lay out this row. for (int page = currentRow.FirstPage; page < currentRow.FirstPage + currentRow.PageCount; page++) { //This should never, ever happen so we'll throw if it does. if (visualChild > _childrenCollection.Count - 1) { throw new InvalidOperationException(SR.Get(SRID.DocumentGridVisualTreeOutOfSync)); } //Get the cached size of this page. Size pageSize = _pageCache.GetPageSize(page); //Scale it by our scale factor pageSize.Width *= Scale; pageSize.Height *= Scale; //Arrange the page if necessary UIElement uiPage = _childrenCollection[visualChild] as UIElement; if (uiPage != null) { Point pageOffset; //Move the page to the right place based on the FlowDirection of the content. if (_pageCache.IsContentRightToLeft) { pageOffset = new Point(Math.Max(ViewportWidth, ExtentWidth) - (xOffset + pageSize.Width), yOffset); } else { pageOffset = new Point(xOffset, yOffset); } uiPage.Arrange(new Rect(pageOffset, pageSize)); } else { throw new InvalidOperationException(SR.Get(SRID.DocumentGridVisualTreeContainsNonUIElement)); } //Increment our horizontal offset to point to where the next page should go. xOffset += (pageSize.Width+HorizontalPageSpacing); //Move to the next page. visualChild++; } } // As we scroll we need to keep the AdornerLayer up-to-date. // This ensures that annotation components scroll with the content. AdornerLayer layer = AdornerLayer.GetAdornerLayer(this); if (layer != null && layer.GetAdorners(this) != null) layer.Update(this); return arrangeSize; } /// /// Override the OnPreviewMouseLeftButtonDown method so that we can trap the /// keyboard+mouse events needed for Rubberband selection. /// Clicking the Left mouse button while holding Alt will enable the Rubberband /// selection "mode" until the Left mouse button is again pressed without the /// Alt key held. /// /// The MouseButtonEventArgs associated with this mouse event. protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { //Determine whether either Alt key is being held at this moment. bool altKeyDown = Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt); //If the Alt key is held and we aren't currently in RubberBandSelection mode, //we can create and attach our RubberBandSelector now. //We'll stay in this mode until the mouse is clicked without the Alt key held. if (altKeyDown && _rubberBandSelector == null ) { //See if our content implements IServiceProvider. IServiceProvider serviceProvider = Content as IServiceProvider; if (serviceProvider != null) { //See if our content supports rubber band selection. _rubberBandSelector = serviceProvider.GetService(typeof(RubberbandSelector)) as RubberbandSelector; if (_rubberBandSelector != null) { DocumentViewerOwner.Focus(); // text editor needs to be focused when cleared ITextRange textRange = TextEditor.Selection; textRange.Select(textRange.Start, textRange.Start); //clear selection DocumentViewerOwner.IsSelectionEnabled = false; _rubberBandSelector.AttachRubberbandSelector((FrameworkElement)this); //attach the Rubber band selector. } } } //We got a mouse-down event and the Alt key is not being held, so we revert back //to normal selection mode now. else if (!altKeyDown && _rubberBandSelector != null) { //Detach the Rubberband Selector if (_rubberBandSelector != null) { _rubberBandSelector.DetachRubberbandSelector(); _rubberBandSelector = null; } DocumentViewerOwner.IsSelectionEnabled = true; } } #endregion Protected Methods //----------------------------------------------------- // // Private Methods // //------------------------------------------------------ #region Private Methods ////// Recalculates the set of pages that are currently visible and updates /// DocumentGrid's VisualCollection so it contains them. /// /// The viewport to search for visible pages in. /// The offset in the document to start the search. private void RecalculateVisualPages(double offset, Size constraint) { //Do we actually have any rows in the cache? //If not, we can just clear our visual collection and return. if (_rowCache.RowCount == 0) { ResetVisualTree(false /*pruneOnly*/); ResetPageViewCollection(); _firstVisibleRow = 0; _visibleRowCount = 0; _firstVisiblePageNumber = 0; _lastVisiblePageNumber = 0; // Perf Tracing - PageVisible Changed if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEVISIBLEGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, _lastVisiblePageNumber); } return; } int newFirstVisibleRow = 0; int newVisibleRowCount = 0; //Ask the RowCache for the currently visible rows. _rowCache.GetVisibleRowIndices(offset, offset + constraint.Height, out newFirstVisibleRow, out newVisibleRowCount); //Do we have no visible rows at all? Then clear the Visual collection and return. if (newVisibleRowCount == 0) { ResetVisualTree(false /*pruneOnly*/); ResetPageViewCollection(); _firstVisibleRow = 0; _visibleRowCount = 0; _firstVisiblePageNumber = 0; _lastVisiblePageNumber = 0; // Perf Tracing - PageVisible Changed if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEVISIBLEGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, _lastVisiblePageNumber); } return; } //Now walk through each visible row and compare the pages therein //with the current set of pages in our Visual Collection. //New pages are inserted into the collection, unused pages are removed. //Get the current first and last pages visible. int firstPage = -1; int lastPage = -1; //If we have more visuals than just the background (element 0) //then we have pages, so get the page numbers from them. if (_childrenCollection.Count > _firstPageVisualIndex) { DocumentGridPage firstDp = _childrenCollection[1] as DocumentGridPage; firstPage = firstDp != null ? firstDp.PageNumber : -1; DocumentGridPage lastDp = _childrenCollection[_childrenCollection.Count-1] as DocumentGridPage; lastPage = lastDp != null ? lastDp.PageNumber : -1; } //Update our First & LastVisiblePage properties RowInfo firstRow = _rowCache.GetRow(newFirstVisibleRow); _firstVisiblePageNumber = firstRow.FirstPage; RowInfo lastRow = _rowCache.GetRow(newFirstVisibleRow + newVisibleRowCount - 1); _lastVisiblePageNumber = lastRow.FirstPage + lastRow.PageCount - 1; // Perf Tracing - PageVisible Changed if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEVISIBLEGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, _lastVisiblePageNumber); } //Update our cached visible row info (used by Measure/Arrange) _firstVisibleRow = newFirstVisibleRow; _visibleRowCount = newVisibleRowCount; //If IDSI properties have changed (namely the First/LastVisiblePage properties) we invalidate them now. if (_firstVisiblePageNumber != firstPage || _lastVisiblePageNumber != lastPage) { //Create our temporary VisualCollection, which will hold the new list of //visible pages. ArrayList visiblePages = new ArrayList(); //Now walk through the visible rows and add the pages to our temporary list. for (int i = _firstVisibleRow; i < _firstVisibleRow + _visibleRowCount; i++) { //Get the row RowInfo currentRow = _rowCache.GetRow(i); //Walk through the row for (int j = currentRow.FirstPage; j < currentRow.FirstPage + currentRow.PageCount; j++) { //Is this page new? if (j < firstPage || j > lastPage || _childrenCollection.Count <= _firstPageVisualIndex) { //Create a new page and add it to our temporary visual collection. DocumentGridPage dp = new DocumentGridPage(Content); dp.ShowPageBorders = ShowPageBorders; dp.PageNumber = j; //Attach the Loaded event handler dp.PageLoaded += new EventHandler(OnPageLoaded); visiblePages.Add(dp); } else { //This page already exists in our visual collection, so we copy that entry over //from the visual collection instead of creating a new page. //(We start at 1 to skip over the background visual.) visiblePages.Add(_childrenCollection[_firstPageVisualIndex + j - Math.Max(0, firstPage)]); } } } //Copy our new visible page collection over to the VisualCollection and update //the MultiPageTextView's list of visible DocumentPageViews. //First, prune our visual tree so it only contains the set of pages that are visible //before and after the layout change. ResetVisualTree(true /*pruneOnly*/); CollectiondocumentPageViews = new Collection (); //State machine for updating visual collection without removing still-visible pages //from the collection: //We walk through the set of visible pages that we computed above. //We insert new pages before existing pages, and add pages after existing pages. // //We take advantage of the fact that both the current set of pages in the Visual Collection //and the set of to-be-made visible pages in visiblePages are in strictly increasing order //with no gaps between pages. //To better understand how the below works, refer to Fig. A below: // // +-----------------------------+ // +------+ +------+ // | new | Pruned Visual Collection | new | // +------+ (Existing Page Visuals) +------+ // +-----------------------------+ // ^ ^ ^ // | | | // A B C // // [Fig. A: Diagram of states] // //- The routine starts off in state A (BeforeExisting). // At this point we insert any new pages in the visiblePages collection until we // find a page in the visiblePages collection that is also in the Pruned Visual Collection. // This indicates that the set of common unchanged pages has been reached. // The state machine then transitions to state B (DuringExisting). //- The routine stays in B merely iterating through visiblePages until it finds a page // in visiblePages that does not correspond to a page in the Visual Tree. This indicates // that the end of the set of common unchanged pages has been reached; at this point we // add the new page and transition to state C (AfterExisting). //- State C ends when no more pages are left in visiblePages. VisualTreeModificationState state = VisualTreeModificationState.BeforeExisting; //The index pointing to the first common page still in the visual tree after the above pruning. int vcIndex = _firstPageVisualIndex; for (int i = 0; i < visiblePages.Count; i++) { Visual current = (Visual)visiblePages[i]; switch (state) { case VisualTreeModificationState.BeforeExisting: //Keep inserting until we find a page that already exists if (vcIndex < _childrenCollection.Count && _childrenCollection[vcIndex] == current) { //Move to "During" state state = VisualTreeModificationState.DuringExisting; } else { //Insert this page at the current index. _childrenCollection.Insert(vcIndex, current); } //Increment the index into the Visual collection to ensure that it continues //to point to the first common page. vcIndex++; break; case VisualTreeModificationState.DuringExisting: //Leave the visual collection alone until we find a page that isn't in the collection //or run out of pages in the collection. if (vcIndex >= _childrenCollection.Count || _childrenCollection[vcIndex] != current) { //Move to "After" state state = VisualTreeModificationState.AfterExisting; //Append this page to the end. _childrenCollection.Add(current); } //Keep moving through the Visual collection... vcIndex++; break; case VisualTreeModificationState.AfterExisting: //Keep going until the end. _childrenCollection.Add(current); break; } //Add this to the collection of PageViews. documentPageViews.Add(((DocumentGridPage)visiblePages[i]).DocumentPageView); } //Update our collection of PageViews with the current set. _pageViews = new ReadOnlyCollection (documentPageViews); //Tell our parent DocumentViewer that we've updated our PageView collection. InvalidatePageViews(); InvalidateDocumentScrollInfo(); } } /// /// Handles the PageLoaded event for a given page, and kicks off /// BringIntoView actions where necessary. /// /// /// private void OnPageLoaded(object sender, EventArgs args) { DocumentGridPage page = sender as DocumentGridPage; Invariant.Assert(page != null, "Invalid sender for OnPageLoaded event."); //Detach the event handler, we don't need this event any longer. page.PageLoaded -= new EventHandler(OnPageLoaded); //Is there a MakeVisible operation waiting for this page to be loaded? //If so, invoke its dispatcher in the background. if (_makeVisiblePageNeeded == page.PageNumber) { _makeVisiblePageNeeded = -1; _makeVisibleDispatcher.Priority = DispatcherPriority.Background; } // Perf Tracing - PageLoaded if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGELOADEDGUID), MS.Utility.EventType.Info, page.PageNumber); } } ////// Calculates the X and Y offsets of the given row based on the current /// Viewport dimensions. /// /// The row to calculate the offsets of /// The X offset of the row /// The Y offset of the row private void CalculateRowOffsets(int row, out double xOffset, out double yOffset) { xOffset = 0.0; yOffset = 0.0; //Get the current row. RowInfo currentRow = _rowCache.GetRow(row); //Figure out the width we'll use to center the pages. //If the ViewportWidth is wider than the document, we use that. Otherwise we center //the content based on the width of the document. double centerWidth = Math.Max(ViewportWidth, ExtentWidth); //Figure out the offset of the upper left corner of this row. //X Coordinate: //If this is the last row in the document and we're viewing //uniformly-sized pages then this row is //always left-aligned (not centered). if (row == _rowCache.RowCount -1 && !_pageCache.DynamicPageSizes) { //This is the last row, so we arrange it such that the left edge of the //page is flush with the left edge of the document. xOffset = (centerWidth - ExtentWidth) / 2.0 + (HorizontalPageSpacing / 2.0) - HorizontalOffset; } else { //Otherwise we center this page inside the document. xOffset = (centerWidth - currentRow.RowSize.Width) / 2.0 + (HorizontalPageSpacing / 2.0) - HorizontalOffset; } //Y Coordinate: if (ExtentHeight > ViewportHeight) { //The document is taller than the viewport, so we just display //the content at the current offset. yOffset = currentRow.VerticalOffset + (VerticalPageSpacing / 2.0) - VerticalOffset; } else { //If the document is shorter than the Viewport we're showing it in, //we center it vertically within the viewport. We do not need to factor in //VerticalOffset as it is always 0.0 in this scenario. yOffset = currentRow.VerticalOffset + (ViewportHeight - ExtentHeight) / 2.0 + (VerticalPageSpacing / 2.0); } } ////// Resets DocumentGrid's visual tree to its initial state or prunes non-visible pages. /// This is empty except for a border which acts as a background. /// /// Whether to clear all pages, or only those that are not visible. private void ResetVisualTree(bool pruneOnly) { //We need to dispose and remove any pages that will no longer be in the visual tree. for (int i = _childrenCollection.Count - 1; i >= _firstPageVisualIndex; i--) { DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage; if (dp != null && (!pruneOnly || _rowCache.RowCount == 0 || dp.PageNumber < _firstVisiblePageNumber || dp.PageNumber > _lastVisiblePageNumber)) { //This page will not be visible any longer, so get rid of it. //Remove this page from the Visual tree. _childrenCollection.Remove(dp); //Remove any PageLoaded event handlers dp.PageLoaded -= new EventHandler(OnPageLoaded); //Dispose of the page. ((IDisposable)dp).Dispose(); } } //Create the background if it does not exist. if (_documentGridBackground == null) { //We create a Border with a transparent background so that it can //participate in Hit-Testing (which allows click events like those //for our Context Menu to work). _documentGridBackground = new Border(); _documentGridBackground.Background = Brushes.Transparent; //Add the background in. _childrenCollection.Add(_documentGridBackground); } } ////// Nulls out the PageViews collection and notifies DocumentViewer of the change. /// private void ResetPageViewCollection() { //Null out our collection of PageViews. _pageViews = null; //Tell our parent DocumentViewer that we've updated our PageView collection. InvalidatePageViews(); } #region MakeVisible Helpers ////// Handles the GetPageNumberCompleted event fired as a result of a MakeContentVisibleAsync /// call. At this point we know the page number corresponding to the ContentPosition we need /// to make visible, so we invoke MakeVisibleAsync() to bring it into view. /// /// The sender of this event /// The args associated with this event. /// We expect e.UserState to be a MakeVisibleData. private void OnGetPageNumberCompleted(object sender, GetPageNumberCompletedEventArgs e) { if (e == null) { throw new ArgumentNullException("e"); } //Ensure that the UserState passed with this event contains an //MakeVisibleData object. If not, we ignore it as this event //could have originated from someone else calling GetPageNumberAsync. if (e.UserState is MakeVisibleData) { MakeVisibleData data = (MakeVisibleData)e.UserState; MakeVisibleAsync(data, e.PageNumber); } } ////// Makes the specified object on the specified page visible, which may be an /// asynchronous operation if the page is not already in view. /// /// Data corresponding to the object to be made visible. /// The page number the object is on. private void MakeVisibleAsync(MakeVisibleData data, int pageNumber) { //This page may not be currently visible. //First we need to make the page visible, if necessary. //This will be done at background priority to allow currently-loading pages time to //finish. If we do not do this, then in the corner case of: // 1) Document has just been loaded (for example, just after a hyperlink navigation to the doc) // 2) Document has non 8.5x11-sized pages at the beginning of the document // 3) Navigation is to a page past the initially visible pages. //In this case, the order of operations is something like this: // 1) Initially visible pages start loading // 2) MakeVisible is invoked, and MakePageVisible is called, which uses cached // page info to decide where to scroll to. (8.5x11 is assumed until a page is loaded) // 3) Document is scrolled to position computed in #2 // 4) Pages loading in #1 finish loading, GetPageCompleted is called, PageCache is updated, // and the document layout changes. This will shift the page we actually want to see up or down, // potentially by a substantial amount. // 5) User is now looking at the wrong page, and if the page that the hyperlink target is on isn't // visible at this point then the MakeVisible operation fails in MakeVisibleImpl since the target // isn't in the visual tree. // Everyone got that? So, doing the initial "MakePageVisible" operation at Background priority // allows step #4 above to finish _before_ we do steps 2 and 3, so the right page will be visible // in 5 and the MakeVisible operation will succeed. Dispatcher.BeginInvoke(DispatcherPriority.Background, new BringPageIntoViewCallback( BringPageIntoViewDelegate ), data, pageNumber); } ////// Delegate method used to bring a specified page into view. /// /// /// private void BringPageIntoViewDelegate(MakeVisibleData data, int pageNumber) { //Make the page visible if necessary: // - If our layout is not yet valid // - If the visual being made visible is a FixedPage and // the bring-into-view rect is the entire page // (in which case we always want to move it as close to the top of the // viewport as possible even if it is already partially visible) // - If the page isn't currently visible. if (!_rowCache.HasValidLayout || (data.Visual is FixedPage && data.Visual.VisualContentBounds == data.Rect) || pageNumber < _firstVisiblePageNumber || pageNumber > _lastVisiblePageNumber) { MakePageVisible(pageNumber); } //The page's contents have already been loaded, we can bring the object into view immediately. if (IsPageLoaded(pageNumber)) { MakeVisibleImpl(data); } else { //Now we have to wait for the page to be loaded so that we can //ensure that the object itself is visible. //As pages are loaded, this page will be checked for, and the dispatcher below //executed as appropriate. _makeVisiblePageNeeded = pageNumber; _makeVisibleDispatcher = Dispatcher.BeginInvoke(DispatcherPriority.Inactive, (DispatcherOperationCallback)delegate(object arg) { MakeVisibleImpl((MakeVisibleData)arg); return null; }, data); } } ////// Implementation of MakeVisible logic, the final step in a MakeVisible operation. /// /// private void MakeVisibleImpl(MakeVisibleData data) { if (data.Visual != null) { //Ensure that the passed-in visual is a descendant of DocumentGrid. if (((Visual)this).IsAncestorOf(data.Visual)) { //Now we can determine where this visual is relative to the upper left //corner of the DocumentGrid and thus make it visible. GeneralTransform transform = data.Visual.TransformToAncestor(this); Rect boundingRect = (data.Rect != Rect.Empty) ? data.Rect : data.Visual.VisualContentBounds; Rect offsetRect = transform.TransformBounds(boundingRect); MakeRectVisible(offsetRect, false /* alwaysCenter */); } } else if (data.ContentPosition != null) { ITextPointer tp = data.ContentPosition as ITextPointer; //If we have a valid TextView and the TextPointer is in that TextView //we can make the TextPointer's Rect visible... if (TextViewContains(tp)) { MakeRectVisible(TextView.GetRectangleFromTextPosition(tp), false /* alwaysCenter */); } } else { Invariant.Assert(false, "Invalid object brought into view."); } } ////// Moves the specified rectangle into view, if it isn't already visible. /// /// A rectangle relative to the upper-left corner of the Viewport /// Whether to center the rect at all times or only when necessary. private void MakeRectVisible(Rect r, bool alwaysCenter) { if (r != Rect.Empty) { //Calculate the real position of the rectangle in the document. Rect translatedRect = new Rect(HorizontalOffset + r.X, VerticalOffset + r.Y, r.Width, r.Height); Rect viewportRect = new Rect(HorizontalOffset, VerticalOffset, ViewportWidth, ViewportHeight); //Unless the alwaysCenter flag is set, if the new position is already //visible we don't need to shift the viewport. Otherwise we shift //the offsets so the rect is visible, centering if possible. if (alwaysCenter || !translatedRect.IntersectsWith(viewportRect)) { SetHorizontalOffsetInternal(translatedRect.X - (ViewportWidth / 2.0)); SetVerticalOffsetInternal(translatedRect.Y - (ViewportHeight / 2.0)); } } } ////// Moves the specified IP into view, if it isn't already visible. /// /// A rectangle relative to the upper-left corner of the Viewport which represents /// an IP (Insertion Point) private void MakeIPVisible(Rect r) { if (r != Rect.Empty && TextEditor != null) { Rect viewportRect = new Rect(HorizontalOffset, VerticalOffset, ViewportWidth, ViewportHeight); //If the new position is already fully visible, we don't need to shift the viewport, //otherwise we shift the offsets so the rect is visible, moving as minimally as possible. if (!viewportRect.Contains(r)) { //Scroll left/right if the IP is off the screen Horizontally. if (r.X < HorizontalOffset) { SetHorizontalOffsetInternal(HorizontalOffset - (HorizontalOffset - r.X)); } else if (r.X > HorizontalOffset + ViewportWidth) { SetHorizontalOffsetInternal(HorizontalOffset + (r.X - (HorizontalOffset + ViewportWidth))); } //Scroll up/down if part of the IP is off the screen Vertically. if (r.Y < VerticalOffset) { SetVerticalOffsetInternal(VerticalOffset - (VerticalOffset - r.Y)); } else if (r.Y + r.Height > VerticalOffset + ViewportHeight) { SetVerticalOffsetInternal(VerticalOffset + ((r.Y + r.Height) - (VerticalOffset + ViewportHeight))); } } } } ////// Invokes GetPageNumberAsync on the passed in ContentPosition. /// The handler for GetPageNumberAsync will bring that ContentPosition into view. /// /// The MakeVisibleData to be made visible private void MakeContentPositionVisibleAsync(MakeVisibleData data) { //If the ContentPosition is valid, we can make it visible now. if (data.ContentPosition != null && data.ContentPosition != ContentPosition.Missing) { Content.GetPageNumberAsync(data.ContentPosition, data); } } #endregion MakeVisible Helpers ////// Places a delegate for an SetScale call on the queue. We do this for /// performance reasons, as changing the document scale takes significant time. /// /// private void QueueSetScale(double scale) { //If there's a SetScale operation in the Pending state, then we'll //abort it (we only care that the last operation invoked completes.) if (_setScaleOperation != null && _setScaleOperation.Status == DispatcherOperationStatus.Pending) { _setScaleOperation.Abort(); } _setScaleOperation = Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input, new DispatcherOperationCallback(SetScaleDelegate), scale); } private object SetScaleDelegate(object scale) { if (!(scale is double)) { return null; } double newScale = (double)scale; _documentLayout.ViewMode = ViewMode.Zoom; //Get the current visible selection, if any. //The results of this will determine how we handle the //zoom operation. ITextPointer selection = GetVisibleSelection(); if (selection != null) { //The visible-IP case: //First, we find out what page the IP is on: int selectionPage = GetPageNumberForVisibleSelection(selection); //Then we scale the document: UpdateLayoutScale(newScale); //Now we ensure that the selection page is still //visible: MakePageVisible(selectionPage); //The rest of this process is done asynchronously -- //We wait for LayoutUpdated (which happens after layout //but before rendering) and then make the IP visible. //This will cause the IP to be centered without any //visible flicker. //Attach a LayoutUpdated handler. LayoutUpdated += new EventHandler(OnZoomLayoutUpdated); } else { //The non-visible-IP case: //This is considerably easier. The expected behavior is that //we zoom in on the upper-left corner of the currently-visible //content. This is accomplished by scaling the Vertical and //Horizontal offsets in tandem with the document scale which will //put us approximately where we were before. //Scale the document: UpdateLayoutScale(newScale); } return null; } ////// Updates the Scale applied to our RowCache. /// /// private void UpdateLayoutScale(double scale) { if (!DoubleUtil.AreClose(scale, Scale)) { double oldExtentHeight = ExtentHeight; double oldExtentWidth = ExtentWidth; //Tell our RowCache to rescale the layout. _rowCache.Scale = scale; //Rescale our offsets //Divide the old extents by the new to determine the amount to //scale the offsets double verticalScale = oldExtentHeight == 0.0 ? 1.0 : ExtentHeight / oldExtentHeight; double horizontalScale = oldExtentWidth == 0.0 ? 1.0 : ExtentWidth / oldExtentWidth; //Now we scale the offsets. SetVerticalOffsetInternal(_verticalOffset * verticalScale); SetHorizontalOffsetInternal(_horizontalOffset * horizontalScale); InvalidateMeasure(); //Invalidate the measure of our visual children so that they can //be resized. InvalidateChildMeasure(); //Invalidate our parents' properties InvalidateDocumentScrollInfo(); } } ////// Places a delegate for an UpdateDocumentLayout call on the queue. We do this for /// performance reasons, as changing the document layout takes significant time. /// /// private void QueueUpdateDocumentLayout(DocumentLayout layout) { // Increase the count of pending DocumentLayout delegates _documentLayoutsPending++; Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input, new DispatcherOperationCallback(UpdateDocumentLayoutDelegate), layout); } ////// Asynchronously invokes UpdateDocumentLayout. /// /// ///private object UpdateDocumentLayoutDelegate(object layout) { if (layout is DocumentLayout) { UpdateDocumentLayout((DocumentLayout)layout); } // Decrease the count of pending DocumentLayout delegates _documentLayoutsPending--; return null; } /// /// Updates the current layout of our RowCache to the specified number of /// columns. /// /// private void UpdateDocumentLayout(DocumentLayout layout) { // Check if the layout is for a Vertical or Horizontal offset update, // in which case the value can be changed immediately. if (layout.ViewMode == ViewMode.SetHorizontalOffset) { SetHorizontalOffsetInternal(layout.Offset); return; } else if (layout.ViewMode == ViewMode.SetVerticalOffset) { SetVerticalOffsetInternal(layout.Offset); return; } //Store off the layout in case we need it later. //(For example, if our viewport is (0,0). _documentLayout = layout; //Update MaxPagesAcross _maxPagesAcross = _documentLayout.Columns; //If we have a non (0,0) Viewport then we can calculate a new layout. //Otherwise we set our "Layout Requested" flag so that when we get //a non-zero Viewport size we'll compute the requested layout. if (IsViewportNonzero) { //If this is a Thumbnails layout, we need to calculate how many //columns we should fit on the pivotRow. if (_documentLayout.ViewMode == ViewMode.Thumbnails) { _maxPagesAcross = _documentLayout.Columns = CalculateThumbnailColumns(); } //We need to determine the page that has the active focus so we know what page //to keep visible in the new layout. int pivotPage = GetActiveFocusPage(); //Ask the RowCache to recalculate the layout based //on the specified pivot page and the number of columns //requested. //The RowCache will call us back with a //RowLayoutCompleted event when the layout is complete //and we'll update the scale and invalidate our layout there. _rowCache.RecalcRows(pivotPage, _documentLayout.Columns); _isLayoutRequested = false; } else { _isLayoutRequested = true; } } ////// Calls UpdateLayout with the saved column and view mode parameters, /// if there's a previously requested layout to perform. /// Used when we get a non-zero Viewport size and we've previously requested /// a new Row layout. /// ///A bool indicating whether a new layout was calculated. private bool ExecutePendingLayoutRequests() { if (_isLayoutRequested) { UpdateDocumentLayout( _documentLayout ); return true; } return false; } ////// Set the HorizontalOffset to the value provided, the value will be set immediately. /// /// private void SetHorizontalOffsetInternal(double offset) { if (!DoubleUtil.AreClose(_horizontalOffset, offset)) { if (Double.IsNaN(offset)) { throw new ArgumentOutOfRangeException("offset"); } _horizontalOffset = offset; InvalidateMeasure(); InvalidateDocumentScrollInfo(); UpdateTextView(); } } ////// Set the VerticalOffset to the value provided, the value will be set immediately. /// /// private void SetVerticalOffsetInternal(double offset) { if (!DoubleUtil.AreClose(_verticalOffset, offset)) { if (Double.IsNaN(offset)) { throw new ArgumentOutOfRangeException("offset"); } _verticalOffset = offset; InvalidateMeasure(); InvalidateDocumentScrollInfo(); UpdateTextView(); } } ////// Updates the TextView so that it knows about size and position changes. /// private void UpdateTextView() { MultiPageTextView tv = TextView as MultiPageTextView; if (tv != null) { tv.OnPageLayoutChanged(); } } ////// Calculates the number of columns to fit on one row so that the resultant /// view will approximate a "thumbnail" view. /// The basic idea is that we attempt to fit (and scale) a number of pages on the row /// such that the resultant view given the current Viewport will show as many /// pages as possible, with a lower bound of a 5% zoom. /// We attempt to optimize to minimize wasted space, where possible. This may mean /// that not all pages will be completely displayed, but we favor a better looking /// layout (less dead space) over being able to see every page. /// ///The number of pages to show on the first row. private int CalculateThumbnailColumns() { //If our current Viewport size is zero, we'll just return 1. //(because there always needs to be at least 1 page on a row regardless.) if (!IsViewportNonzero) { return 1; } //If we have no pages, we'll just return 1 //(because there always needs to be at least 1 page on a row regardless.) if (_pageCache.PageCount == 0) { return 1; } //We use the first page of the document as our basis for our calculations. //This means that documents with varying page sizes can potentially have //sub-optimal thumbnail views. Size pageSize = _pageCache.GetPageSize(0); //Calculate the viewport's aspect ratio. double viewportAspect = ViewportWidth / ViewportHeight; //Calculate the maximum number of columns we can lay out on a single row //without needing to scale below our floor of 12.5%. int maxColumns = (int)Math.Floor( ViewportWidth / (CurrentMinimumScale * pageSize.Width + HorizontalPageSpacing)); //Ensure this value isn't greater than the number of pages in the document, //since we can't possibly lay out a row with more than that number of pages in it. maxColumns = Math.Min(maxColumns, _pageCache.PageCount); maxColumns = Math.Min(maxColumns, DocumentViewerConstants.MaximumMaxPagesAcross); //Now we do the following: //We iterate through the possible permutations of row and column //combinations and choose the arrangement of columns that best fits the current //viewport's aspect ratio. int minAspectColumns = 1; //The current optimal number of columns found. double minAspectDiff = Double.MaxValue; //The current optimal aspect ratio match for (int columns = 1; columns <= maxColumns; columns++) { //Calculate the number of rows for this arrangment given the current column count int rows = (int)Math.Floor((double)(_pageCache.PageCount / columns)); //Calculate the approximate dimensions that this layout would //have. double width = pageSize.Width * columns; double height = pageSize.Height * rows; //Determine the aspect ratio of this layout. double layoutAspect = width / height; //See if the aspect ratio of this layout is a closer match for our Viewport //than previous attempts. double aspectDiff = Math.Abs(layoutAspect - viewportAspect); if ( aspectDiff < minAspectDiff) { //It is, so save it. minAspectDiff = aspectDiff; minAspectColumns = columns; } } return minAspectColumns; } ////// Calls InvalidateMeasure() on our visual children in order to force them /// to be re-measured and arranged on the next layout pass. This is called /// whenever the Scale is changed, as it is only then when re-measuring is /// required. We do this to avoid unnecessary layout/measure passes on our /// pages. /// private void InvalidateChildMeasure() { //Get the current Visual Collection, which contains our pages. int count = _childrenCollection.Count; for (int i = 0; i < count; i++) { UIElement page = _childrenCollection[i] as UIElement; if (page != null) { page.InvalidateMeasure(); } } } ////// Helper function that indicates whether all the pages on the specified /// row point to clean cache entries. /// /// ///private bool RowIsClean(RowInfo row) { bool clean = true; for (int i = row.FirstPage; i < row.FirstPage + row.PageCount; i++) { if (_pageCache.IsPageDirty(i)) { clean = false; break; } } return clean; } /// /// Checks that the current scale factor is optimal for the passed in row. /// /// The Row to pass to the Delegate private void EnsureFit(RowInfo pivotRow) { //Get the scale factor necessary to fit this row into view. //If the result is not 1.0 (within a certain margin of error) //then we need to re-layout, alas. double neededScaleFactor = CalculateScaleFactor(pivotRow); double newScale = neededScaleFactor * _rowCache.Scale; //If the neededScaleFactor would require DocumentGrid scale the pages //below the minimum allowed zoom, or above the maximum, then we won't //do anything here. if (newScale < CurrentMinimumScale || newScale > DocumentViewerConstants.MaximumScale) { return; } if (!DoubleUtil.AreClose(1.0, neededScaleFactor)) { //Rescale the row. ApplyViewParameters( pivotRow ); //Make the row visible again -- the offsets may have //changed due to the above rescaling. SetVerticalOffsetInternal(pivotRow.VerticalOffset); } } ////// Given a pivot row and a previously set ViewMode, the scale is adjusted so as /// to cause the pivot row to be fit based on the specified ViewMode. /// /// private void ApplyViewParameters(RowInfo pivotRow) { //Update our MaxPagesAcross property to the number of rows on the pivot row //if page sizes vary. (If page sizes are uniform, this value will not change as a result of //a layout change) if (_pageCache.DynamicPageSizes) { _maxPagesAcross = pivotRow.PageCount; } //Get the scale factor necessary to fit the given row into the Viewport. double scaleFactor = CalculateScaleFactor(pivotRow); //Calculate the new scale. We multiply our scale factor by the old scale factor to cancel out any //previously applied scale. double newScale = scaleFactor * _rowCache.Scale; //Clip the value into the acceptable range newScale = Math.Max(newScale, CurrentMinimumScale); newScale = Math.Min(newScale, DocumentViewerConstants.MaximumScale); //Update the Row Layout's scale. UpdateLayoutScale(newScale); } private double CalculateScaleFactor(RowInfo pivotRow) { //Determine the dimensions of this row minus any spacing between the pages. //We use this as the baseline for our scale factor as page spacing does not scale. double rowWidth; //If the page sizes vary, we use the width of the pivot row, //otherwise we use the overall width of the document (ExtentWidth). //(For uniform page sizes, we always use the width of the document, even //for the last row which may not have the same width as the rest of the document). if (_pageCache.DynamicPageSizes) { rowWidth = pivotRow.RowSize.Width - pivotRow.PageCount * HorizontalPageSpacing; } else { rowWidth = ExtentWidth - MaxPagesAcross * HorizontalPageSpacing; } double rowHeight = pivotRow.RowSize.Height - VerticalPageSpacing; //If we have row dimensions of zero or less, there's no reason to scale anything. //So just return 1.0 to indicate no change. if (rowWidth <= 0.0 || rowHeight <= 0.0) { return 1.0; } //The dimensions of our Viewport minus any spacing. We use this as the baseline for our //scale factor as page spacing does not scale. double compensatedViewportWidth; if (_pageCache.DynamicPageSizes) { compensatedViewportWidth = ViewportWidth - pivotRow.PageCount * HorizontalPageSpacing; } else { compensatedViewportWidth = ViewportWidth - MaxPagesAcross * HorizontalPageSpacing; } double compensatedViewportHeight = ViewportHeight - VerticalPageSpacing; //If we have no space to display pages, there's nothing to scale. //So just return 1.0 to indicate no change. if (compensatedViewportWidth <= 0.0 || compensatedViewportHeight <= 0.0) { return 1.0; } double scaleFactor = 1.0; //Based on the previously determined ViewMode (set in SetColumns(), FitToWidth(), etc.. //scale the pages appropriately. switch (_documentLayout.ViewMode) { case ViewMode.SetColumns: //We leave the scale factor as is -- this is not a "page-fit" mode. break; case ViewMode.FitColumns: //Update the scale factor so that the pivot row is completely visible. scaleFactor = Math.Min(compensatedViewportWidth / rowWidth, compensatedViewportHeight / rowHeight); break; case ViewMode.PageWidth: //Update the scale factor so that the pivot row is as wide as the viewport. scaleFactor = compensatedViewportWidth / rowWidth; break; case ViewMode.PageHeight: //Update the scale factor so that the pivot row is as tall as the viewport. scaleFactor = compensatedViewportHeight / rowHeight; break; case ViewMode.Thumbnails: //Update the scale factor so that the _entire layout_ is completely visible. As in previous //cases we must compensate for the fact that the spacing between pages does not scale. //However, unlike in previous cases, we must exclude the space between all rows rather //merely one space, so we must recalculate the compensated values. Furthermore we must //also compensate for the ExtentHeight as well since it includes the spaces. double thumbnailCompensatedExtentHeight = ExtentHeight - VerticalPageSpacing * _rowCache.RowCount; double thumbnailCompensatedViewportHeight = ViewportHeight - VerticalPageSpacing * _rowCache.RowCount; //If we have no space to display pages, there's nothing to scale. //So just return 1.0 to indicate no change. if (thumbnailCompensatedViewportHeight <= 0.0) { scaleFactor = 1.0; } else { scaleFactor = Math.Min(compensatedViewportWidth / rowWidth, thumbnailCompensatedViewportHeight / thumbnailCompensatedExtentHeight); } break; case ViewMode.Zoom: //We will not change the scale here, as this is not a "page-fit" mode. break; default: throw new InvalidOperationException(SR.Get(SRID.DocumentGridInvalidViewMode)); } return scaleFactor; } ////// Creates the caches used by DocumentGrid, and sets default property values. /// private void Initialize() { //Create our caches _pageCache = new PageCache(); _childrenCollection = new VisualCollection(this); _rowCache = new RowCache(); _rowCache.PageCache = _pageCache; _rowCache.RowCacheChanged += new RowCacheChangedEventHandler(OnRowCacheChanged); _rowCache.RowLayoutCompleted += new RowLayoutCompletedEventHandler(OnRowLayoutCompleted); } ////// Updates Scrolling-related properties and calls /// InvalidateScrollInfo and InvalidateDocumentScrollInfo on the /// ScrollOwner and DocumentScrollOwner, respectively. /// private void InvalidateDocumentScrollInfo() { if (ScrollOwner != null) { ScrollOwner.InvalidateScrollInfo(); } if (DocumentViewerOwner != null) { DocumentViewerOwner.InvalidateDocumentScrollInfo(); } } ////// Calls InvalidatePageViews and ApplyTemplate on our DocumentViewer owner /// so that the base implementation can keep its collection up to date. /// private void InvalidatePageViews() { Invariant.Assert(DocumentViewerOwner != null, "DocumentViewerOwner cannot be null."); if (DocumentViewerOwner != null) { DocumentViewerOwner.InvalidatePageViewsInternal(); DocumentViewerOwner.ApplyTemplate(); } //Perf Tracing - InvalidatePageViews EventTrace.NormalTraceEvent(EventTraceGuidId.DRXINVALIDATEVIEWGUID, MS.Utility.EventType.Info); } ////// Returns an ITextPointer to the current visible selection, if there is one. /// ///An ITextPointer to the current selection, or null if none exists. private ITextPointer GetVisibleSelection() { ITextPointer selection = null; if (HasSelection()) { ITextPointer tp = TextEditor.Selection.Start; //If the TextView contains the selection //then the selection is on a visible page. if (TextViewContains(tp)) { selection = tp; } } return selection; } ////// Indicates whether a selection (visible or not) has been made. /// ///true if a selection has been made, false otherwise. private bool HasSelection() { return (TextEditor != null && TextEditor.Selection != null); } ////// Gets the page number that the specified ITextPointer to a visible selection /// is on. /// /// The TextPointer to find the page number for. ///private int GetPageNumberForVisibleSelection(ITextPointer selection) { Invariant.Assert(TextViewContains(selection)); //Walk through the current DocumentPageViews and see which one contains the selection. foreach (DocumentPageView pageView in _pageViews) { //Get the TextView for this page. DocumentPageTextView textView = ((IServiceProvider)pageView).GetService(typeof(ITextView)) as DocumentPageTextView; //If this TextView contains the selection, return the page's number. if (textView != null && textView.IsValid && textView.Contains(selection)) { return pageView.PageNumber; } } Invariant.Assert(false, "Selection was in TextView, but not found in any visible page!"); return 0; } /// /// Finds the "Active Focus" point: /// Either the page that has a Selection/Insertion Point on it, /// or lacking that, the center of the viewport. /// ///private Point GetActiveFocusPoint() { ITextPointer tp = GetVisibleSelection(); if (tp != null && tp.HasValidLayout) { Rect selectionRect = TextView.GetRectangleFromTextPosition(tp); //If the selection rectangle is not empty, then we have a selection or an IP if (selectionRect != Rect.Empty) { //Return the upper-left corner of the selection. return new Point(selectionRect.Left, selectionRect.Top); } } //No selection, so we default to the upper-left of the viewport. return new Point(0.0, 0.0); } /// /// Returns the page that has "Active Focus," or /// the first visible page if there is none. /// ///private int GetActiveFocusPage() { DocumentPageView dp = GetDocumentPageViewFromPoint(GetActiveFocusPoint()); if (dp != null) { return dp.PageNumber; } //No selection, we default to the first visible page. return _firstVisiblePageNumber; } /// /// Given a point onscreen, returns a DocumentPageView that occupies that point, /// if any. /// /// The point at which to search for a DocumentPageView. ///private DocumentPageView GetDocumentPageViewFromPoint(Point point) { //Hit test to find the DocumentPageView HitTestResult result = VisualTreeHelper.HitTest(this, point); DependencyObject currentVisual = (result != null) ? result.VisualHit : null; DocumentPageView page = null; // Traverse the visual parent chain until we encounter a DocumentPageView. while (currentVisual != null) { page = currentVisual as DocumentPageView; if (page != null) { //We found the DocumentPageView. return page; } currentVisual = VisualTreeHelper.GetParent(currentVisual); } //Didn't find one at this point. return null; } /// /// Helper function to safely verify that the TextView contains a given TextPointer. /// /// The TextPointer to check ///private bool TextViewContains( ITextPointer tp ) { return (TextView != null && TextView.IsValid && TextView.Contains(tp)); } /// /// Helper function that calculates the Horizontal offset of the given page. /// /// The row which the desired page lives on /// The page to find the offset for ///The Horizontal offset of the page in the document. private double GetHorizontalOffsetForPage( RowInfo row, int pageNumber ) { if (row == null) { throw new ArgumentNullException("row"); } if (pageNumber < row.FirstPage || pageNumber > row.FirstPage + row.PageCount) { throw new ArgumentOutOfRangeException("pageNumber"); } //Rows are centered if the content has varying page sizes, //Left-aligned otherwise. double horizontalOffset = _pageCache.DynamicPageSizes ? Math.Max(0.0, (ExtentWidth - row.RowSize.Width) / 2.0) : 0.0; //Add the widths of the pages (and spacing) prior to this one on the row for (int i = row.FirstPage; i < pageNumber; i++) { Size pageSize = _pageCache.GetPageSize(i); horizontalOffset += pageSize.Width * Scale + HorizontalPageSpacing; } return horizontalOffset; } ////// Helper method that determines whether a given RowCacheChange will have /// an impact on currently-visible rows. /// /// ///private bool RowCacheChangeIsVisible(RowCacheChange change) { int firstVisibleRow = _firstVisibleRow; int lastVisibleRow = _firstVisibleRow + _visibleRowCount; int firstChangedRow = change.Start; int lastChangedRow = change.Start + change.Count; //If the first changed row (and hence following changes) are visible OR //The last changed row (and hence prior changes) are visible OR //if the changes are a super-set of the visible range, then the change is visible. if ((firstChangedRow >= firstVisibleRow && firstChangedRow <= lastVisibleRow) || (lastChangedRow >= firstVisibleRow && lastChangedRow <= lastVisibleRow) || (firstChangedRow < firstVisibleRow && lastChangedRow > lastVisibleRow)) { return true; } return false; } /// /// Determines whether the given page's contents have been loaded into the visual tree. /// /// The number of the page to check ///private bool IsPageLoaded(int pageNumber) { DocumentGridPage page = GetDocumentGridPageForPageNumber(pageNumber); if (page != null) { return page.IsPageLoaded; } else { return false; } } /// /// Determines whether every page currently visible has been loaded into the visual tree. /// ///private bool IsViewLoaded() { bool viewIsLoaded = true; for (int i = _firstPageVisualIndex; i < _childrenCollection.Count; i++) { DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage; // Check that this page has been loaded; break if not. if (dp != null && !dp.IsPageLoaded) { viewIsLoaded = false; break; } } return viewIsLoaded; } /// /// Retrieves a DocumentGridPage from our Visual Tree that has the given page number (if one exists). /// /// The number of the page to get ///private DocumentGridPage GetDocumentGridPageForPageNumber(int pageNumber) { for (int i = _firstPageVisualIndex; i < _childrenCollection.Count; i++) { DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage; if (dp != null && dp.PageNumber == pageNumber) { return dp; } } return null; } #region Event Handlers /// /// Handles the RequestBringIntoView routed event in the case where the element to be /// brought into view is DocumentGrid itself, as is the case when the TextEditor's IP moves. /// In this case we use the incoming target rectangle /// and ensure that rect is made visible inside of DocumentGrid. /// /// The sender of this routed event, expected to be a DocumentGrid. /// The RequestBringIntoView event args associated with this event. private static void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs args) { //We only handle this here if the sender and the target of this event are both the same //DocumentGrid. DocumentGrid senderGrid = sender as DocumentGrid; DocumentGrid targetGrid = args.TargetObject as DocumentGrid; if (senderGrid != null && targetGrid != null && senderGrid == targetGrid) { //Bring the IP into view and mark the event as handled. args.Handled = true; targetGrid.MakeIPVisible(args.TargetRect); } else { args.Handled = false; } } ////// This event is fired when our parent ScrollViewer's layout has changed(before render). /// If we need to ensure that a given row has been properly fit -- if ScrollBars have been hidden/ /// made visible due to this change then we may need to resize. We call EnsureFit from here /// to make sure that's done. /// /// /// private void OnScrollChanged(object sender, EventArgs args) { //Remove our handler. if (ScrollOwner != null) { _scrollChangedEventAttached = false; ScrollOwner.ScrollChanged -= new ScrollChangedEventHandler(OnScrollChanged); } //Ensure that our fit is good for the currently displayed row if we have any. if (_rowCache.HasValidLayout) { EnsureFit(_rowCache.GetRowForPageNumber(FirstVisiblePageNumber)); } } ////// This event is fired after a Zoom change when Layout has completed (but before render). /// We make sure the current visible selection is centered onscreen. /// /// /// private void OnZoomLayoutUpdated(object sender, EventArgs args) { //Remove the event handler so we don't get called again. LayoutUpdated -= new EventHandler(OnZoomLayoutUpdated); ITextPointer selection = GetVisibleSelection(); if (selection != null) { //Now we make the selection visible. MakeRectVisible(TextView.GetRectangleFromTextPosition( selection), true /* alwaysCenter */); } } ////// When the RowCache is changed for any reason (due to a new layout, or if pages change, etc...) /// then we need to invalidate our Measure (so we can pick up any changes to visible pages) /// and invalidate our IDocumentScrollInfo parents so they know that something's changed. /// /// /// private void OnRowCacheChanged(object source, RowCacheChangedEventArgs args) { //If: //1) We have a saved pivot row from a previous RowCacheCompleted event, //and //2) We've been told to do a "Page-Fit" operation (that is, a non-zoom viewing preference) //and //3) The pivot row is now "clean" (that is, we know the actual dimensions of all the pages // on the row and we aren't just guessing) //Then we can now officially calculate the scale needed in order to fit the given row in the manner //chosen. if (_savedPivotRow != null && RowIsClean(_savedPivotRow)) { if (_documentLayout.ViewMode != ViewMode.Zoom && _documentLayout.ViewMode != ViewMode.SetColumns ) { if (_savedPivotRow.FirstPage < _rowCache.RowCount) { RowInfo newRow = _rowCache.GetRowForPageNumber(_savedPivotRow.FirstPage); //If the new row's dimensions differ, then we need to rescale, otherwise we do nothing. if (newRow.RowSize.Width != _savedPivotRow.RowSize.Width || newRow.RowSize.Height != _savedPivotRow.RowSize.Height) { //Rescale. ApplyViewParameters(newRow); } //Null out the saved Pivot Row -- we've scaled this row properly now //so we don't need to be concerned with it any longer. _savedPivotRow = null; } } else { // The view is already correct; null out the saved Pivot Row. _savedPivotRow = null; } } //If we're viewing a document with varying page size, we've scrolled since the last layout change //and the Width of the document has increased //then this means that new, wider pages have just been scrolled into view. //If we do nothing here, then the content prior to these new pages will appear to "jump" //to the right (because we center the pages within the width of the document.) //This jump is jarring and not a good user experience. //To prevent this, we adjust the HorizontalOffset such that the content that was previously visible //appears at the same position when it is rendered. if (_pageCache.DynamicPageSizes && _lastRowChangeVerticalOffset != VerticalOffset && _lastRowChangeExtentWidth < ExtentWidth) { if (_lastRowChangeExtentWidth != 0.0) { //Tweak the HorizontalOffset so that the content does not appear to move. SetHorizontalOffsetInternal(HorizontalOffset + (ExtentWidth - _lastRowChangeExtentWidth) / 2.0); } _lastRowChangeExtentWidth = ExtentWidth; } _lastRowChangeVerticalOffset = VerticalOffset; //The row cache has been changed. //If we're displaying rows that were affected, //we need to invalidate our measure so they'll be //redrawn. for (int i = 0; i < args.Changes.Count; i++) { RowCacheChange change = args.Changes[i]; if( RowCacheChangeIsVisible( change )) { InvalidateMeasure(); InvalidateChildMeasure(); } } InvalidateDocumentScrollInfo(); } ////// When a new RowLayout has finished being computed we scale the layout such that it /// fits within our window. /// /// /// private void OnRowLayoutCompleted(object source, RowLayoutCompletedEventArgs args) { if (args == null) { return; } if (args.PivotRowIndex >= _rowCache.RowCount) { throw new ArgumentOutOfRangeException("args"); } //Get the pivot row RowInfo pivotRow = _rowCache.GetRow(args.PivotRowIndex); //If this row is not clean, and we're not applying a //Zoom to the content then we need to rescale the layout when the //pages on this row are retrieved. if (!RowIsClean(pivotRow) && _documentLayout.ViewMode != ViewMode.Zoom) { //Save off this row in case we need to rescale due to //dirty cache entries becoming clean (i.e. page sizes changing //due to the cached size being an inaccurate guess.) //OnRowCacheChanged will check this row to ensure that it gets scaled //properly when all the pages on the row become available. _savedPivotRow = pivotRow; } else { _savedPivotRow = null; } //Now rescale. We do this after checking the cleanliness of the row //so that _savedPivotRow is properly set before we apply our view parameters. //Otherwise the code that relies on it in OnRowCacheChanged (which may be called //as a result of calling ApplyViewParameters) may use the wrong row. ApplyViewParameters(pivotRow); //Now that we've recalculated the row layout, it's time to make the previously-visible //content visible again. //We do not do this the first time the content is assigned, for two reasons, //(similar to the ones described in DocumentViewer.OnDocumentChanged()): // 1) If this is the first assignment, then we're already there by default. // 2) The user may have specified vertical or horizontal offsets in markup or // otherwise () and we need to honor // those settings. if (!_firstRowLayout && !_pageJumpAfterLayout) { MakePageVisible(pivotRow.FirstPage); } else if (_pageJumpAfterLayout) { MakePageVisible(_pageJumpAfterLayoutPageNumber); _pageJumpAfterLayout = false; } _firstRowLayout = false; //If our view was of a "Fit" type, we need to ensure that the fit is //correct -- if the status of Vertical/Horizontal Scrollbars has changed //as a result of our view selection then the Viewport size may have changed. //If so, our current fit is probably wrong. We'll attach a ScrollChanged handler //to our ScrollOwner and when the event is invoked (after layout, but before rendering) //we'll check. //This is "Step 2" of the two-pass layout necessary to do fit properly inside of a //ScrollViewer. if (!_scrollChangedEventAttached && ScrollOwner != null && _documentLayout.ViewMode != ViewMode.Zoom && _documentLayout.ViewMode != ViewMode.SetColumns) { _scrollChangedEventAttached = true; ScrollOwner.ScrollChanged += new ScrollChangedEventHandler(OnScrollChanged); } } #endregion Event Handlers #endregion Private Methods //------------------------------------------------------ // // Private Properties // //----------------------------------------------------- #region Private Properties /// /// Indicates that our Viewport is or is not exactly (0,0). /// ///private bool IsViewportNonzero { get { return (ViewportWidth != 0.0 && ViewportHeight != 0.0); } } /// /// Provides access to DocumentViewer's TextEditor. /// private TextEditor TextEditor { get { if (DocumentViewerOwner != null) { return DocumentViewerOwner.TextEditor; } else { return null; } } } ////// Represents the number of pixels to scroll by when using the /// Mouse Wheel; based on System.Parameters.WheelScrollLines. /// private double MouseWheelVerticalScrollAmount { get { //SystemParameters.WheelScrollLines indicates the number of lines to //scroll when the wheel is moved one "click," we multiply this by //our scroll amount to get the number of pixels to move. return _verticalLineScrollAmount * SystemParameters.WheelScrollLines; } } ////// Represents the number of pixels to scroll by when using the /// Mouse Wheel; based on System.Parameters.WheelScrollLines. /// private double MouseWheelHorizontalScrollAmount { get { //SystemParameters.WheelScrollLines indicates the number of lines to //scroll when the wheel is moved one "click," we multiply this by //our scroll amount to get the number of pixels to move. return _horizontalLineScrollAmount * SystemParameters.WheelScrollLines; } } ////// Returns the minimum allowed scale based on the current view mode. /// Thumbnails mode has a higher minimum than other views. /// private double CurrentMinimumScale { get { return _documentLayout.ViewMode == ViewMode.Thumbnails ? DocumentViewerConstants.MinimumThumbnailsScale : DocumentViewerConstants.MinimumScale; } } #endregion Private Properties //------------------------------------------------------ // // Private Fields // //----------------------------------------------------- #region Private Fields // Our Caches private PageCache _pageCache; private RowCache _rowCache; // Our collection of currently-displayed pages. private ReadOnlyCollection_pageViews; // Data for Properties private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private double _verticalOffset; private double _horizontalOffset; private double _viewportHeight; private double _viewportWidth; private int _firstVisibleRow; private int _visibleRowCount; private int _firstVisiblePageNumber; private int _lastVisiblePageNumber; private ScrollViewer _scrollOwner; private DocumentViewer _documentViewerOwner; private bool _showPageBorders = true; private bool _lockViewModes; private int _maxPagesAcross = 1; // The previous constraint passed to ArrangeCore. private Size _previousConstraint; // The viewing mode (columns, fit, etc...) last used to request a layout. private DocumentLayout _documentLayout = new DocumentLayout(1, ViewMode.SetColumns); private int _documentLayoutsPending; // The pivot rows last used to form the basis of a layout. private RowInfo _savedPivotRow; // The last ExtentWidth and VerticalOffsets encountered in // OnRowCacheChanged, used to determine whether to tweak the HorizontalOffset. private double _lastRowChangeExtentWidth; private double _lastRowChangeVerticalOffset; // Editing private ITextContainer _textContainer; // RubberBand selector used for rubberband selection. private RubberbandSelector _rubberBandSelector; // Flags private bool _isLayoutRequested; //Whether we have requested a layout from the RowCache. private bool _pageJumpAfterLayout; //Whether we need to bring a page into view after layout private int _pageJumpAfterLayoutPageNumber; //The page to jump to after layout private bool _firstRowLayout = true; private bool _scrollChangedEventAttached; //Whether or not we've attached a ScrollChanged event to our ScrollViewer. // We create a Border with a transparent background so that it can // participate in Hit-Testing (which allows click events like those // for our Context Menu to work). This border is displayed behind // the displayed pages so that "dead space" surrounding the pages can // be clicked on. private Border _documentGridBackground; private const int _backgroundVisualIndex = 0; private const int _firstPageVisualIndex = 1; //Constants for MeasureCore constraints //We use this size if we're placed inside a "Size-To-Parent" container like //ScrollViewer or StackPanel and are given Infinite constraints. private readonly Size _defaultConstraint = new Size(250.0, 250.0); //Store all our visual children (pages) here private VisualCollection _childrenCollection; //Information for MakeVisible operations involving pages that are not //yet visible. private int _makeVisiblePageNeeded = -1; private DispatcherOperation _makeVisibleDispatcher; //DispatcherOperations used for executing time-consuming property changes in the background. private DispatcherOperation _setScaleOperation; //Delegate used for BringPageIntoView. private delegate void BringPageIntoViewCallback(MakeVisibleData data, int pageNumber); /// /// Represents a state in the Visual tree merging state machine /// used in RecalcVisiblePages. /// private enum VisualTreeModificationState { ////// Inserting pages before existing /// BeforeExisting = 0, ////// Scanning through existing pages /// DuringExisting, ////// Adding pages after existing /// AfterExisting } ////// Represents a layout mode specified by SetColumns, /// FitToPage, FitToWidth, etc... /// private enum ViewMode { ////// A request to lay out a specified number of columns was made. /// SetColumns = 0, ////// A request to make the specified number of columns visible was made. /// FitColumns, ////// A request for a fit-to-page-width view was made. /// PageWidth, ////// A request for a fit-to-page-height view was made. /// PageHeight, ////// A request for a thumbnail view was made. /// Thumbnails, ////// A request for a non "page-fit" view was made. /// Zoom, ////// A request for the HorizontalOffset to be updated. /// SetHorizontalOffset, ////// A request for the VerticalOffset to be updated. /// SetVerticalOffset } ////// Represents a particular document layout -- /// includes the number of Columns to view and the /// mode to view them in. /// private class DocumentLayout { public DocumentLayout(int columns, ViewMode viewMode) : this(columns, 0.0 /* default */, viewMode) { } public DocumentLayout(double offset, ViewMode viewMode) : this(1 /* default */, offset, viewMode) { } public DocumentLayout(int columns, double offset, ViewMode viewMode) { _columns = columns; _offset = offset; _viewMode = viewMode; } ////// The ViewMode to apply to the layout. /// public ViewMode ViewMode { set { _viewMode = value; } get { return _viewMode; } } ////// The number of columns for the layout. /// public int Columns { set { _columns = value; } get { return _columns; } } ////// The offset (horizontal of vertical) for the layout. /// public double Offset { // Set not currently used. // set { _offset = value; } get { return _offset; } } private ViewMode _viewMode; private int _columns; private double _offset; } ////// An MakeVisibleData object contains data and operation information /// related to asynchronous MakeVisible operations. /// private struct MakeVisibleData { ////// Constructs a new MakeVisibleData object /// /// A visual to be made visible. /// A ContentPosition to be made visible. /// Any bounding rect to be made visible. public MakeVisibleData(Visual visual, ContentPosition contentPosition, Rect rect) { _visual = visual; _contentPosition = contentPosition; _rect = rect; } ////// The Visual to be made visible /// public Visual Visual { get { return _visual; } } ////// The ContentPosition to be made Visible /// public ContentPosition ContentPosition { get { return _contentPosition; } } ////// The bounding rectangle to be made visible /// public Rect Rect { get { return _rect; } } private Visual _visual; private ContentPosition _contentPosition; private Rect _rect; } //Constants for line scrolling amounts private const double _verticalLineScrollAmount = 16.0; private const double _horizontalLineScrollAmount = 16.0; #endregion Private Fields } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------------------- // //// Copyright (C) Microsoft Corporation. All rights reserved. // // // // Description: DocumentGrid displays DocumentPaginator content in a grid-like // arrangement and is used by DocumentViewer to display documents. // // History: // 10/21/04 - jdersch created for complete and utter overhaul for the new // DocumentViewer control. // //--------------------------------------------------------------------------- using MS.Internal; using MS.Internal.Media; using MS.Utility; using MS.Win32; using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; namespace MS.Internal.Documents { ////// DocumentGrid is an internal Avalon FrameworkElement that executes all the /// "heavy lifting" involved in loading and displaying an DocumentPaginator-based /// document inside of a DocumentViewer control. /// ///http://d2/DRX/default.aspx internal class DocumentGrid : FrameworkElement, IDocumentScrollInfo { //----------------------------------------------------- // // Constructors // //----------------------------------------------------- #region Constructors ////// Static constructor /// static DocumentGrid() { //Register for the RequestBringIntoView event so we can get BIV events for //TextEditor IP movements. EventManager.RegisterClassHandler(typeof(DocumentGrid), RequestBringIntoViewEvent, new RequestBringIntoViewEventHandler(OnRequestBringIntoView)); //Register the default ContextMenu DocumentGridContextMenu.RegisterClassHandler(); } ////// The constructor /// public DocumentGrid() : base() { Initialize(); } #endregion Constructors //------------------------------------------------------ // // Internal Methods // //----------------------------------------------------- #region Internal Methods ////// Hit-Test on the multi-page UI scope to return a DocumentPage /// that contains this point /// /// Point in pixel unit, relative to the UI Scope's coordinates ///A DocumentPage that is hit or null if no page is hit internal DocumentPage GetDocumentPageFromPoint(Point point) { DocumentPageView dp = GetDocumentPageViewFromPoint(point); // if we hit a DocumentPageView we can return its DocumentPage. if (dp != null) { return dp.DocumentPage; } //Nothing hit, return null. return null; } #endregion Internal Methods //------------------------------------------------------ // // Public Interfaces // //------------------------------------------------------ #region Interface Implementations #region IDocumentScrollInfo //----------------------------------------------------- // // IDocumentScrollInfo Methods // //------------------------------------------------------ ////// Scroll content by one line to the top. /// public void LineUp() { if (_canVerticallyScroll) { SetVerticalOffsetInternal(VerticalOffset - _verticalLineScrollAmount); } } ////// Scroll content by one line to the bottom. /// public void LineDown() { if (_canVerticallyScroll) { SetVerticalOffsetInternal(VerticalOffset + _verticalLineScrollAmount); //Perf Tracing - Mark LineDown Start if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXLINEDOWNGUID), MS.Utility.EventType.Info, (int)VerticalOffset ); } } } ////// Scroll content by one line to the left. /// public void LineLeft() { if (_canHorizontallyScroll) { SetHorizontalOffsetInternal(HorizontalOffset - _horizontalLineScrollAmount); } } ////// Scroll content by one line to the right. /// public void LineRight() { if (_canHorizontallyScroll) { SetHorizontalOffsetInternal(HorizontalOffset + _horizontalLineScrollAmount); } } ////// Scroll content by one viewport to the top. /// public void PageUp() { SetVerticalOffsetInternal(VerticalOffset - ViewportHeight); } ////// Scroll content by one viewport to the bottom. /// public void PageDown() { SetVerticalOffsetInternal(VerticalOffset + ViewportHeight); //Perf Tracing - Mark PageDown Start if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEDOWNGUID), MS.Utility.EventType.Info, (int)VerticalOffset); } } ////// Scroll content by one viewport to the left. /// public void PageLeft() { SetHorizontalOffsetInternal(HorizontalOffset - ViewportWidth); } ////// Scroll content by one viewport to the right. /// public void PageRight() { SetHorizontalOffsetInternal(HorizontalOffset + ViewportWidth); } ////// Scroll content up via the mousewheel. /// public void MouseWheelUp() { if (_canVerticallyScroll) { SetVerticalOffsetInternal(VerticalOffset - MouseWheelVerticalScrollAmount); } else { PageUp(); } } ////// Scroll content down via the mousewheel. /// public void MouseWheelDown() { if (_canVerticallyScroll) { SetVerticalOffsetInternal(VerticalOffset + MouseWheelVerticalScrollAmount); } else { PageDown(); } } ////// Scroll content left via the mousewheel. /// public void MouseWheelLeft() { if (_canHorizontallyScroll) { SetHorizontalOffsetInternal(HorizontalOffset - MouseWheelHorizontalScrollAmount); } else { PageLeft(); } } ////// Scroll content right via the mousewheel. /// public void MouseWheelRight() { if (_canHorizontallyScroll) { SetHorizontalOffsetInternal(HorizontalOffset + MouseWheelHorizontalScrollAmount); } else { PageRight(); } } ////// Ensures that the specified visual is made visible. /// ////// 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. /// public Rect MakeVisible(Visual v, Rect r) { if (Content != null && v != null) { ContentPosition cp = Content.GetObjectPosition(v); MakeContentPositionVisibleAsync(new MakeVisibleData(v, cp, r)); } return r; } ////// Ensures that the specified object is made visible, given that the page it lives on is already known. /// ////// 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. /// public Rect MakeVisible(object o, Rect r, int pageNumber) { MakeVisibleAsync(new MakeVisibleData(o as Visual, o as ContentPosition, r), pageNumber); return r; } ////// Scrolls the current selection into view. Requests for empty or /// invalid selections will do nothing. /// public void MakeSelectionVisible() { //We can only continue if we have a TextEditor attached... if (TextEditor != null && TextEditor.Selection != null) { //Get the TextPointer for the start of our selection. ITextPointer tp = TextEditor.Selection.Start; //Ensure that the TextPointer we use has gravity set to forwards (or into //the selection) so that the selected text is always displayed. tp = tp.CreatePointer(LogicalDirection.Forward); //If the TextPointer is also a ContentPosition, we can //make that ContentPosition visible. ContentPosition cp = tp as ContentPosition; MakeContentPositionVisibleAsync(new MakeVisibleData(null, cp, Rect.Empty)); } } ////// Scrolls the requested page into view. /// /// The page to make visible. public void MakePageVisible(int pageNumber) { //If we're moving more than one page then this is a "page jump" //and we should log the perf event. if (Math.Abs(pageNumber - _firstVisiblePageNumber) > 1) { //Perf Tracing - Mark Page Jump Start if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEJUMPGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, pageNumber); } } //Clip the offset into range for out-of-range page numbers if (pageNumber < 0 ) { //Clip to the top-left of the document SetVerticalOffsetInternal(0.0d); SetHorizontalOffsetInternal(0.0d); } else if (pageNumber >= _pageCache.PageCount || _rowCache.RowCount == 0) { //If the doc is done loading, then this page is out of range. if (_pageCache.IsPaginationCompleted && _rowCache.HasValidLayout) { //Clip to the bottom-right of the document SetVerticalOffsetInternal(ExtentHeight); SetHorizontalOffsetInternal(ExtentWidth); } else { //The doc is not done loading. //Wait for the page to be laid out and try again. _pageJumpAfterLayout = true; _pageJumpAfterLayoutPageNumber = pageNumber; } } else { //This page is valid, so scroll to it now. RowInfo scrolledRow = _rowCache.GetRowForPageNumber(pageNumber); SetVerticalOffsetInternal(scrolledRow.VerticalOffset); //Calculate the Horizontal offset of the page we're bringing into view: double horizontalOffset = GetHorizontalOffsetForPage(scrolledRow, pageNumber); SetHorizontalOffsetInternal(horizontalOffset); } } ////// Scrolls the next row of pages into view. This differs from /// IScrollInfo?s "PageDown" in that PageDown pages by Viewports /// which may not coincide with page dimensions, whereas /// ScrollToNextRow takes these dimensions into account so that /// precisely the next row of pages is displayed. /// public void ScrollToNextRow() { //We change our vertical offset to be the offset of the next row (if there is one). //If there isn't, we do nothing. int nextRow = _firstVisibleRow + 1; if (nextRow < _rowCache.RowCount) { //Get the next row. RowInfo row = _rowCache.GetRow(nextRow); SetVerticalOffsetInternal(row.VerticalOffset); } } /// Scrolls the previous row of pages into view. This differs from /// IScrollInfo?s "PageUp" in that PageUp pages by Viewports /// which may not coincide with page dimensions, whereas /// ScrollToPreviousRow takes these dimensions into account so that /// precisely the previously row of pages is displayed. public void ScrollToPreviousRow() { //We change our vertical offset to be the offset of the previous row (if there is one). int previousRow = _firstVisibleRow - 1; if (previousRow >= 0 && previousRow < _rowCache.RowCount) { //Get the previous row. RowInfo row = _rowCache.GetRow(previousRow); SetVerticalOffsetInternal(row.VerticalOffset); } } ////// Scrolls to the top of the document. /// public void ScrollToHome() { //We just set the VerticalOffset to 0. SetVerticalOffsetInternal(0); } ////// Scrolls to the bottom of the document. /// public void ScrollToEnd() { //We just set the VerticalOffset to our document's extent. SetVerticalOffsetInternal(ExtentHeight); } ////// Sets the scale factor applied to pages in the document, while /// keeping the "Active Focus" centered. /// /// public void SetScale(double scale) { if (!DoubleUtil.AreClose(scale, Scale)) { if (scale <= 0.0) { throw new ArgumentOutOfRangeException("scale"); } if (!Helper.IsDoubleValid(scale)) { throw new ArgumentOutOfRangeException("scale"); } QueueSetScale(scale); } } ////// Changes the view to the specified number of columns. /// /// public void SetColumns(int columns) { if (columns < 1) { throw new ArgumentOutOfRangeException("columns"); } //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout(new DocumentLayout(columns, ViewMode.SetColumns)); } ////// Changes the view to the specified number of columns. /// /// public void FitColumns(int columns) { if (columns < 1) { throw new ArgumentOutOfRangeException("columns"); } //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout(new DocumentLayout(columns, ViewMode.FitColumns)); } ////// Changes the view to a single page, scaled such that it is as wide as the Viewport. /// public void FitToPageWidth() { //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout( new DocumentLayout(1 /* one column */, ViewMode.PageWidth)); } ////// Changes the view to a single page, scaled such that it is as tall as the Viewport. /// public void FitToPageHeight() { //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout( new DocumentLayout(1 /* one column */, ViewMode.PageHeight)); } ////// Changes the view to ?thumbnail view? which will scale the document /// such that as many pages are visible at once as is possible. /// public void ViewThumbnails() { //Perf Tracing - Mark Layout Change Start EventTrace.NormalTraceEvent(EventTraceGuidId.DRXLAYOUTGUID, MS.Utility.EventType.Info); QueueUpdateDocumentLayout( new DocumentLayout(1 /* one column, arbitrary */, ViewMode.Thumbnails)); } //----------------------------------------------------- // // IDocumentScrollInfo Properties // //----------------------------------------------------- ////// DocumentGrid always scrolls in both dimensions. /// public bool CanHorizontallyScroll { get { return _canHorizontallyScroll; } set { _canHorizontallyScroll = value; } } ////// DocumentGrid always scrolls in both dimensions. /// public bool CanVerticallyScroll { get { return _canVerticallyScroll; } set { _canVerticallyScroll = value; } } ////// ExtentWidth contains the full horizontal range of the scrolled content. /// public double ExtentWidth { get { return _rowCache.ExtentWidth; } } ////// ExtentHeight contains the full vertical range of the scrolled content. /// public double ExtentHeight { get { return _rowCache.ExtentHeight; } } ////// ViewportWidth contains the currently visible horizontal range of the scrolled content. /// public double ViewportWidth { get { return _viewportWidth; } } ////// ViewportHeight contains the currently visible vertical range of the scrolled content. /// public double ViewportHeight { get { return _viewportHeight; } } ////// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible. /// Valid values are inclusively between 0 and public double HorizontalOffset { get { //Clip HorizontalOffset into range. double clippedHorizontalOffset = Math.Min(_horizontalOffset, ExtentWidth - ViewportWidth); clippedHorizontalOffset = Math.Max(clippedHorizontalOffset, 0.0); return clippedHorizontalOffset; } } ///less . /// /// Set the HorizontalOffset. If there are pending layout delegates, then /// this will be processed by a delegate. /// /// public void SetHorizontalOffset(double offset) { if (!DoubleUtil.AreClose(_horizontalOffset,offset)) { if (Double.IsNaN(offset)) { throw new ArgumentOutOfRangeException("offset"); } // If there aren't any pending document layout delegates, then change // the HorizontalOffset immediately, otherwise schedule a delegate for it. if (_documentLayoutsPending == 0) { SetHorizontalOffsetInternal(offset); } else { QueueUpdateDocumentLayout(new DocumentLayout(offset, ViewMode.SetHorizontalOffset)); } } } ////// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible. /// Valid values are inclusively between 0 and public double VerticalOffset { get { //Clip VerticalOffset into range. double clippedVerticalOffset = Math.Min(_verticalOffset, ExtentHeight - ViewportHeight); clippedVerticalOffset = Math.Max(clippedVerticalOffset, 0.0); return clippedVerticalOffset; } } ///less . /// /// Set the VerticalOffset. If there are pending layout delegates, then /// this will be processed by a delegate. /// /// public void SetVerticalOffset(double offset) { if (!DoubleUtil.AreClose(_verticalOffset, offset)) { if (Double.IsNaN(offset)) { throw new ArgumentOutOfRangeException("offset"); } // If there aren't any pending document layout delegates, then change // the VerticalOffset immediately, otherwise schedule a delegate for it. if (_documentLayoutsPending == 0) { SetVerticalOffsetInternal(offset); } else { QueueUpdateDocumentLayout(new DocumentLayout(offset, ViewMode.SetVerticalOffset)); } } } ////// Provides the IDocumentScrollInfo implementer with a content /// tree to be paginated. Developers are free to modify this /// Content at any time (remove, add, modify pages, etc?) /// and the IDocumentScrollInfo implementer is responsible for /// noting the changes and updating as necessary. /// ///The DocumentPaginator to be assigned as the content public DynamicDocumentPaginator Content { get { //_pageCache is guaranteed to be non-null as it's created in the //Constructor. return _pageCache.Content; } set { //_pageCache is guaranteed to be non-null as it's created in the //Constructor. if (value != _pageCache.Content) { //Null out our TextContainer. It will be created as needed. _textContainer = null; //Remove our old events from the content if (_pageCache.Content != null) { _pageCache.Content.GetPageNumberCompleted -= new GetPageNumberCompletedEventHandler(OnGetPageNumberCompleted); } //Remove our ScrollChanged events from our ScrollViewer if (ScrollOwner != null) { ScrollOwner.ScrollChanged -= new ScrollChangedEventHandler(OnScrollChanged); _scrollChangedEventAttached = false; } //Assign the new content _pageCache.Content = value; if (_pageCache.Content != null) { //Add our new events to the content _pageCache.Content.GetPageNumberCompleted += new GetPageNumberCompletedEventHandler(OnGetPageNumberCompleted); } //Clear out our visual collection so that the old pages (pointing to old content) //will be replaced with new ones on the next Measure/Arrange pass. ResetVisualTree(false /*pruneOnly*/); ResetPageViewCollection(); //Reset our visible pages. _firstVisiblePageNumber = 0; _lastVisiblePageNumber = 0; // Perf Tracing - PageVisible Changed if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEVISIBLEGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, _lastVisiblePageNumber); } _lastRowChangeExtentWidth = 0.0; _lastRowChangeVerticalOffset = 0.0; //Cause the new content to be laid out in the same fashion as //the previous content. //If the view is Thumbnails we'll change it to SetColumns //so that the Column count will be maintained. This is done //because we're getting new content which initially has 0 //pages and a Thumbnail view only gives decent results after //the entire content has been loaded; since we don't want to provide a //"jarring" situation (where layout suddenly changes after the content's loaded) //we use SetColumns to maintain the same exact layout as the old content. //(This is consistent with DocumentViewer's overall behavior -- any view setting is //a "one time thing" and isn't recomputed if the content changes.) if (_documentLayout.ViewMode == ViewMode.Thumbnails) { _documentLayout.ViewMode = ViewMode.SetColumns; } QueueUpdateDocumentLayout(_documentLayout); //Invalidate Measure and our IDSI so that properties changed //by the content assignment will be properly updated. InvalidateMeasure(); InvalidateDocumentScrollInfo(); } } } ////// Indicates the number of pages currently in the document. /// ///public int PageCount { get { return _pageCache.PageCount; } } /// /// When queried, FirstVisiblePageNumber returns the first page visible onscreen. /// ///public int FirstVisiblePageNumber { get { return _firstVisiblePageNumber; } } /// /// Returns the current Scale factor applied to the pages given the current settings. /// ///public double Scale { get { return _rowCache.Scale; } } /// /// Returns the current number of Columns of pages displayed given the current settings. /// ///public int MaxPagesAcross { get { return _maxPagesAcross; } } /// /// Specifies the vertical gap between Pages when laid out, in pixel (1/96?) units. /// ///public double VerticalPageSpacing { get { return _rowCache.VerticalPageSpacing; } set { if (!Helper.IsDoubleValid(value)) { throw new ArgumentOutOfRangeException("value"); } _rowCache.VerticalPageSpacing = value; } } /// /// Specifies the horizontal gap between Pages when laid out, in pixel (1/96?) units. /// ///public double HorizontalPageSpacing { get { return _rowCache.HorizontalPageSpacing; } set { if (!Helper.IsDoubleValid(value)) { throw new ArgumentOutOfRangeException("value"); } _rowCache.HorizontalPageSpacing = value; } } /// /// Specifies whether each displayed page should be adorned with a ?Drop Shadow? border or not. /// ///public bool ShowPageBorders { get { return _showPageBorders; } set { if (_showPageBorders != value) { _showPageBorders = value; //Update our pages' ShowPageBorder properties. //Get the current Visual Collection, which contains our pages. int count = _childrenCollection.Count; for (int i = 0; i < count; i++) { DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage; if (dp != null) { dp.ShowPageBorders = _showPageBorders; } } } } } /// /// Specifies whether the last "view mode" related property change should be locked /// for resizing. /// ///public bool LockViewModes { get { return _lockViewModes; } set { _lockViewModes = value; } } /// /// Returns a TextContainer for current content /// ///The content's TextContainer, or null if there is none. public ITextContainer TextContainer { get { if (_textContainer == null) { if (Content != null) { IServiceProvider isp = Content as IServiceProvider; if (isp != null) { _textContainer = (ITextContainer)isp.GetService(typeof(ITextContainer)); } } } return _textContainer; } } ////// Returns the MultiPageTextView for the current content. /// ///public ITextView TextView { get { if (TextEditor != null) { return TextEditor.TextView; } else { return null; } } } /// /// The collection of currently-visible DocumentPageViews. /// public ReadOnlyCollectionPageViews { get { return _pageViews; } } /// /// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependent /// on this IScrollInfo's properties. Implementers of IScrollInfo should call InvalidateScrollInfo() /// on this object when related properties change. /// public ScrollViewer ScrollOwner { get { return _scrollOwner; } set { _scrollOwner = value; InvalidateDocumentScrollInfo(); } } ////// DocumentViewerOwner is the DocumentViewer Control and UI that hosts the IDocumentScrollInfo object. /// This control is dependent on this IDSI?s properties, so implementers of IDSI should call /// InvalidateDocumentScrollInfo() on this object when related properties change so that /// DocumentViewer?s UI will be kept in [....]. This property is analogous to IScrollInfo?s ScrollOwner /// property. /// ///public DocumentViewer DocumentViewerOwner { get { return _documentViewerOwner; } set { _documentViewerOwner = value; } } #endregion IDocumentScrollInfo #endregion Interface Implementations //----------------------------------------------------- // // Protected Methods // //------------------------------------------------------ #region Protected Methods /// /// Derived class must implement to support Visual children. The method must return /// the child at the specified index. Index must be between 0 and GetVisualChildrenCount-1. /// /// By default a Visual does not have any children. /// /// Remark: /// During this virtual call it is not valid to modify the Visual tree. /// protected override Visual GetVisualChild(int index) { if(_childrenCollection == null || index < 0 || index >= _childrenCollection.Count) { throw new ArgumentOutOfRangeException("index", index, SR.Get(SRID.Visual_ArgumentOutOfRange)); } return _childrenCollection[index]; } ////// Derived classes override this property to enable the Visual code to enumerate /// the Visual children. Derived classes need to return the number of children /// from this method. /// /// By default a Visual does not have any children. /// /// Remark: During this virtual method the Visual tree must not be modified. /// protected override int VisualChildrenCount { get { // _childrenCollection cannot be null since its initialized in the constructor return _childrenCollection.Count; } } ////// MeasureOverride is repsonsible for measuring any visible pages to their correct sizes. /// /// The upper bound for child sizes ///protected override Size MeasureOverride(Size constraint) { // If layoutSize is infinity, we need to return our absolute smallest size. // This might happen if we are inside an element which sizes-to-content. // For DocumentGrid, we use a hard coded constraint. if (double.IsInfinity(constraint.Width) || double.IsInfinity(constraint.Height)) { constraint = _defaultConstraint; } //Determine which pages are visible at the current offset given the current constraint. RecalculateVisualPages(VerticalOffset, constraint); //Get our visual children count... int count = _childrenCollection.Count; //Now go through our child collection and measure all the pages to their sizes. for(int i = 0; i < count; i++) { //This should be our background. if (i == _backgroundVisualIndex) { Border background = _childrenCollection[i] as Border; if (background == null) { throw new InvalidOperationException(SR.Get(SRID.DocumentGridVisualTreeContainsNonBorderAsFirstElement)); } //We measure this to the size of our constraint. background.Measure(constraint); } //Otherwise it's a page. else { //Ensure that this is actually a DocumentGridPage. If it is not, //Then someone's been mucking with our VisualTree, so we'll throw. DocumentGridPage page = _childrenCollection[i] as DocumentGridPage; if (page == null) { throw new InvalidOperationException(SR.Get(SRID.DocumentGridVisualTreeContainsNonDocumentGridPage)); } //Get the cached size of this page and scale it to our current scale factor. Size pageSize = _pageCache.GetPageSize(page.PageNumber); pageSize.Width *= Scale; pageSize.Height *= Scale; //Measure the page if necessary. if (!page.IsMeasureValid) { page.Measure(pageSize); //See if the cached size has changed since we Measured. //This can happen if in the course of Measuring the page //a GetPageAsync() calls back immediately with the real page size. //If this happens we need to re-measure the page before we finish here, //otherwise we'll end up with a page that's Measured to one size //and Arranged to another, which looks bad. Size newPageSize = _pageCache.GetPageSize(page.PageNumber); if (newPageSize != Size.Empty) { newPageSize.Width *= Scale; newPageSize.Height *= Scale; if (newPageSize.Width != pageSize.Width || newPageSize.Height != pageSize.Height) { //Measure again. page.Measure(newPageSize); } } } } } return constraint; } /// /// ArrangeOverride is responsible for arranging the previously measured pages in the right order. /// /// The final constraint, inside of which everything must live. ///protected override Size ArrangeOverride(Size arrangeSize) { if (_viewportHeight != arrangeSize.Height || _viewportWidth != arrangeSize.Width) { //Update our Viewport sizes _viewportWidth = arrangeSize.Width; _viewportHeight = arrangeSize.Height; if( LockViewModes && IsViewLoaded()) { if (_firstVisiblePageNumber < _pageCache.PageCount && _rowCache.HasValidLayout) { //If we’re locking the view modes and we have loaded content, then we need to re-apply //the last mode setting since our constraint has changed ApplyViewParameters(_rowCache.GetRowForPageNumber(_firstVisiblePageNumber)); MeasureOverride(arrangeSize); } } UpdateTextView(); } //If we have a non-zero viewport size, we should execute any //requests for layout that may have been made but were unable //to complete due to a zero viewport size. if (IsViewportNonzero) { if (ExecutePendingLayoutRequests()) { //We need to re-do layout (RowCache has changed), so call measure here //to ensure everything's updated accordingly. MeasureOverride(arrangeSize); } } //If our constraint size has changed then we need to //alert our parents so they can update their ViewportWidth/Height properties. if ( _previousConstraint != arrangeSize) { _previousConstraint = arrangeSize; InvalidateDocumentScrollInfo(); } //Now we go through the visible rows and arrange the pages within them. //Get our visual collection count int count = _childrenCollection.Count; //If we have no visual children, there's nothing to arrange //so quit now. if (count == 0) { return arrangeSize; } //Arrange the background first. This is always child 0. //The background takes up the entire constraint. UIElement background =_childrenCollection[_backgroundVisualIndex] as UIElement; background.Arrange(new Rect(new Point(0, 0), arrangeSize)); //The offsets for the current page being arranged. double xOffset; double yOffset; //The current visual child (aka DocumentGridPage) we're arranging. //The first child in our tree is always the background so we start at //1 which is our first page. int visualChild = _firstPageVisualIndex; //Now walk through the visible rows and arrange the pages therein. for (int row = _firstVisibleRow; row < _firstVisibleRow + _visibleRowCount; row++) { //Calculate the position for this row. CalculateRowOffsets(row, out xOffset, out yOffset); //Get the current row. RowInfo currentRow = _rowCache.GetRow(row); //Now we can lay out this row. for (int page = currentRow.FirstPage; page < currentRow.FirstPage + currentRow.PageCount; page++) { //This should never, ever happen so we'll throw if it does. if (visualChild > _childrenCollection.Count - 1) { throw new InvalidOperationException(SR.Get(SRID.DocumentGridVisualTreeOutOfSync)); } //Get the cached size of this page. Size pageSize = _pageCache.GetPageSize(page); //Scale it by our scale factor pageSize.Width *= Scale; pageSize.Height *= Scale; //Arrange the page if necessary UIElement uiPage = _childrenCollection[visualChild] as UIElement; if (uiPage != null) { Point pageOffset; //Move the page to the right place based on the FlowDirection of the content. if (_pageCache.IsContentRightToLeft) { pageOffset = new Point(Math.Max(ViewportWidth, ExtentWidth) - (xOffset + pageSize.Width), yOffset); } else { pageOffset = new Point(xOffset, yOffset); } uiPage.Arrange(new Rect(pageOffset, pageSize)); } else { throw new InvalidOperationException(SR.Get(SRID.DocumentGridVisualTreeContainsNonUIElement)); } //Increment our horizontal offset to point to where the next page should go. xOffset += (pageSize.Width+HorizontalPageSpacing); //Move to the next page. visualChild++; } } // As we scroll we need to keep the AdornerLayer up-to-date. // This ensures that annotation components scroll with the content. AdornerLayer layer = AdornerLayer.GetAdornerLayer(this); if (layer != null && layer.GetAdorners(this) != null) layer.Update(this); return arrangeSize; } /// /// Override the OnPreviewMouseLeftButtonDown method so that we can trap the /// keyboard+mouse events needed for Rubberband selection. /// Clicking the Left mouse button while holding Alt will enable the Rubberband /// selection "mode" until the Left mouse button is again pressed without the /// Alt key held. /// /// The MouseButtonEventArgs associated with this mouse event. protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { //Determine whether either Alt key is being held at this moment. bool altKeyDown = Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt); //If the Alt key is held and we aren't currently in RubberBandSelection mode, //we can create and attach our RubberBandSelector now. //We'll stay in this mode until the mouse is clicked without the Alt key held. if (altKeyDown && _rubberBandSelector == null ) { //See if our content implements IServiceProvider. IServiceProvider serviceProvider = Content as IServiceProvider; if (serviceProvider != null) { //See if our content supports rubber band selection. _rubberBandSelector = serviceProvider.GetService(typeof(RubberbandSelector)) as RubberbandSelector; if (_rubberBandSelector != null) { DocumentViewerOwner.Focus(); // text editor needs to be focused when cleared ITextRange textRange = TextEditor.Selection; textRange.Select(textRange.Start, textRange.Start); //clear selection DocumentViewerOwner.IsSelectionEnabled = false; _rubberBandSelector.AttachRubberbandSelector((FrameworkElement)this); //attach the Rubber band selector. } } } //We got a mouse-down event and the Alt key is not being held, so we revert back //to normal selection mode now. else if (!altKeyDown && _rubberBandSelector != null) { //Detach the Rubberband Selector if (_rubberBandSelector != null) { _rubberBandSelector.DetachRubberbandSelector(); _rubberBandSelector = null; } DocumentViewerOwner.IsSelectionEnabled = true; } } #endregion Protected Methods //----------------------------------------------------- // // Private Methods // //------------------------------------------------------ #region Private Methods ////// Recalculates the set of pages that are currently visible and updates /// DocumentGrid's VisualCollection so it contains them. /// /// The viewport to search for visible pages in. /// The offset in the document to start the search. private void RecalculateVisualPages(double offset, Size constraint) { //Do we actually have any rows in the cache? //If not, we can just clear our visual collection and return. if (_rowCache.RowCount == 0) { ResetVisualTree(false /*pruneOnly*/); ResetPageViewCollection(); _firstVisibleRow = 0; _visibleRowCount = 0; _firstVisiblePageNumber = 0; _lastVisiblePageNumber = 0; // Perf Tracing - PageVisible Changed if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEVISIBLEGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, _lastVisiblePageNumber); } return; } int newFirstVisibleRow = 0; int newVisibleRowCount = 0; //Ask the RowCache for the currently visible rows. _rowCache.GetVisibleRowIndices(offset, offset + constraint.Height, out newFirstVisibleRow, out newVisibleRowCount); //Do we have no visible rows at all? Then clear the Visual collection and return. if (newVisibleRowCount == 0) { ResetVisualTree(false /*pruneOnly*/); ResetPageViewCollection(); _firstVisibleRow = 0; _visibleRowCount = 0; _firstVisiblePageNumber = 0; _lastVisiblePageNumber = 0; // Perf Tracing - PageVisible Changed if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEVISIBLEGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, _lastVisiblePageNumber); } return; } //Now walk through each visible row and compare the pages therein //with the current set of pages in our Visual Collection. //New pages are inserted into the collection, unused pages are removed. //Get the current first and last pages visible. int firstPage = -1; int lastPage = -1; //If we have more visuals than just the background (element 0) //then we have pages, so get the page numbers from them. if (_childrenCollection.Count > _firstPageVisualIndex) { DocumentGridPage firstDp = _childrenCollection[1] as DocumentGridPage; firstPage = firstDp != null ? firstDp.PageNumber : -1; DocumentGridPage lastDp = _childrenCollection[_childrenCollection.Count-1] as DocumentGridPage; lastPage = lastDp != null ? lastDp.PageNumber : -1; } //Update our First & LastVisiblePage properties RowInfo firstRow = _rowCache.GetRow(newFirstVisibleRow); _firstVisiblePageNumber = firstRow.FirstPage; RowInfo lastRow = _rowCache.GetRow(newFirstVisibleRow + newVisibleRowCount - 1); _lastVisiblePageNumber = lastRow.FirstPage + lastRow.PageCount - 1; // Perf Tracing - PageVisible Changed if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGEVISIBLEGUID), MS.Utility.EventType.Info, _firstVisiblePageNumber, _lastVisiblePageNumber); } //Update our cached visible row info (used by Measure/Arrange) _firstVisibleRow = newFirstVisibleRow; _visibleRowCount = newVisibleRowCount; //If IDSI properties have changed (namely the First/LastVisiblePage properties) we invalidate them now. if (_firstVisiblePageNumber != firstPage || _lastVisiblePageNumber != lastPage) { //Create our temporary VisualCollection, which will hold the new list of //visible pages. ArrayList visiblePages = new ArrayList(); //Now walk through the visible rows and add the pages to our temporary list. for (int i = _firstVisibleRow; i < _firstVisibleRow + _visibleRowCount; i++) { //Get the row RowInfo currentRow = _rowCache.GetRow(i); //Walk through the row for (int j = currentRow.FirstPage; j < currentRow.FirstPage + currentRow.PageCount; j++) { //Is this page new? if (j < firstPage || j > lastPage || _childrenCollection.Count <= _firstPageVisualIndex) { //Create a new page and add it to our temporary visual collection. DocumentGridPage dp = new DocumentGridPage(Content); dp.ShowPageBorders = ShowPageBorders; dp.PageNumber = j; //Attach the Loaded event handler dp.PageLoaded += new EventHandler(OnPageLoaded); visiblePages.Add(dp); } else { //This page already exists in our visual collection, so we copy that entry over //from the visual collection instead of creating a new page. //(We start at 1 to skip over the background visual.) visiblePages.Add(_childrenCollection[_firstPageVisualIndex + j - Math.Max(0, firstPage)]); } } } //Copy our new visible page collection over to the VisualCollection and update //the MultiPageTextView's list of visible DocumentPageViews. //First, prune our visual tree so it only contains the set of pages that are visible //before and after the layout change. ResetVisualTree(true /*pruneOnly*/); CollectiondocumentPageViews = new Collection (); //State machine for updating visual collection without removing still-visible pages //from the collection: //We walk through the set of visible pages that we computed above. //We insert new pages before existing pages, and add pages after existing pages. // //We take advantage of the fact that both the current set of pages in the Visual Collection //and the set of to-be-made visible pages in visiblePages are in strictly increasing order //with no gaps between pages. //To better understand how the below works, refer to Fig. A below: // // +-----------------------------+ // +------+ +------+ // | new | Pruned Visual Collection | new | // +------+ (Existing Page Visuals) +------+ // +-----------------------------+ // ^ ^ ^ // | | | // A B C // // [Fig. A: Diagram of states] // //- The routine starts off in state A (BeforeExisting). // At this point we insert any new pages in the visiblePages collection until we // find a page in the visiblePages collection that is also in the Pruned Visual Collection. // This indicates that the set of common unchanged pages has been reached. // The state machine then transitions to state B (DuringExisting). //- The routine stays in B merely iterating through visiblePages until it finds a page // in visiblePages that does not correspond to a page in the Visual Tree. This indicates // that the end of the set of common unchanged pages has been reached; at this point we // add the new page and transition to state C (AfterExisting). //- State C ends when no more pages are left in visiblePages. VisualTreeModificationState state = VisualTreeModificationState.BeforeExisting; //The index pointing to the first common page still in the visual tree after the above pruning. int vcIndex = _firstPageVisualIndex; for (int i = 0; i < visiblePages.Count; i++) { Visual current = (Visual)visiblePages[i]; switch (state) { case VisualTreeModificationState.BeforeExisting: //Keep inserting until we find a page that already exists if (vcIndex < _childrenCollection.Count && _childrenCollection[vcIndex] == current) { //Move to "During" state state = VisualTreeModificationState.DuringExisting; } else { //Insert this page at the current index. _childrenCollection.Insert(vcIndex, current); } //Increment the index into the Visual collection to ensure that it continues //to point to the first common page. vcIndex++; break; case VisualTreeModificationState.DuringExisting: //Leave the visual collection alone until we find a page that isn't in the collection //or run out of pages in the collection. if (vcIndex >= _childrenCollection.Count || _childrenCollection[vcIndex] != current) { //Move to "After" state state = VisualTreeModificationState.AfterExisting; //Append this page to the end. _childrenCollection.Add(current); } //Keep moving through the Visual collection... vcIndex++; break; case VisualTreeModificationState.AfterExisting: //Keep going until the end. _childrenCollection.Add(current); break; } //Add this to the collection of PageViews. documentPageViews.Add(((DocumentGridPage)visiblePages[i]).DocumentPageView); } //Update our collection of PageViews with the current set. _pageViews = new ReadOnlyCollection (documentPageViews); //Tell our parent DocumentViewer that we've updated our PageView collection. InvalidatePageViews(); InvalidateDocumentScrollInfo(); } } /// /// Handles the PageLoaded event for a given page, and kicks off /// BringIntoView actions where necessary. /// /// /// private void OnPageLoaded(object sender, EventArgs args) { DocumentGridPage page = sender as DocumentGridPage; Invariant.Assert(page != null, "Invalid sender for OnPageLoaded event."); //Detach the event handler, we don't need this event any longer. page.PageLoaded -= new EventHandler(OnPageLoaded); //Is there a MakeVisible operation waiting for this page to be loaded? //If so, invoke its dispatcher in the background. if (_makeVisiblePageNeeded == page.PageNumber) { _makeVisiblePageNeeded = -1; _makeVisibleDispatcher.Priority = DispatcherPriority.Background; } // Perf Tracing - PageLoaded if (EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal)) { EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.DRXPAGELOADEDGUID), MS.Utility.EventType.Info, page.PageNumber); } } ////// Calculates the X and Y offsets of the given row based on the current /// Viewport dimensions. /// /// The row to calculate the offsets of /// The X offset of the row /// The Y offset of the row private void CalculateRowOffsets(int row, out double xOffset, out double yOffset) { xOffset = 0.0; yOffset = 0.0; //Get the current row. RowInfo currentRow = _rowCache.GetRow(row); //Figure out the width we'll use to center the pages. //If the ViewportWidth is wider than the document, we use that. Otherwise we center //the content based on the width of the document. double centerWidth = Math.Max(ViewportWidth, ExtentWidth); //Figure out the offset of the upper left corner of this row. //X Coordinate: //If this is the last row in the document and we're viewing //uniformly-sized pages then this row is //always left-aligned (not centered). if (row == _rowCache.RowCount -1 && !_pageCache.DynamicPageSizes) { //This is the last row, so we arrange it such that the left edge of the //page is flush with the left edge of the document. xOffset = (centerWidth - ExtentWidth) / 2.0 + (HorizontalPageSpacing / 2.0) - HorizontalOffset; } else { //Otherwise we center this page inside the document. xOffset = (centerWidth - currentRow.RowSize.Width) / 2.0 + (HorizontalPageSpacing / 2.0) - HorizontalOffset; } //Y Coordinate: if (ExtentHeight > ViewportHeight) { //The document is taller than the viewport, so we just display //the content at the current offset. yOffset = currentRow.VerticalOffset + (VerticalPageSpacing / 2.0) - VerticalOffset; } else { //If the document is shorter than the Viewport we're showing it in, //we center it vertically within the viewport. We do not need to factor in //VerticalOffset as it is always 0.0 in this scenario. yOffset = currentRow.VerticalOffset + (ViewportHeight - ExtentHeight) / 2.0 + (VerticalPageSpacing / 2.0); } } ////// Resets DocumentGrid's visual tree to its initial state or prunes non-visible pages. /// This is empty except for a border which acts as a background. /// /// Whether to clear all pages, or only those that are not visible. private void ResetVisualTree(bool pruneOnly) { //We need to dispose and remove any pages that will no longer be in the visual tree. for (int i = _childrenCollection.Count - 1; i >= _firstPageVisualIndex; i--) { DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage; if (dp != null && (!pruneOnly || _rowCache.RowCount == 0 || dp.PageNumber < _firstVisiblePageNumber || dp.PageNumber > _lastVisiblePageNumber)) { //This page will not be visible any longer, so get rid of it. //Remove this page from the Visual tree. _childrenCollection.Remove(dp); //Remove any PageLoaded event handlers dp.PageLoaded -= new EventHandler(OnPageLoaded); //Dispose of the page. ((IDisposable)dp).Dispose(); } } //Create the background if it does not exist. if (_documentGridBackground == null) { //We create a Border with a transparent background so that it can //participate in Hit-Testing (which allows click events like those //for our Context Menu to work). _documentGridBackground = new Border(); _documentGridBackground.Background = Brushes.Transparent; //Add the background in. _childrenCollection.Add(_documentGridBackground); } } ////// Nulls out the PageViews collection and notifies DocumentViewer of the change. /// private void ResetPageViewCollection() { //Null out our collection of PageViews. _pageViews = null; //Tell our parent DocumentViewer that we've updated our PageView collection. InvalidatePageViews(); } #region MakeVisible Helpers ////// Handles the GetPageNumberCompleted event fired as a result of a MakeContentVisibleAsync /// call. At this point we know the page number corresponding to the ContentPosition we need /// to make visible, so we invoke MakeVisibleAsync() to bring it into view. /// /// The sender of this event /// The args associated with this event. /// We expect e.UserState to be a MakeVisibleData. private void OnGetPageNumberCompleted(object sender, GetPageNumberCompletedEventArgs e) { if (e == null) { throw new ArgumentNullException("e"); } //Ensure that the UserState passed with this event contains an //MakeVisibleData object. If not, we ignore it as this event //could have originated from someone else calling GetPageNumberAsync. if (e.UserState is MakeVisibleData) { MakeVisibleData data = (MakeVisibleData)e.UserState; MakeVisibleAsync(data, e.PageNumber); } } ////// Makes the specified object on the specified page visible, which may be an /// asynchronous operation if the page is not already in view. /// /// Data corresponding to the object to be made visible. /// The page number the object is on. private void MakeVisibleAsync(MakeVisibleData data, int pageNumber) { //This page may not be currently visible. //First we need to make the page visible, if necessary. //This will be done at background priority to allow currently-loading pages time to //finish. If we do not do this, then in the corner case of: // 1) Document has just been loaded (for example, just after a hyperlink navigation to the doc) // 2) Document has non 8.5x11-sized pages at the beginning of the document // 3) Navigation is to a page past the initially visible pages. //In this case, the order of operations is something like this: // 1) Initially visible pages start loading // 2) MakeVisible is invoked, and MakePageVisible is called, which uses cached // page info to decide where to scroll to. (8.5x11 is assumed until a page is loaded) // 3) Document is scrolled to position computed in #2 // 4) Pages loading in #1 finish loading, GetPageCompleted is called, PageCache is updated, // and the document layout changes. This will shift the page we actually want to see up or down, // potentially by a substantial amount. // 5) User is now looking at the wrong page, and if the page that the hyperlink target is on isn't // visible at this point then the MakeVisible operation fails in MakeVisibleImpl since the target // isn't in the visual tree. // Everyone got that? So, doing the initial "MakePageVisible" operation at Background priority // allows step #4 above to finish _before_ we do steps 2 and 3, so the right page will be visible // in 5 and the MakeVisible operation will succeed. Dispatcher.BeginInvoke(DispatcherPriority.Background, new BringPageIntoViewCallback( BringPageIntoViewDelegate ), data, pageNumber); } ////// Delegate method used to bring a specified page into view. /// /// /// private void BringPageIntoViewDelegate(MakeVisibleData data, int pageNumber) { //Make the page visible if necessary: // - If our layout is not yet valid // - If the visual being made visible is a FixedPage and // the bring-into-view rect is the entire page // (in which case we always want to move it as close to the top of the // viewport as possible even if it is already partially visible) // - If the page isn't currently visible. if (!_rowCache.HasValidLayout || (data.Visual is FixedPage && data.Visual.VisualContentBounds == data.Rect) || pageNumber < _firstVisiblePageNumber || pageNumber > _lastVisiblePageNumber) { MakePageVisible(pageNumber); } //The page's contents have already been loaded, we can bring the object into view immediately. if (IsPageLoaded(pageNumber)) { MakeVisibleImpl(data); } else { //Now we have to wait for the page to be loaded so that we can //ensure that the object itself is visible. //As pages are loaded, this page will be checked for, and the dispatcher below //executed as appropriate. _makeVisiblePageNeeded = pageNumber; _makeVisibleDispatcher = Dispatcher.BeginInvoke(DispatcherPriority.Inactive, (DispatcherOperationCallback)delegate(object arg) { MakeVisibleImpl((MakeVisibleData)arg); return null; }, data); } } ////// Implementation of MakeVisible logic, the final step in a MakeVisible operation. /// /// private void MakeVisibleImpl(MakeVisibleData data) { if (data.Visual != null) { //Ensure that the passed-in visual is a descendant of DocumentGrid. if (((Visual)this).IsAncestorOf(data.Visual)) { //Now we can determine where this visual is relative to the upper left //corner of the DocumentGrid and thus make it visible. GeneralTransform transform = data.Visual.TransformToAncestor(this); Rect boundingRect = (data.Rect != Rect.Empty) ? data.Rect : data.Visual.VisualContentBounds; Rect offsetRect = transform.TransformBounds(boundingRect); MakeRectVisible(offsetRect, false /* alwaysCenter */); } } else if (data.ContentPosition != null) { ITextPointer tp = data.ContentPosition as ITextPointer; //If we have a valid TextView and the TextPointer is in that TextView //we can make the TextPointer's Rect visible... if (TextViewContains(tp)) { MakeRectVisible(TextView.GetRectangleFromTextPosition(tp), false /* alwaysCenter */); } } else { Invariant.Assert(false, "Invalid object brought into view."); } } ////// Moves the specified rectangle into view, if it isn't already visible. /// /// A rectangle relative to the upper-left corner of the Viewport /// Whether to center the rect at all times or only when necessary. private void MakeRectVisible(Rect r, bool alwaysCenter) { if (r != Rect.Empty) { //Calculate the real position of the rectangle in the document. Rect translatedRect = new Rect(HorizontalOffset + r.X, VerticalOffset + r.Y, r.Width, r.Height); Rect viewportRect = new Rect(HorizontalOffset, VerticalOffset, ViewportWidth, ViewportHeight); //Unless the alwaysCenter flag is set, if the new position is already //visible we don't need to shift the viewport. Otherwise we shift //the offsets so the rect is visible, centering if possible. if (alwaysCenter || !translatedRect.IntersectsWith(viewportRect)) { SetHorizontalOffsetInternal(translatedRect.X - (ViewportWidth / 2.0)); SetVerticalOffsetInternal(translatedRect.Y - (ViewportHeight / 2.0)); } } } ////// Moves the specified IP into view, if it isn't already visible. /// /// A rectangle relative to the upper-left corner of the Viewport which represents /// an IP (Insertion Point) private void MakeIPVisible(Rect r) { if (r != Rect.Empty && TextEditor != null) { Rect viewportRect = new Rect(HorizontalOffset, VerticalOffset, ViewportWidth, ViewportHeight); //If the new position is already fully visible, we don't need to shift the viewport, //otherwise we shift the offsets so the rect is visible, moving as minimally as possible. if (!viewportRect.Contains(r)) { //Scroll left/right if the IP is off the screen Horizontally. if (r.X < HorizontalOffset) { SetHorizontalOffsetInternal(HorizontalOffset - (HorizontalOffset - r.X)); } else if (r.X > HorizontalOffset + ViewportWidth) { SetHorizontalOffsetInternal(HorizontalOffset + (r.X - (HorizontalOffset + ViewportWidth))); } //Scroll up/down if part of the IP is off the screen Vertically. if (r.Y < VerticalOffset) { SetVerticalOffsetInternal(VerticalOffset - (VerticalOffset - r.Y)); } else if (r.Y + r.Height > VerticalOffset + ViewportHeight) { SetVerticalOffsetInternal(VerticalOffset + ((r.Y + r.Height) - (VerticalOffset + ViewportHeight))); } } } } ////// Invokes GetPageNumberAsync on the passed in ContentPosition. /// The handler for GetPageNumberAsync will bring that ContentPosition into view. /// /// The MakeVisibleData to be made visible private void MakeContentPositionVisibleAsync(MakeVisibleData data) { //If the ContentPosition is valid, we can make it visible now. if (data.ContentPosition != null && data.ContentPosition != ContentPosition.Missing) { Content.GetPageNumberAsync(data.ContentPosition, data); } } #endregion MakeVisible Helpers ////// Places a delegate for an SetScale call on the queue. We do this for /// performance reasons, as changing the document scale takes significant time. /// /// private void QueueSetScale(double scale) { //If there's a SetScale operation in the Pending state, then we'll //abort it (we only care that the last operation invoked completes.) if (_setScaleOperation != null && _setScaleOperation.Status == DispatcherOperationStatus.Pending) { _setScaleOperation.Abort(); } _setScaleOperation = Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input, new DispatcherOperationCallback(SetScaleDelegate), scale); } private object SetScaleDelegate(object scale) { if (!(scale is double)) { return null; } double newScale = (double)scale; _documentLayout.ViewMode = ViewMode.Zoom; //Get the current visible selection, if any. //The results of this will determine how we handle the //zoom operation. ITextPointer selection = GetVisibleSelection(); if (selection != null) { //The visible-IP case: //First, we find out what page the IP is on: int selectionPage = GetPageNumberForVisibleSelection(selection); //Then we scale the document: UpdateLayoutScale(newScale); //Now we ensure that the selection page is still //visible: MakePageVisible(selectionPage); //The rest of this process is done asynchronously -- //We wait for LayoutUpdated (which happens after layout //but before rendering) and then make the IP visible. //This will cause the IP to be centered without any //visible flicker. //Attach a LayoutUpdated handler. LayoutUpdated += new EventHandler(OnZoomLayoutUpdated); } else { //The non-visible-IP case: //This is considerably easier. The expected behavior is that //we zoom in on the upper-left corner of the currently-visible //content. This is accomplished by scaling the Vertical and //Horizontal offsets in tandem with the document scale which will //put us approximately where we were before. //Scale the document: UpdateLayoutScale(newScale); } return null; } ////// Updates the Scale applied to our RowCache. /// /// private void UpdateLayoutScale(double scale) { if (!DoubleUtil.AreClose(scale, Scale)) { double oldExtentHeight = ExtentHeight; double oldExtentWidth = ExtentWidth; //Tell our RowCache to rescale the layout. _rowCache.Scale = scale; //Rescale our offsets //Divide the old extents by the new to determine the amount to //scale the offsets double verticalScale = oldExtentHeight == 0.0 ? 1.0 : ExtentHeight / oldExtentHeight; double horizontalScale = oldExtentWidth == 0.0 ? 1.0 : ExtentWidth / oldExtentWidth; //Now we scale the offsets. SetVerticalOffsetInternal(_verticalOffset * verticalScale); SetHorizontalOffsetInternal(_horizontalOffset * horizontalScale); InvalidateMeasure(); //Invalidate the measure of our visual children so that they can //be resized. InvalidateChildMeasure(); //Invalidate our parents' properties InvalidateDocumentScrollInfo(); } } ////// Places a delegate for an UpdateDocumentLayout call on the queue. We do this for /// performance reasons, as changing the document layout takes significant time. /// /// private void QueueUpdateDocumentLayout(DocumentLayout layout) { // Increase the count of pending DocumentLayout delegates _documentLayoutsPending++; Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input, new DispatcherOperationCallback(UpdateDocumentLayoutDelegate), layout); } ////// Asynchronously invokes UpdateDocumentLayout. /// /// ///private object UpdateDocumentLayoutDelegate(object layout) { if (layout is DocumentLayout) { UpdateDocumentLayout((DocumentLayout)layout); } // Decrease the count of pending DocumentLayout delegates _documentLayoutsPending--; return null; } /// /// Updates the current layout of our RowCache to the specified number of /// columns. /// /// private void UpdateDocumentLayout(DocumentLayout layout) { // Check if the layout is for a Vertical or Horizontal offset update, // in which case the value can be changed immediately. if (layout.ViewMode == ViewMode.SetHorizontalOffset) { SetHorizontalOffsetInternal(layout.Offset); return; } else if (layout.ViewMode == ViewMode.SetVerticalOffset) { SetVerticalOffsetInternal(layout.Offset); return; } //Store off the layout in case we need it later. //(For example, if our viewport is (0,0). _documentLayout = layout; //Update MaxPagesAcross _maxPagesAcross = _documentLayout.Columns; //If we have a non (0,0) Viewport then we can calculate a new layout. //Otherwise we set our "Layout Requested" flag so that when we get //a non-zero Viewport size we'll compute the requested layout. if (IsViewportNonzero) { //If this is a Thumbnails layout, we need to calculate how many //columns we should fit on the pivotRow. if (_documentLayout.ViewMode == ViewMode.Thumbnails) { _maxPagesAcross = _documentLayout.Columns = CalculateThumbnailColumns(); } //We need to determine the page that has the active focus so we know what page //to keep visible in the new layout. int pivotPage = GetActiveFocusPage(); //Ask the RowCache to recalculate the layout based //on the specified pivot page and the number of columns //requested. //The RowCache will call us back with a //RowLayoutCompleted event when the layout is complete //and we'll update the scale and invalidate our layout there. _rowCache.RecalcRows(pivotPage, _documentLayout.Columns); _isLayoutRequested = false; } else { _isLayoutRequested = true; } } ////// Calls UpdateLayout with the saved column and view mode parameters, /// if there's a previously requested layout to perform. /// Used when we get a non-zero Viewport size and we've previously requested /// a new Row layout. /// ///A bool indicating whether a new layout was calculated. private bool ExecutePendingLayoutRequests() { if (_isLayoutRequested) { UpdateDocumentLayout( _documentLayout ); return true; } return false; } ////// Set the HorizontalOffset to the value provided, the value will be set immediately. /// /// private void SetHorizontalOffsetInternal(double offset) { if (!DoubleUtil.AreClose(_horizontalOffset, offset)) { if (Double.IsNaN(offset)) { throw new ArgumentOutOfRangeException("offset"); } _horizontalOffset = offset; InvalidateMeasure(); InvalidateDocumentScrollInfo(); UpdateTextView(); } } ////// Set the VerticalOffset to the value provided, the value will be set immediately. /// /// private void SetVerticalOffsetInternal(double offset) { if (!DoubleUtil.AreClose(_verticalOffset, offset)) { if (Double.IsNaN(offset)) { throw new ArgumentOutOfRangeException("offset"); } _verticalOffset = offset; InvalidateMeasure(); InvalidateDocumentScrollInfo(); UpdateTextView(); } } ////// Updates the TextView so that it knows about size and position changes. /// private void UpdateTextView() { MultiPageTextView tv = TextView as MultiPageTextView; if (tv != null) { tv.OnPageLayoutChanged(); } } ////// Calculates the number of columns to fit on one row so that the resultant /// view will approximate a "thumbnail" view. /// The basic idea is that we attempt to fit (and scale) a number of pages on the row /// such that the resultant view given the current Viewport will show as many /// pages as possible, with a lower bound of a 5% zoom. /// We attempt to optimize to minimize wasted space, where possible. This may mean /// that not all pages will be completely displayed, but we favor a better looking /// layout (less dead space) over being able to see every page. /// ///The number of pages to show on the first row. private int CalculateThumbnailColumns() { //If our current Viewport size is zero, we'll just return 1. //(because there always needs to be at least 1 page on a row regardless.) if (!IsViewportNonzero) { return 1; } //If we have no pages, we'll just return 1 //(because there always needs to be at least 1 page on a row regardless.) if (_pageCache.PageCount == 0) { return 1; } //We use the first page of the document as our basis for our calculations. //This means that documents with varying page sizes can potentially have //sub-optimal thumbnail views. Size pageSize = _pageCache.GetPageSize(0); //Calculate the viewport's aspect ratio. double viewportAspect = ViewportWidth / ViewportHeight; //Calculate the maximum number of columns we can lay out on a single row //without needing to scale below our floor of 12.5%. int maxColumns = (int)Math.Floor( ViewportWidth / (CurrentMinimumScale * pageSize.Width + HorizontalPageSpacing)); //Ensure this value isn't greater than the number of pages in the document, //since we can't possibly lay out a row with more than that number of pages in it. maxColumns = Math.Min(maxColumns, _pageCache.PageCount); maxColumns = Math.Min(maxColumns, DocumentViewerConstants.MaximumMaxPagesAcross); //Now we do the following: //We iterate through the possible permutations of row and column //combinations and choose the arrangement of columns that best fits the current //viewport's aspect ratio. int minAspectColumns = 1; //The current optimal number of columns found. double minAspectDiff = Double.MaxValue; //The current optimal aspect ratio match for (int columns = 1; columns <= maxColumns; columns++) { //Calculate the number of rows for this arrangment given the current column count int rows = (int)Math.Floor((double)(_pageCache.PageCount / columns)); //Calculate the approximate dimensions that this layout would //have. double width = pageSize.Width * columns; double height = pageSize.Height * rows; //Determine the aspect ratio of this layout. double layoutAspect = width / height; //See if the aspect ratio of this layout is a closer match for our Viewport //than previous attempts. double aspectDiff = Math.Abs(layoutAspect - viewportAspect); if ( aspectDiff < minAspectDiff) { //It is, so save it. minAspectDiff = aspectDiff; minAspectColumns = columns; } } return minAspectColumns; } ////// Calls InvalidateMeasure() on our visual children in order to force them /// to be re-measured and arranged on the next layout pass. This is called /// whenever the Scale is changed, as it is only then when re-measuring is /// required. We do this to avoid unnecessary layout/measure passes on our /// pages. /// private void InvalidateChildMeasure() { //Get the current Visual Collection, which contains our pages. int count = _childrenCollection.Count; for (int i = 0; i < count; i++) { UIElement page = _childrenCollection[i] as UIElement; if (page != null) { page.InvalidateMeasure(); } } } ////// Helper function that indicates whether all the pages on the specified /// row point to clean cache entries. /// /// ///private bool RowIsClean(RowInfo row) { bool clean = true; for (int i = row.FirstPage; i < row.FirstPage + row.PageCount; i++) { if (_pageCache.IsPageDirty(i)) { clean = false; break; } } return clean; } /// /// Checks that the current scale factor is optimal for the passed in row. /// /// The Row to pass to the Delegate private void EnsureFit(RowInfo pivotRow) { //Get the scale factor necessary to fit this row into view. //If the result is not 1.0 (within a certain margin of error) //then we need to re-layout, alas. double neededScaleFactor = CalculateScaleFactor(pivotRow); double newScale = neededScaleFactor * _rowCache.Scale; //If the neededScaleFactor would require DocumentGrid scale the pages //below the minimum allowed zoom, or above the maximum, then we won't //do anything here. if (newScale < CurrentMinimumScale || newScale > DocumentViewerConstants.MaximumScale) { return; } if (!DoubleUtil.AreClose(1.0, neededScaleFactor)) { //Rescale the row. ApplyViewParameters( pivotRow ); //Make the row visible again -- the offsets may have //changed due to the above rescaling. SetVerticalOffsetInternal(pivotRow.VerticalOffset); } } ////// Given a pivot row and a previously set ViewMode, the scale is adjusted so as /// to cause the pivot row to be fit based on the specified ViewMode. /// /// private void ApplyViewParameters(RowInfo pivotRow) { //Update our MaxPagesAcross property to the number of rows on the pivot row //if page sizes vary. (If page sizes are uniform, this value will not change as a result of //a layout change) if (_pageCache.DynamicPageSizes) { _maxPagesAcross = pivotRow.PageCount; } //Get the scale factor necessary to fit the given row into the Viewport. double scaleFactor = CalculateScaleFactor(pivotRow); //Calculate the new scale. We multiply our scale factor by the old scale factor to cancel out any //previously applied scale. double newScale = scaleFactor * _rowCache.Scale; //Clip the value into the acceptable range newScale = Math.Max(newScale, CurrentMinimumScale); newScale = Math.Min(newScale, DocumentViewerConstants.MaximumScale); //Update the Row Layout's scale. UpdateLayoutScale(newScale); } private double CalculateScaleFactor(RowInfo pivotRow) { //Determine the dimensions of this row minus any spacing between the pages. //We use this as the baseline for our scale factor as page spacing does not scale. double rowWidth; //If the page sizes vary, we use the width of the pivot row, //otherwise we use the overall width of the document (ExtentWidth). //(For uniform page sizes, we always use the width of the document, even //for the last row which may not have the same width as the rest of the document). if (_pageCache.DynamicPageSizes) { rowWidth = pivotRow.RowSize.Width - pivotRow.PageCount * HorizontalPageSpacing; } else { rowWidth = ExtentWidth - MaxPagesAcross * HorizontalPageSpacing; } double rowHeight = pivotRow.RowSize.Height - VerticalPageSpacing; //If we have row dimensions of zero or less, there's no reason to scale anything. //So just return 1.0 to indicate no change. if (rowWidth <= 0.0 || rowHeight <= 0.0) { return 1.0; } //The dimensions of our Viewport minus any spacing. We use this as the baseline for our //scale factor as page spacing does not scale. double compensatedViewportWidth; if (_pageCache.DynamicPageSizes) { compensatedViewportWidth = ViewportWidth - pivotRow.PageCount * HorizontalPageSpacing; } else { compensatedViewportWidth = ViewportWidth - MaxPagesAcross * HorizontalPageSpacing; } double compensatedViewportHeight = ViewportHeight - VerticalPageSpacing; //If we have no space to display pages, there's nothing to scale. //So just return 1.0 to indicate no change. if (compensatedViewportWidth <= 0.0 || compensatedViewportHeight <= 0.0) { return 1.0; } double scaleFactor = 1.0; //Based on the previously determined ViewMode (set in SetColumns(), FitToWidth(), etc.. //scale the pages appropriately. switch (_documentLayout.ViewMode) { case ViewMode.SetColumns: //We leave the scale factor as is -- this is not a "page-fit" mode. break; case ViewMode.FitColumns: //Update the scale factor so that the pivot row is completely visible. scaleFactor = Math.Min(compensatedViewportWidth / rowWidth, compensatedViewportHeight / rowHeight); break; case ViewMode.PageWidth: //Update the scale factor so that the pivot row is as wide as the viewport. scaleFactor = compensatedViewportWidth / rowWidth; break; case ViewMode.PageHeight: //Update the scale factor so that the pivot row is as tall as the viewport. scaleFactor = compensatedViewportHeight / rowHeight; break; case ViewMode.Thumbnails: //Update the scale factor so that the _entire layout_ is completely visible. As in previous //cases we must compensate for the fact that the spacing between pages does not scale. //However, unlike in previous cases, we must exclude the space between all rows rather //merely one space, so we must recalculate the compensated values. Furthermore we must //also compensate for the ExtentHeight as well since it includes the spaces. double thumbnailCompensatedExtentHeight = ExtentHeight - VerticalPageSpacing * _rowCache.RowCount; double thumbnailCompensatedViewportHeight = ViewportHeight - VerticalPageSpacing * _rowCache.RowCount; //If we have no space to display pages, there's nothing to scale. //So just return 1.0 to indicate no change. if (thumbnailCompensatedViewportHeight <= 0.0) { scaleFactor = 1.0; } else { scaleFactor = Math.Min(compensatedViewportWidth / rowWidth, thumbnailCompensatedViewportHeight / thumbnailCompensatedExtentHeight); } break; case ViewMode.Zoom: //We will not change the scale here, as this is not a "page-fit" mode. break; default: throw new InvalidOperationException(SR.Get(SRID.DocumentGridInvalidViewMode)); } return scaleFactor; } ////// Creates the caches used by DocumentGrid, and sets default property values. /// private void Initialize() { //Create our caches _pageCache = new PageCache(); _childrenCollection = new VisualCollection(this); _rowCache = new RowCache(); _rowCache.PageCache = _pageCache; _rowCache.RowCacheChanged += new RowCacheChangedEventHandler(OnRowCacheChanged); _rowCache.RowLayoutCompleted += new RowLayoutCompletedEventHandler(OnRowLayoutCompleted); } ////// Updates Scrolling-related properties and calls /// InvalidateScrollInfo and InvalidateDocumentScrollInfo on the /// ScrollOwner and DocumentScrollOwner, respectively. /// private void InvalidateDocumentScrollInfo() { if (ScrollOwner != null) { ScrollOwner.InvalidateScrollInfo(); } if (DocumentViewerOwner != null) { DocumentViewerOwner.InvalidateDocumentScrollInfo(); } } ////// Calls InvalidatePageViews and ApplyTemplate on our DocumentViewer owner /// so that the base implementation can keep its collection up to date. /// private void InvalidatePageViews() { Invariant.Assert(DocumentViewerOwner != null, "DocumentViewerOwner cannot be null."); if (DocumentViewerOwner != null) { DocumentViewerOwner.InvalidatePageViewsInternal(); DocumentViewerOwner.ApplyTemplate(); } //Perf Tracing - InvalidatePageViews EventTrace.NormalTraceEvent(EventTraceGuidId.DRXINVALIDATEVIEWGUID, MS.Utility.EventType.Info); } ////// Returns an ITextPointer to the current visible selection, if there is one. /// ///An ITextPointer to the current selection, or null if none exists. private ITextPointer GetVisibleSelection() { ITextPointer selection = null; if (HasSelection()) { ITextPointer tp = TextEditor.Selection.Start; //If the TextView contains the selection //then the selection is on a visible page. if (TextViewContains(tp)) { selection = tp; } } return selection; } ////// Indicates whether a selection (visible or not) has been made. /// ///true if a selection has been made, false otherwise. private bool HasSelection() { return (TextEditor != null && TextEditor.Selection != null); } ////// Gets the page number that the specified ITextPointer to a visible selection /// is on. /// /// The TextPointer to find the page number for. ///private int GetPageNumberForVisibleSelection(ITextPointer selection) { Invariant.Assert(TextViewContains(selection)); //Walk through the current DocumentPageViews and see which one contains the selection. foreach (DocumentPageView pageView in _pageViews) { //Get the TextView for this page. DocumentPageTextView textView = ((IServiceProvider)pageView).GetService(typeof(ITextView)) as DocumentPageTextView; //If this TextView contains the selection, return the page's number. if (textView != null && textView.IsValid && textView.Contains(selection)) { return pageView.PageNumber; } } Invariant.Assert(false, "Selection was in TextView, but not found in any visible page!"); return 0; } /// /// Finds the "Active Focus" point: /// Either the page that has a Selection/Insertion Point on it, /// or lacking that, the center of the viewport. /// ///private Point GetActiveFocusPoint() { ITextPointer tp = GetVisibleSelection(); if (tp != null && tp.HasValidLayout) { Rect selectionRect = TextView.GetRectangleFromTextPosition(tp); //If the selection rectangle is not empty, then we have a selection or an IP if (selectionRect != Rect.Empty) { //Return the upper-left corner of the selection. return new Point(selectionRect.Left, selectionRect.Top); } } //No selection, so we default to the upper-left of the viewport. return new Point(0.0, 0.0); } /// /// Returns the page that has "Active Focus," or /// the first visible page if there is none. /// ///private int GetActiveFocusPage() { DocumentPageView dp = GetDocumentPageViewFromPoint(GetActiveFocusPoint()); if (dp != null) { return dp.PageNumber; } //No selection, we default to the first visible page. return _firstVisiblePageNumber; } /// /// Given a point onscreen, returns a DocumentPageView that occupies that point, /// if any. /// /// The point at which to search for a DocumentPageView. ///private DocumentPageView GetDocumentPageViewFromPoint(Point point) { //Hit test to find the DocumentPageView HitTestResult result = VisualTreeHelper.HitTest(this, point); DependencyObject currentVisual = (result != null) ? result.VisualHit : null; DocumentPageView page = null; // Traverse the visual parent chain until we encounter a DocumentPageView. while (currentVisual != null) { page = currentVisual as DocumentPageView; if (page != null) { //We found the DocumentPageView. return page; } currentVisual = VisualTreeHelper.GetParent(currentVisual); } //Didn't find one at this point. return null; } /// /// Helper function to safely verify that the TextView contains a given TextPointer. /// /// The TextPointer to check ///private bool TextViewContains( ITextPointer tp ) { return (TextView != null && TextView.IsValid && TextView.Contains(tp)); } /// /// Helper function that calculates the Horizontal offset of the given page. /// /// The row which the desired page lives on /// The page to find the offset for ///The Horizontal offset of the page in the document. private double GetHorizontalOffsetForPage( RowInfo row, int pageNumber ) { if (row == null) { throw new ArgumentNullException("row"); } if (pageNumber < row.FirstPage || pageNumber > row.FirstPage + row.PageCount) { throw new ArgumentOutOfRangeException("pageNumber"); } //Rows are centered if the content has varying page sizes, //Left-aligned otherwise. double horizontalOffset = _pageCache.DynamicPageSizes ? Math.Max(0.0, (ExtentWidth - row.RowSize.Width) / 2.0) : 0.0; //Add the widths of the pages (and spacing) prior to this one on the row for (int i = row.FirstPage; i < pageNumber; i++) { Size pageSize = _pageCache.GetPageSize(i); horizontalOffset += pageSize.Width * Scale + HorizontalPageSpacing; } return horizontalOffset; } ////// Helper method that determines whether a given RowCacheChange will have /// an impact on currently-visible rows. /// /// ///private bool RowCacheChangeIsVisible(RowCacheChange change) { int firstVisibleRow = _firstVisibleRow; int lastVisibleRow = _firstVisibleRow + _visibleRowCount; int firstChangedRow = change.Start; int lastChangedRow = change.Start + change.Count; //If the first changed row (and hence following changes) are visible OR //The last changed row (and hence prior changes) are visible OR //if the changes are a super-set of the visible range, then the change is visible. if ((firstChangedRow >= firstVisibleRow && firstChangedRow <= lastVisibleRow) || (lastChangedRow >= firstVisibleRow && lastChangedRow <= lastVisibleRow) || (firstChangedRow < firstVisibleRow && lastChangedRow > lastVisibleRow)) { return true; } return false; } /// /// Determines whether the given page's contents have been loaded into the visual tree. /// /// The number of the page to check ///private bool IsPageLoaded(int pageNumber) { DocumentGridPage page = GetDocumentGridPageForPageNumber(pageNumber); if (page != null) { return page.IsPageLoaded; } else { return false; } } /// /// Determines whether every page currently visible has been loaded into the visual tree. /// ///private bool IsViewLoaded() { bool viewIsLoaded = true; for (int i = _firstPageVisualIndex; i < _childrenCollection.Count; i++) { DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage; // Check that this page has been loaded; break if not. if (dp != null && !dp.IsPageLoaded) { viewIsLoaded = false; break; } } return viewIsLoaded; } /// /// Retrieves a DocumentGridPage from our Visual Tree that has the given page number (if one exists). /// /// The number of the page to get ///private DocumentGridPage GetDocumentGridPageForPageNumber(int pageNumber) { for (int i = _firstPageVisualIndex; i < _childrenCollection.Count; i++) { DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage; if (dp != null && dp.PageNumber == pageNumber) { return dp; } } return null; } #region Event Handlers /// /// Handles the RequestBringIntoView routed event in the case where the element to be /// brought into view is DocumentGrid itself, as is the case when the TextEditor's IP moves. /// In this case we use the incoming target rectangle /// and ensure that rect is made visible inside of DocumentGrid. /// /// The sender of this routed event, expected to be a DocumentGrid. /// The RequestBringIntoView event args associated with this event. private static void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs args) { //We only handle this here if the sender and the target of this event are both the same //DocumentGrid. DocumentGrid senderGrid = sender as DocumentGrid; DocumentGrid targetGrid = args.TargetObject as DocumentGrid; if (senderGrid != null && targetGrid != null && senderGrid == targetGrid) { //Bring the IP into view and mark the event as handled. args.Handled = true; targetGrid.MakeIPVisible(args.TargetRect); } else { args.Handled = false; } } ////// This event is fired when our parent ScrollViewer's layout has changed(before render). /// If we need to ensure that a given row has been properly fit -- if ScrollBars have been hidden/ /// made visible due to this change then we may need to resize. We call EnsureFit from here /// to make sure that's done. /// /// /// private void OnScrollChanged(object sender, EventArgs args) { //Remove our handler. if (ScrollOwner != null) { _scrollChangedEventAttached = false; ScrollOwner.ScrollChanged -= new ScrollChangedEventHandler(OnScrollChanged); } //Ensure that our fit is good for the currently displayed row if we have any. if (_rowCache.HasValidLayout) { EnsureFit(_rowCache.GetRowForPageNumber(FirstVisiblePageNumber)); } } ////// This event is fired after a Zoom change when Layout has completed (but before render). /// We make sure the current visible selection is centered onscreen. /// /// /// private void OnZoomLayoutUpdated(object sender, EventArgs args) { //Remove the event handler so we don't get called again. LayoutUpdated -= new EventHandler(OnZoomLayoutUpdated); ITextPointer selection = GetVisibleSelection(); if (selection != null) { //Now we make the selection visible. MakeRectVisible(TextView.GetRectangleFromTextPosition( selection), true /* alwaysCenter */); } } ////// When the RowCache is changed for any reason (due to a new layout, or if pages change, etc...) /// then we need to invalidate our Measure (so we can pick up any changes to visible pages) /// and invalidate our IDocumentScrollInfo parents so they know that something's changed. /// /// /// private void OnRowCacheChanged(object source, RowCacheChangedEventArgs args) { //If: //1) We have a saved pivot row from a previous RowCacheCompleted event, //and //2) We've been told to do a "Page-Fit" operation (that is, a non-zoom viewing preference) //and //3) The pivot row is now "clean" (that is, we know the actual dimensions of all the pages // on the row and we aren't just guessing) //Then we can now officially calculate the scale needed in order to fit the given row in the manner //chosen. if (_savedPivotRow != null && RowIsClean(_savedPivotRow)) { if (_documentLayout.ViewMode != ViewMode.Zoom && _documentLayout.ViewMode != ViewMode.SetColumns ) { if (_savedPivotRow.FirstPage < _rowCache.RowCount) { RowInfo newRow = _rowCache.GetRowForPageNumber(_savedPivotRow.FirstPage); //If the new row's dimensions differ, then we need to rescale, otherwise we do nothing. if (newRow.RowSize.Width != _savedPivotRow.RowSize.Width || newRow.RowSize.Height != _savedPivotRow.RowSize.Height) { //Rescale. ApplyViewParameters(newRow); } //Null out the saved Pivot Row -- we've scaled this row properly now //so we don't need to be concerned with it any longer. _savedPivotRow = null; } } else { // The view is already correct; null out the saved Pivot Row. _savedPivotRow = null; } } //If we're viewing a document with varying page size, we've scrolled since the last layout change //and the Width of the document has increased //then this means that new, wider pages have just been scrolled into view. //If we do nothing here, then the content prior to these new pages will appear to "jump" //to the right (because we center the pages within the width of the document.) //This jump is jarring and not a good user experience. //To prevent this, we adjust the HorizontalOffset such that the content that was previously visible //appears at the same position when it is rendered. if (_pageCache.DynamicPageSizes && _lastRowChangeVerticalOffset != VerticalOffset && _lastRowChangeExtentWidth < ExtentWidth) { if (_lastRowChangeExtentWidth != 0.0) { //Tweak the HorizontalOffset so that the content does not appear to move. SetHorizontalOffsetInternal(HorizontalOffset + (ExtentWidth - _lastRowChangeExtentWidth) / 2.0); } _lastRowChangeExtentWidth = ExtentWidth; } _lastRowChangeVerticalOffset = VerticalOffset; //The row cache has been changed. //If we're displaying rows that were affected, //we need to invalidate our measure so they'll be //redrawn. for (int i = 0; i < args.Changes.Count; i++) { RowCacheChange change = args.Changes[i]; if( RowCacheChangeIsVisible( change )) { InvalidateMeasure(); InvalidateChildMeasure(); } } InvalidateDocumentScrollInfo(); } ////// When a new RowLayout has finished being computed we scale the layout such that it /// fits within our window. /// /// /// private void OnRowLayoutCompleted(object source, RowLayoutCompletedEventArgs args) { if (args == null) { return; } if (args.PivotRowIndex >= _rowCache.RowCount) { throw new ArgumentOutOfRangeException("args"); } //Get the pivot row RowInfo pivotRow = _rowCache.GetRow(args.PivotRowIndex); //If this row is not clean, and we're not applying a //Zoom to the content then we need to rescale the layout when the //pages on this row are retrieved. if (!RowIsClean(pivotRow) && _documentLayout.ViewMode != ViewMode.Zoom) { //Save off this row in case we need to rescale due to //dirty cache entries becoming clean (i.e. page sizes changing //due to the cached size being an inaccurate guess.) //OnRowCacheChanged will check this row to ensure that it gets scaled //properly when all the pages on the row become available. _savedPivotRow = pivotRow; } else { _savedPivotRow = null; } //Now rescale. We do this after checking the cleanliness of the row //so that _savedPivotRow is properly set before we apply our view parameters. //Otherwise the code that relies on it in OnRowCacheChanged (which may be called //as a result of calling ApplyViewParameters) may use the wrong row. ApplyViewParameters(pivotRow); //Now that we've recalculated the row layout, it's time to make the previously-visible //content visible again. //We do not do this the first time the content is assigned, for two reasons, //(similar to the ones described in DocumentViewer.OnDocumentChanged()): // 1) If this is the first assignment, then we're already there by default. // 2) The user may have specified vertical or horizontal offsets in markup or // otherwise () and we need to honor // those settings. if (!_firstRowLayout && !_pageJumpAfterLayout) { MakePageVisible(pivotRow.FirstPage); } else if (_pageJumpAfterLayout) { MakePageVisible(_pageJumpAfterLayoutPageNumber); _pageJumpAfterLayout = false; } _firstRowLayout = false; //If our view was of a "Fit" type, we need to ensure that the fit is //correct -- if the status of Vertical/Horizontal Scrollbars has changed //as a result of our view selection then the Viewport size may have changed. //If so, our current fit is probably wrong. We'll attach a ScrollChanged handler //to our ScrollOwner and when the event is invoked (after layout, but before rendering) //we'll check. //This is "Step 2" of the two-pass layout necessary to do fit properly inside of a //ScrollViewer. if (!_scrollChangedEventAttached && ScrollOwner != null && _documentLayout.ViewMode != ViewMode.Zoom && _documentLayout.ViewMode != ViewMode.SetColumns) { _scrollChangedEventAttached = true; ScrollOwner.ScrollChanged += new ScrollChangedEventHandler(OnScrollChanged); } } #endregion Event Handlers #endregion Private Methods //------------------------------------------------------ // // Private Properties // //----------------------------------------------------- #region Private Properties /// /// Indicates that our Viewport is or is not exactly (0,0). /// ///private bool IsViewportNonzero { get { return (ViewportWidth != 0.0 && ViewportHeight != 0.0); } } /// /// Provides access to DocumentViewer's TextEditor. /// private TextEditor TextEditor { get { if (DocumentViewerOwner != null) { return DocumentViewerOwner.TextEditor; } else { return null; } } } ////// Represents the number of pixels to scroll by when using the /// Mouse Wheel; based on System.Parameters.WheelScrollLines. /// private double MouseWheelVerticalScrollAmount { get { //SystemParameters.WheelScrollLines indicates the number of lines to //scroll when the wheel is moved one "click," we multiply this by //our scroll amount to get the number of pixels to move. return _verticalLineScrollAmount * SystemParameters.WheelScrollLines; } } ////// Represents the number of pixels to scroll by when using the /// Mouse Wheel; based on System.Parameters.WheelScrollLines. /// private double MouseWheelHorizontalScrollAmount { get { //SystemParameters.WheelScrollLines indicates the number of lines to //scroll when the wheel is moved one "click," we multiply this by //our scroll amount to get the number of pixels to move. return _horizontalLineScrollAmount * SystemParameters.WheelScrollLines; } } ////// Returns the minimum allowed scale based on the current view mode. /// Thumbnails mode has a higher minimum than other views. /// private double CurrentMinimumScale { get { return _documentLayout.ViewMode == ViewMode.Thumbnails ? DocumentViewerConstants.MinimumThumbnailsScale : DocumentViewerConstants.MinimumScale; } } #endregion Private Properties //------------------------------------------------------ // // Private Fields // //----------------------------------------------------- #region Private Fields // Our Caches private PageCache _pageCache; private RowCache _rowCache; // Our collection of currently-displayed pages. private ReadOnlyCollection_pageViews; // Data for Properties private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private double _verticalOffset; private double _horizontalOffset; private double _viewportHeight; private double _viewportWidth; private int _firstVisibleRow; private int _visibleRowCount; private int _firstVisiblePageNumber; private int _lastVisiblePageNumber; private ScrollViewer _scrollOwner; private DocumentViewer _documentViewerOwner; private bool _showPageBorders = true; private bool _lockViewModes; private int _maxPagesAcross = 1; // The previous constraint passed to ArrangeCore. private Size _previousConstraint; // The viewing mode (columns, fit, etc...) last used to request a layout. private DocumentLayout _documentLayout = new DocumentLayout(1, ViewMode.SetColumns); private int _documentLayoutsPending; // The pivot rows last used to form the basis of a layout. private RowInfo _savedPivotRow; // The last ExtentWidth and VerticalOffsets encountered in // OnRowCacheChanged, used to determine whether to tweak the HorizontalOffset. private double _lastRowChangeExtentWidth; private double _lastRowChangeVerticalOffset; // Editing private ITextContainer _textContainer; // RubberBand selector used for rubberband selection. private RubberbandSelector _rubberBandSelector; // Flags private bool _isLayoutRequested; //Whether we have requested a layout from the RowCache. private bool _pageJumpAfterLayout; //Whether we need to bring a page into view after layout private int _pageJumpAfterLayoutPageNumber; //The page to jump to after layout private bool _firstRowLayout = true; private bool _scrollChangedEventAttached; //Whether or not we've attached a ScrollChanged event to our ScrollViewer. // We create a Border with a transparent background so that it can // participate in Hit-Testing (which allows click events like those // for our Context Menu to work). This border is displayed behind // the displayed pages so that "dead space" surrounding the pages can // be clicked on. private Border _documentGridBackground; private const int _backgroundVisualIndex = 0; private const int _firstPageVisualIndex = 1; //Constants for MeasureCore constraints //We use this size if we're placed inside a "Size-To-Parent" container like //ScrollViewer or StackPanel and are given Infinite constraints. private readonly Size _defaultConstraint = new Size(250.0, 250.0); //Store all our visual children (pages) here private VisualCollection _childrenCollection; //Information for MakeVisible operations involving pages that are not //yet visible. private int _makeVisiblePageNeeded = -1; private DispatcherOperation _makeVisibleDispatcher; //DispatcherOperations used for executing time-consuming property changes in the background. private DispatcherOperation _setScaleOperation; //Delegate used for BringPageIntoView. private delegate void BringPageIntoViewCallback(MakeVisibleData data, int pageNumber); /// /// Represents a state in the Visual tree merging state machine /// used in RecalcVisiblePages. /// private enum VisualTreeModificationState { ////// Inserting pages before existing /// BeforeExisting = 0, ////// Scanning through existing pages /// DuringExisting, ////// Adding pages after existing /// AfterExisting } ////// Represents a layout mode specified by SetColumns, /// FitToPage, FitToWidth, etc... /// private enum ViewMode { ////// A request to lay out a specified number of columns was made. /// SetColumns = 0, ////// A request to make the specified number of columns visible was made. /// FitColumns, ////// A request for a fit-to-page-width view was made. /// PageWidth, ////// A request for a fit-to-page-height view was made. /// PageHeight, ////// A request for a thumbnail view was made. /// Thumbnails, ////// A request for a non "page-fit" view was made. /// Zoom, ////// A request for the HorizontalOffset to be updated. /// SetHorizontalOffset, ////// A request for the VerticalOffset to be updated. /// SetVerticalOffset } ////// Represents a particular document layout -- /// includes the number of Columns to view and the /// mode to view them in. /// private class DocumentLayout { public DocumentLayout(int columns, ViewMode viewMode) : this(columns, 0.0 /* default */, viewMode) { } public DocumentLayout(double offset, ViewMode viewMode) : this(1 /* default */, offset, viewMode) { } public DocumentLayout(int columns, double offset, ViewMode viewMode) { _columns = columns; _offset = offset; _viewMode = viewMode; } ////// The ViewMode to apply to the layout. /// public ViewMode ViewMode { set { _viewMode = value; } get { return _viewMode; } } ////// The number of columns for the layout. /// public int Columns { set { _columns = value; } get { return _columns; } } ////// The offset (horizontal of vertical) for the layout. /// public double Offset { // Set not currently used. // set { _offset = value; } get { return _offset; } } private ViewMode _viewMode; private int _columns; private double _offset; } ////// An MakeVisibleData object contains data and operation information /// related to asynchronous MakeVisible operations. /// private struct MakeVisibleData { ////// Constructs a new MakeVisibleData object /// /// A visual to be made visible. /// A ContentPosition to be made visible. /// Any bounding rect to be made visible. public MakeVisibleData(Visual visual, ContentPosition contentPosition, Rect rect) { _visual = visual; _contentPosition = contentPosition; _rect = rect; } ////// The Visual to be made visible /// public Visual Visual { get { return _visual; } } ////// The ContentPosition to be made Visible /// public ContentPosition ContentPosition { get { return _contentPosition; } } ////// The bounding rectangle to be made visible /// public Rect Rect { get { return _rect; } } private Visual _visual; private ContentPosition _contentPosition; private Rect _rect; } //Constants for line scrolling amounts private const double _verticalLineScrollAmount = 16.0; private const double _horizontalLineScrollAmount = 16.0; #endregion Private Fields } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- DataKeyArray.cs
- Transform3D.cs
- VectorAnimationUsingKeyFrames.cs
- EventlogProvider.cs
- FileNotFoundException.cs
- GuidConverter.cs
- ObjectItemConventionAssemblyLoader.cs
- SqlEnums.cs
- WebConfigurationFileMap.cs
- LogRestartAreaEnumerator.cs
- ProjectionPruner.cs
- BamlMapTable.cs
- TCPClient.cs
- CustomErrorsSection.cs
- TreeView.cs
- XmlReaderSettings.cs
- SectionRecord.cs
- StyleHelper.cs
- AnonymousIdentificationSection.cs
- SqlNotificationEventArgs.cs
- MasterPageCodeDomTreeGenerator.cs
- WrappingXamlSchemaContext.cs
- LocalizabilityAttribute.cs
- QilNode.cs
- ToolStripDropDownClosingEventArgs.cs
- mactripleDES.cs
- CaretElement.cs
- WindowsTab.cs
- ServiceInstanceProvider.cs
- FormsAuthenticationUserCollection.cs
- WebPartConnectionsConfigureVerb.cs
- ArgumentDesigner.xaml.cs
- CreateUserWizard.cs
- IndexerNameAttribute.cs
- WebRequest.cs
- ObservableCollection.cs
- QuadraticEase.cs
- CheckBoxStandardAdapter.cs
- RankException.cs
- RemoteWebConfigurationHostStream.cs
- XmlSchemaAttribute.cs
- ScaleTransform3D.cs
- CellRelation.cs
- RuntimeConfigurationRecord.cs
- MessageBodyDescription.cs
- BitmapCacheBrush.cs
- WebHttpBinding.cs
- DLinqDataModelProvider.cs
- Propagator.cs
- BackStopAuthenticationModule.cs
- Vector3D.cs
- PolicyLevel.cs
- PropertyNames.cs
- InfoCardRSAOAEPKeyExchangeDeformatter.cs
- JsonFormatReaderGenerator.cs
- TrackBarRenderer.cs
- NameNode.cs
- InlineUIContainer.cs
- IxmlLineInfo.cs
- AudioFileOut.cs
- RC2CryptoServiceProvider.cs
- DataSourceDescriptorCollection.cs
- InternalConfigSettingsFactory.cs
- SqlBulkCopyColumnMapping.cs
- SqlUtil.cs
- HMACRIPEMD160.cs
- HtmlInputSubmit.cs
- GridItemCollection.cs
- TextElementEnumerator.cs
- ResXDataNode.cs
- COM2Enum.cs
- DBDataPermission.cs
- ExpressionEditorAttribute.cs
- WebDisplayNameAttribute.cs
- SchemaNamespaceManager.cs
- Converter.cs
- WebPartDescription.cs
- CompiledScopeCriteria.cs
- WindowsRegion.cs
- CancellationTokenSource.cs
- DynamicPropertyHolder.cs
- DesignerAutoFormatStyle.cs
- Byte.cs
- MembershipSection.cs
- VSWCFServiceContractGenerator.cs
- Error.cs
- Point3DCollectionValueSerializer.cs
- PartitionResolver.cs
- AssemblyAttributesGoHere.cs
- ProfileEventArgs.cs
- FormsAuthenticationCredentials.cs
- UnsafeNativeMethodsCLR.cs
- ToolboxBitmapAttribute.cs
- ToolStripItemTextRenderEventArgs.cs
- ClientCultureInfo.cs
- OutKeywords.cs
- DataServiceRequest.cs
- SystemTcpStatistics.cs
- InkCanvas.cs
- SQLMoney.cs