From 7d39cff890ab322505259fb526c04c791153d45e Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 8 Mar 2009 00:16:56 +0000 Subject: [PATCH] More changes to the handling of invalid visual lines. git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@3835 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61 --- .../Gui/FoldingSection.cs | 3 +- .../Gui/NoReadOnlySections.cs | 3 + .../Gui/SelectionMouseHandler.cs | 229 +++++++------ .../ICSharpCode.AvalonEdit/Gui/TextArea.cs | 103 +++--- .../ICSharpCode.AvalonEdit/Gui/TextView.cs | 312 ++++++++++-------- .../Gui/VisualLinesInvalidException.cs | 48 +++ .../ICSharpCode.AvalonEdit.csproj | 1 + 7 files changed, 413 insertions(+), 286 deletions(-) create mode 100644 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLinesInvalidException.cs diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingSection.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingSection.cs index c4077d1ac1..a1be0a6a43 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingSection.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/FoldingSection.cs @@ -7,6 +7,7 @@ using System; using ICSharpCode.AvalonEdit.Document; +using System.Windows.Threading; namespace ICSharpCode.AvalonEdit.Gui { @@ -40,7 +41,7 @@ namespace ICSharpCode.AvalonEdit.Gui collapsedSection = null; } } - manager.textView.Redraw(); + manager.textView.Redraw(this, DispatcherPriority.Normal); } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NoReadOnlySections.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NoReadOnlySections.cs index c658a4b86c..2849d28137 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NoReadOnlySections.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NoReadOnlySections.cs @@ -12,6 +12,9 @@ using System.Collections.Generic; namespace ICSharpCode.AvalonEdit.Gui { + /// + /// that has no read-only sections; all text is editable. + /// sealed class NoReadOnlySections : IReadOnlySectionProvider { public static readonly NoReadOnlySections Instance = new NoReadOnlySections(); diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/SelectionMouseHandler.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/SelectionMouseHandler.cs index 567d398f21..ab74a08921 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/SelectionMouseHandler.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/SelectionMouseHandler.cs @@ -23,8 +23,43 @@ namespace ICSharpCode.AvalonEdit.Gui /// sealed class SelectionMouseHandler { + #region enum SelectionMode + enum SelectionMode + { + /// + /// no selection (no mouse button down) + /// + None, + /// + /// left mouse button down on selection, might be normal click + /// or might be drag'n'drop + /// + PossibleDragStart, + /// + /// dragging text + /// + Drag, + /// + /// normal selection (click+drag) + /// + Normal, + /// + /// whole-word selection (double click+drag) + /// + WholeWord + } + #endregion + + // TODO: allow disabling text drag'n'drop + const bool AllowTextDragDrop = true; + readonly TextArea textArea; + SelectionMode mode; + AnchorSegment startWord; + Point possibleDragStartMousePos; + + #region Constructor + Attach + Detach public SelectionMouseHandler(TextArea textArea) { this.textArea = textArea; @@ -46,7 +81,27 @@ namespace ICSharpCode.AvalonEdit.Gui textArea.Drop += textArea_Drop; } } - + + public void Detach() + { + mode = SelectionMode.None; + textArea.MouseLeftButtonDown -= textArea_MouseLeftButtonDown; + textArea.MouseMove -= textArea_MouseMove; + textArea.MouseLeftButtonUp -= textArea_MouseLeftButtonUp; + textArea.QueryCursor -= textArea_QueryCursor; + if (AllowTextDragDrop) { + textArea.AllowDrop = false; + textArea.GiveFeedback -= textArea_GiveFeedback; + textArea.QueryContinueDrag -= textArea_QueryContinueDrag; + textArea.DragEnter -= textArea_DragEnter; + textArea.DragOver -= textArea_DragOver; + textArea.DragLeave -= textArea_DragLeave; + textArea.Drop -= textArea_Drop; + } + } + #endregion + + #region Dropping text [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] void textArea_DragEnter(object sender, DragEventArgs e) { @@ -131,22 +186,60 @@ namespace ICSharpCode.AvalonEdit.Gui })); } - public void Detach() + void textArea_GiveFeedback(object sender, GiveFeedbackEventArgs e) { - mode = SelectionMode.None; - textArea.MouseLeftButtonDown -= textArea_MouseLeftButtonDown; - textArea.MouseMove -= textArea_MouseMove; - textArea.MouseLeftButtonUp -= textArea_MouseLeftButtonUp; - textArea.QueryCursor -= textArea_QueryCursor; - if (AllowTextDragDrop) { - textArea.GiveFeedback -= textArea_GiveFeedback; - textArea.QueryContinueDrag -= textArea_QueryContinueDrag; + e.UseDefaultCursors = true; + e.Handled = true; + } + + void textArea_QueryContinueDrag(object sender, QueryContinueDragEventArgs e) + { + if (e.EscapePressed) { + e.Action = DragAction.Cancel; + } else if ((e.KeyStates & DragDropKeyStates.LeftMouseButton) != DragDropKeyStates.LeftMouseButton) { + e.Action = DragAction.Drop; + } else { + e.Action = DragAction.Continue; } + e.Handled = true; } + #endregion - // TODO: allow disabling text drag'n'drop - const bool AllowTextDragDrop = true; + #region Start Drag + void StartDrag() + { + // prevent nested StartDrag calls + mode = SelectionMode.Drag; + + // mouse capture and Drag'n'Drop doesn't mix + textArea.ReleaseMouseCapture(); + + string text = textArea.Selection.GetText(textArea.Document); + DataObject dataObject = new DataObject(); + dataObject.SetText(text); + + DragDropEffects allowedEffects = DragDropEffects.All; + List deleteOnMove; + deleteOnMove = textArea.Selection.Segments.Select(s => new AnchorSegment(textArea.Document, s)).ToList(); + + Debug.WriteLine("DoDragDrop with allowedEffects=" + allowedEffects); + DragDropEffects resultEffect = DragDrop.DoDragDrop(textArea, dataObject, allowedEffects); + Debug.WriteLine("DoDragDrop done, resultEffect=" + resultEffect); + + if (deleteOnMove != null && resultEffect == DragDropEffects.Move) { + textArea.Document.BeginUpdate(); + try { + foreach (ISegment s in deleteOnMove) { + textArea.Document.Remove(s.Offset, s.Length); + } + } finally { + textArea.Document.EndUpdate(); + } + } + } + #endregion + #region QueryCursor // provide the IBeam Cursor for the text area void textArea_QueryCursor(object sender, QueryCursorEventArgs e) { @@ -171,7 +264,9 @@ namespace ICSharpCode.AvalonEdit.Gui } } } + #endregion + #region ContainsOffset helper methods static bool SelectionContains(Selection selection, int offset) { if (selection.IsEmpty) @@ -194,36 +289,9 @@ namespace ICSharpCode.AvalonEdit.Gui int end = start + segment.Length; return offset >= start && offset <= end; } + #endregion - enum SelectionMode - { - /// - /// no selection (no mouse button down) - /// - None, - /// - /// left mouse button down on selection, might be normal click - /// or might be drag'n'drop - /// - PossibleDragStart, - /// - /// dragging text - /// - Drag, - /// - /// normal selection (click+drag) - /// - Normal, - /// - /// whole-word selection (double click+drag) - /// - WholeWord - } - - SelectionMode mode; - AnchorSegment startWord; - Point possibleDragStartMousePos; - + #region LeftButtonDown void textArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { mode = SelectionMode.None; @@ -279,7 +347,9 @@ namespace ICSharpCode.AvalonEdit.Gui } e.Handled = true; } + #endregion + #region Mouse Position <-> Text coordinates SimpleSegment GetWordAtMousePosition(MouseEventArgs e) { TextView textView = textArea.TextView; @@ -308,16 +378,6 @@ namespace ICSharpCode.AvalonEdit.Gui } } - void SetCaretOffsetToMousePosition(MouseEventArgs e) - { - int visualColumn; - int offset = GetOffsetFromMousePosition(e, out visualColumn); - if (offset >= 0) { - textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(offset), visualColumn); - textArea.Caret.DesiredXPos = double.NaN; - } - } - int GetOffsetFromMousePosition(MouseEventArgs e, out int visualColumn) { return GetOffsetFromMousePosition(e.GetPosition(textArea.TextView), out visualColumn); @@ -342,7 +402,9 @@ namespace ICSharpCode.AvalonEdit.Gui } return -1; } + #endregion + #region MouseMove void textArea_MouseMove(object sender, MouseEventArgs e) { if (e.Handled) @@ -365,6 +427,18 @@ namespace ICSharpCode.AvalonEdit.Gui } } } + #endregion + + #region ExtendSelection + void SetCaretOffsetToMousePosition(MouseEventArgs e) + { + int visualColumn; + int offset = GetOffsetFromMousePosition(e, out visualColumn); + if (offset >= 0) { + textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(offset), visualColumn); + textArea.Caret.DesiredXPos = double.NaN; + } + } void ExtendSelectionToMouse(MouseEventArgs e) { @@ -379,7 +453,9 @@ namespace ICSharpCode.AvalonEdit.Gui } } } + #endregion + #region MouseLeftButtonUp void textArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (mode == SelectionMode.None || e.Handled) @@ -395,55 +471,6 @@ namespace ICSharpCode.AvalonEdit.Gui mode = SelectionMode.None; textArea.ReleaseMouseCapture(); } - - void StartDrag() - { - // prevent nested StartDrag calls - mode = SelectionMode.Drag; - - // mouse capture and Drag'n'Drop doesn't mix - textArea.ReleaseMouseCapture(); - - string text = textArea.Selection.GetText(textArea.Document); - DataObject dataObject = new DataObject(); - dataObject.SetText(text); - - DragDropEffects allowedEffects = DragDropEffects.All; - List deleteOnMove; - deleteOnMove = textArea.Selection.Segments.Select(s => new AnchorSegment(textArea.Document, s)).ToList(); - - Debug.WriteLine("DoDragDrop with allowedEffects=" + allowedEffects); - DragDropEffects resultEffect = DragDrop.DoDragDrop(textArea, dataObject, allowedEffects); - Debug.WriteLine("DoDragDrop done, resultEffect=" + resultEffect); - - if (deleteOnMove != null && resultEffect == DragDropEffects.Move) { - textArea.Document.BeginUpdate(); - try { - foreach (ISegment s in deleteOnMove) { - textArea.Document.Remove(s.Offset, s.Length); - } - } finally { - textArea.Document.EndUpdate(); - } - } - } - - void textArea_GiveFeedback(object sender, GiveFeedbackEventArgs e) - { - e.UseDefaultCursors = true; - e.Handled = true; - } - - void textArea_QueryContinueDrag(object sender, QueryContinueDragEventArgs e) - { - if (e.EscapePressed) { - e.Action = DragAction.Cancel; - } else if ((e.KeyStates & DragDropKeyStates.LeftMouseButton) != DragDropKeyStates.LeftMouseButton) { - e.Action = DragAction.Drop; - } else { - e.Action = DragAction.Continue; - } - e.Handled = true; - } + #endregion } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextArea.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextArea.cs index 1543c86264..50866ab912 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextArea.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextArea.cs @@ -8,6 +8,7 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; @@ -126,6 +127,7 @@ namespace ICSharpCode.AvalonEdit } #endregion + #region Caret handling on document changes bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { if (managerType == typeof(TextDocumentWeakEventManager.Changing)) { @@ -181,18 +183,37 @@ namespace ICSharpCode.AvalonEdit Undo(); } } + #endregion - readonly Caret caret; - + #region TextView property + readonly TextView textView; + IScrollInfo scrollInfo; + /// - /// Gets the Caret used for this text area. + /// Gets the text view used to display text in this text area. /// - public Caret Caret { - get { return caret; } + public TextView TextView { + get { + return textView; + } } + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + scrollInfo = textView; + ApplyScrollInfo(); + } + #endregion + #region Selection property Selection selection = Selection.Empty; + /// + /// Occurs when the selection has changed. + /// + public event EventHandler SelectionChanged; + /// /// Gets/Sets the selection in this text area. /// @@ -213,30 +234,16 @@ namespace ICSharpCode.AvalonEdit } } } + #endregion - /// - /// Occurs when the selection has changed. - /// - public event EventHandler SelectionChanged; - - readonly TextView textView; - IScrollInfo scrollInfo; - - /// - public override void OnApplyTemplate() - { - base.OnApplyTemplate(); - scrollInfo = textView; - ApplyScrollInfo(); - } + #region Properties + readonly Caret caret; /// - /// Gets the text view used to display text in this text area. + /// Gets the Caret used for this text area. /// - public TextView TextView { - get { - return textView; - } + public Caret Caret { + get { return caret; } } ObservableCollection leftMargins = new ObservableCollection(); @@ -250,6 +257,21 @@ namespace ICSharpCode.AvalonEdit } } + IReadOnlySectionProvider readOnlySectionProvider = NoReadOnlySections.Instance; + + /// + /// Gets/Sets an object that provides read-only sections for the text area. + /// + public IReadOnlySectionProvider ReadOnlySectionProvider { + get { return readOnlySectionProvider; } + set { + if (value == null) + throw new ArgumentNullException("value"); + readOnlySectionProvider = value; + } + } + #endregion + #region Undo / Redo UndoStack GetUndoStack() { @@ -432,6 +454,7 @@ namespace ICSharpCode.AvalonEdit } #endregion + #region Focus Handling (Show/Hide Caret) /// protected override void OnMouseDown(MouseButtonEventArgs e) { @@ -454,21 +477,9 @@ namespace ICSharpCode.AvalonEdit caret.Hide(); e.Handled = true; } + #endregion - IReadOnlySectionProvider readOnlySectionProvider = NoReadOnlySections.Instance; - - /// - /// Gets/Sets an object that provides read-only sections for the text area. - /// - public IReadOnlySectionProvider ReadOnlySectionProvider { - get { return readOnlySectionProvider; } - set { - if (value == null) - throw new ArgumentNullException("value"); - readOnlySectionProvider = value; - } - } - + #region OnTextInput / RemoveSelectedText / ReplaceSelectionWithText /// protected override void OnTextInput(TextCompositionEventArgs e) { @@ -476,14 +487,7 @@ namespace ICSharpCode.AvalonEdit if (!e.Handled) { TextDocument document = this.Document; if (document != null) { - document.BeginUpdate(); - try { - RemoveSelectedText(); - if (readOnlySectionProvider.CanInsert(caret.Offset)) - document.Insert(caret.Offset, e.Text); - } finally { - document.EndUpdate(); - } + ReplaceSelectionWithText(e.Text); caret.BringCaretToView(); e.Handled = true; } @@ -495,7 +499,9 @@ namespace ICSharpCode.AvalonEdit selection.RemoveSelectedText(this); #if DEBUG if (!selection.IsEmpty) { - // TODO: assert that the remaining selection is read-only + foreach (ISegment s in selection.Segments) { + Debug.Assert(ReadOnlySectionProvider.GetDeletableSegments(s).Count() == 0); + } } #endif } @@ -512,5 +518,6 @@ namespace ICSharpCode.AvalonEdit Document.EndUpdate(); } } + #endregion } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextView.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextView.cs index 898c57ac7b..a0304ad1cc 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextView.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/TextView.cs @@ -26,9 +26,13 @@ namespace ICSharpCode.AvalonEdit.Gui { /// /// A virtualizing panel producing+showing s for a . + /// + /// This is the heart of the text editor, this class controls the text rendering process. + /// + /// Taken as a standalone control, it's a text viewer without any editing capability. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", - Justification = "The user usually doesn't work with TextView but with TextEditor; nulling the Document property is sufficient to dispose everything.")] + Justification = "The user usually doesn't work with TextView but with TextEditor; and nulling the Document property is sufficient to dispose everything.")] public class TextView : FrameworkElement, IScrollInfo, IWeakEventListener { #region Constructor @@ -50,7 +54,7 @@ namespace ICSharpCode.AvalonEdit.Gui } #endregion - #region Properties + #region Document Property /// /// Document property. /// @@ -119,6 +123,7 @@ namespace ICSharpCode.AvalonEdit.Gui } #endregion + #region Collection Properties readonly ObservableCollection elementGenerators = new ObservableCollection(); /// @@ -145,13 +150,15 @@ namespace ICSharpCode.AvalonEdit.Gui public UIElementCollection Adorners { get { return adorners; } } + #endregion + #region Redraw methods / VisualLine invalidation /// /// Causes the text editor to regenerate all visual lines. /// public void Redraw() { - Redraw(DispatcherPriority.Render); + Redraw(DispatcherPriority.Normal); } /// @@ -183,23 +190,21 @@ namespace ICSharpCode.AvalonEdit.Gui public void Redraw(int offset, int length, DispatcherPriority redrawPriority) { VerifyAccess(); - if (allVisualLines.Count != 0 || visibleVisualLines != null) { - bool removedLine = false; - for (int i = 0; i < allVisualLines.Count; i++) { - VisualLine visualLine = allVisualLines[i]; - int lineStart = visualLine.FirstDocumentLine.Offset; - int lineEnd = visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength; - if (!(lineEnd < offset || lineStart > offset + length)) { - removedLine = true; - allVisualLines.RemoveAt(i--); - DisposeVisualLine(visualLine); - } - } - if (removedLine) { - visibleVisualLines = null; - InvalidateMeasure(redrawPriority); + bool removedLine = false; + for (int i = 0; i < allVisualLines.Count; i++) { + VisualLine visualLine = allVisualLines[i]; + int lineStart = visualLine.FirstDocumentLine.Offset; + int lineEnd = visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength; + if (!(lineEnd < offset || lineStart > offset + length)) { + removedLine = true; + allVisualLines.RemoveAt(i--); + DisposeVisualLine(visualLine); } } + if (removedLine) { + visibleVisualLines = null; + InvalidateMeasure(redrawPriority); + } } /// @@ -213,6 +218,36 @@ namespace ICSharpCode.AvalonEdit.Gui } } + /// + /// Invalidates all visual lines. + /// The caller of ClearVisualLines() must also call InvalidateMeasure() to ensure + /// that the visual lines will be recreated. + /// + void ClearVisualLines() + { + visibleVisualLines = null; + if (allVisualLines.Count != 0) { + foreach (VisualLine visualLine in allVisualLines) { + DisposeVisualLine(visualLine); + } + allVisualLines.Clear(); + } + } + + void DisposeVisualLine(VisualLine visualLine) + { + if (newVisualLines != null && newVisualLines.Contains(visualLine)) { + throw new ArgumentException("Cannot dispose visual line because it is in construction!"); + } + visualLine.IsDisposed = true; + foreach (TextLine textLine in visualLine.TextLines) { + textLine.Dispose(); + } + RemoveInlineObjects(visualLine); + } + #endregion + + #region InvalidateMeasure(DispatcherPriority) DispatcherOperation invalidateMeasureOperation; void InvalidateMeasure(DispatcherPriority priority) @@ -239,44 +274,9 @@ namespace ICSharpCode.AvalonEdit.Gui } } } + #endregion - /// - /// Waits for the visual lines to be built. - /// - private void EnsureVisualLines() - { - Dispatcher.VerifyAccess(); - if (visibleVisualLines == null) { - // increase priority for real Redraw - InvalidateMeasure(DispatcherPriority.Normal); - // force immediate re-measure - UpdateLayout(); - } - } - - void ClearVisualLines() - { - visibleVisualLines = null; - if (allVisualLines.Count != 0) { - foreach (VisualLine visualLine in allVisualLines) { - DisposeVisualLine(visualLine); - } - allVisualLines.Clear(); - } - } - - void DisposeVisualLine(VisualLine visualLine) - { - if (newVisualLines != null && newVisualLines.Contains(visualLine)) { - throw new ArgumentException("Cannot dispose visual line because it is in construction!"); - } - visualLine.IsDisposed = true; - foreach (TextLine textLine in visualLine.TextLines) { - textLine.Dispose(); - } - RemoveInlineObjects(visualLine); - } - + #region Get(OrConstruct)VisualLine /// /// Gets the visual line that contains the document line with the specified number. /// Returns null if the document line is outside the visible range. @@ -324,39 +324,27 @@ namespace ICSharpCode.AvalonEdit.Gui } return l; } + #endregion - /// - /// Collapses lines for the purpose of scrolling. This method is meant for - /// s that cause s to span - /// multiple s. Do not call it without providing a corresponding - /// . - /// If you want to create collapsible text sections, see . - /// - public CollapsedLineSection CollapseLines(DocumentLine start, DocumentLine end) - { - VerifyAccess(); - return heightTree.CollapseText(start, end); - } - - /// - /// Gets the height of the document. - /// - public double DocumentHeight { - get { return heightTree.TotalHeight; } - } - - #region Measure - TextFormatter formatter; + #region Visual Lines (fields and properties) List allVisualLines = new List(); ReadOnlyCollection visibleVisualLines; double clippedPixelsOnTop; + List newVisualLines; /// /// Gets the currently visible visual lines. /// + /// + /// Gets thrown if there are invalid visual lines when this property is accessed. + /// You can use the property to check for this case, + /// or use the method to force creating the visual lines + /// when they are invalid. + /// public ReadOnlyCollection VisualLines { get { - EnsureVisualLines(); + if (visibleVisualLines == null) + throw new VisualLinesInvalidException(); return visibleVisualLines; } } @@ -375,56 +363,75 @@ namespace ICSharpCode.AvalonEdit.Gui /// public event EventHandler VisualLinesChanged; - TextRunProperties CreateGlobalTextRunProperties() - { - return new GlobalTextRunProperties { - typeface = this.CreateTypeface(), - fontRenderingEmSize = LineHeight, - foregroundBrush = (Brush)GetValue(Control.ForegroundProperty), - cultureInfo = CultureInfo.CurrentCulture - }; - } - - TextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties) + /// + /// If the visual lines are invalid, creates new visual lines for the visible part + /// of the document. + /// If all visual lines are valid, this method does nothing. + /// + /// The visual line build process is already running. + /// It is not allowed to call this method during the construction of a visual line. + public void EnsureVisualLines() { - return new VisualLineTextParagraphProperties { - defaultTextRunProperties = defaultTextRunProperties, - textWrapping = canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, - tabSize = 4 * WideSpaceWidth - }; + Dispatcher.VerifyAccess(); + if (inMeasure) + throw new InvalidOperationException("The visual line build process is already running! Cannot EnsureVisualLines() during Measure!"); + if (visibleVisualLines == null) { + // increase priority for re-measure + InvalidateMeasure(DispatcherPriority.Normal); + // force immediate re-measure + UpdateLayout(); + } } + #endregion + #region Measure Size lastAvailableSize; - List newVisualLines; + bool inMeasure; - /// - /// Measure implementation. - /// + /// protected override Size MeasureOverride(Size availableSize) { if (!canHorizontallyScroll && !availableSize.Width.IsClose(lastAvailableSize.Width)) ClearVisualLines(); lastAvailableSize = availableSize; - return DoMeasure(availableSize); + + RemoveInlineObjectsNow(); + + if (document == null) + return Size.Empty; + + InvalidateVisual(); // = InvalidateArrange+InvalidateRender + + double maxWidth; + inMeasure = true; + try { + maxWidth = CreateAndMeasureVisualLines(availableSize); + } finally { + inMeasure = false; + } + + RemoveInlineObjectsNow(); + + SetScrollData(availableSize, + new Size(maxWidth, heightTree.TotalHeight), + scrollOffset); + if (VisualLinesChanged != null) + VisualLinesChanged(this, EventArgs.Empty); + if (canHorizontallyScroll) { + return availableSize; + } else { + return new Size(maxWidth, availableSize.Height); + } } /// - /// Immediately performs the text creation. + /// Build all VisualLines in the visible range. /// - /// The size of the text view. - /// - Size DoMeasure(Size availableSize) + /// Width the longest line + double CreateAndMeasureVisualLines(Size availableSize) { - bool isRealMeasure = true; - if (isRealMeasure) - RemoveInlineObjectsNow(); - - if (document == null) - return Size.Empty; - TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties(); TextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties); - InvalidateVisual(); // = InvalidateArrange+InvalidateRender Debug.WriteLine("Measure availableSize=" + availableSize + ", scrollOffset=" + scrollOffset); var firstLineInView = heightTree.GetLineByVisualPosition(scrollOffset.Y); @@ -472,8 +479,6 @@ namespace ICSharpCode.AvalonEdit.Gui if (!newVisualLines.Contains(line)) DisposeVisualLine(line); } - if (isRealMeasure) - RemoveInlineObjectsNow(); allVisualLines = newVisualLines; // visibleVisualLines = readonly copy of visual lines @@ -485,17 +490,30 @@ namespace ICSharpCode.AvalonEdit.Gui "This can happen when Redraw() is called during measure for lines " + "that are already constructed."); } - - SetScrollData(availableSize, - new Size(maxWidth, heightTree.TotalHeight), - scrollOffset); - if (VisualLinesChanged != null) - VisualLinesChanged(this, EventArgs.Empty); - if (canHorizontallyScroll) { - return availableSize; - } else { - return new Size(maxWidth, availableSize.Height); - } + return maxWidth; + } + #endregion + + #region BuildVisualLine + TextFormatter formatter; + + TextRunProperties CreateGlobalTextRunProperties() + { + return new GlobalTextRunProperties { + typeface = this.CreateTypeface(), + fontRenderingEmSize = LineHeight, + foregroundBrush = (Brush)GetValue(Control.ForegroundProperty), + cultureInfo = CultureInfo.CurrentCulture + }; + } + + TextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties) + { + return new VisualLineTextParagraphProperties { + defaultTextRunProperties = defaultTextRunProperties, + textWrapping = canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, + tabSize = 4 * WideSpaceWidth + }; } VisualLine BuildVisualLine(DocumentLine documentLine, @@ -969,17 +987,6 @@ namespace ICSharpCode.AvalonEdit.Gui } #endregion - /// - /// Gets the document line at the specified visual position. - /// - public DocumentLine GetDocumentLineByVisualTop(double visualTop) - { - VerifyAccess(); - if (heightTree == null) - throw new InvalidOperationException(); - return heightTree.GetLineByVisualPosition(visualTop); - } - #region Visual element mouse handling /// protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) @@ -1041,15 +1048,17 @@ namespace ICSharpCode.AvalonEdit.Gui } } } + #endregion + #region Getting elements from Visual Position /// /// Gets the visual line at the specified document position (relative to start of document). /// Returns null if there is no visual line for the position (e.g. the position is outside the visible /// text area). - /// You may want to call () before calling this method. /// public VisualLine GetVisualLineFromVisualTop(double visualTop) { + EnsureVisualLines(); foreach (VisualLine vl in this.VisualLines) { if (visualTop < vl.VisualTop) continue; @@ -1074,5 +1083,36 @@ namespace ICSharpCode.AvalonEdit.Gui return null; } #endregion + + /// + /// Collapses lines for the purpose of scrolling. This method is meant for + /// s that cause s to span + /// multiple s. Do not call it without providing a corresponding + /// . + /// If you want to create collapsible text sections, see . + /// + public CollapsedLineSection CollapseLines(DocumentLine start, DocumentLine end) + { + VerifyAccess(); + return heightTree.CollapseText(start, end); + } + + /// + /// Gets the height of the document. + /// + public double DocumentHeight { + get { return heightTree.TotalHeight; } + } + + /// + /// Gets the document line at the specified visual position. + /// + public DocumentLine GetDocumentLineByVisualTop(double visualTop) + { + VerifyAccess(); + if (heightTree == null) + throw new InvalidOperationException(); + return heightTree.GetLineByVisualPosition(visualTop); + } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLinesInvalidException.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLinesInvalidException.cs new file mode 100644 index 0000000000..268f94c267 --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLinesInvalidException.cs @@ -0,0 +1,48 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Runtime.Serialization; + +namespace ICSharpCode.AvalonEdit.Gui +{ + /// + /// A VisualLinesInvalidException indicates that you accessed the property + /// of the while the visual lines were invalid. + /// + [Serializable] + public class VisualLinesInvalidException : Exception + { + /// + /// Creates a new VisualLinesInvalidException instance. + /// + public VisualLinesInvalidException() : base() + { + } + + /// + /// Creates a new VisualLinesInvalidException instance. + /// + public VisualLinesInvalidException(string message) : base(message) + { + } + + /// + /// Creates a new VisualLinesInvalidException instance. + /// + public VisualLinesInvalidException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Creates a new VisualLinesInvalidException instance. + /// + protected VisualLinesInvalidException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index 283e1c5137..103d09ca76 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -168,6 +168,7 @@ + VisualLine.cs