// // // // // $Revision$ // using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; using ICSharpCode.TextEditor.Document; namespace ICSharpCode.TextEditor { /// /// This class paints the textarea. /// public class TextView : AbstractMargin, IDisposable { int fontHeight; //Hashtable charWitdh = new Hashtable(); //StringFormat measureStringFormat = (StringFormat)StringFormat.GenericTypographic.Clone(); Highlight highlight; int physicalColumn = 0; // used for calculating physical column during paint public void Dispose() { measureCache.Clear(); //measureStringFormat.Dispose(); } public Highlight Highlight { get { return highlight; } set { highlight = value; } } public int FirstPhysicalLine { get { return textArea.VirtualTop.Y / fontHeight; } } public int LineHeightRemainder { get { return textArea.VirtualTop.Y % fontHeight; } } /// Gets the first visible logical line. public int FirstVisibleLine { get { return textArea.Document.GetFirstLogicalLine(textArea.VirtualTop.Y / fontHeight); } set { if (FirstVisibleLine != value) { textArea.VirtualTop = new Point(textArea.VirtualTop.X, textArea.Document.GetVisibleLine(value) * fontHeight); } } } public int VisibleLineDrawingRemainder { get { return textArea.VirtualTop.Y % fontHeight; } } public int FontHeight { get { return fontHeight; } } public int VisibleLineCount { get { return 1 + DrawingPosition.Height / fontHeight; } } public int VisibleColumnCount { get { return (int)(DrawingPosition.Width / WideSpaceWidth) - 1; } } public TextView(TextArea textArea) : base(textArea) { base.Cursor = Cursors.IBeam; OptionsChanged(); } static int GetFontHeight(Font font) { int height1 = TextRenderer.MeasureText("_", font).Height; int height2 = (int)Math.Ceiling(font.GetHeight()); return Math.Max(height1, height2) + 1; } int spaceWidth; /// /// Gets the width of a space character. /// This value can be quite small in some fonts - consider using WideSpaceWidth instead. /// public int SpaceWidth { get { return spaceWidth; } } int wideSpaceWidth; /// /// Gets the width of a 'wide space' (=one quarter of a tab, if tab is set to 4 spaces). /// On monospaced fonts, this is the same value as spaceWidth. /// public int WideSpaceWidth { get { return wideSpaceWidth; } } Font lastFont; public void OptionsChanged() { this.lastFont = TextEditorProperties.FontContainer.RegularFont; this.fontHeight = GetFontHeight(lastFont); // use minimum width - in some fonts, space has no width but kerning is used instead // -> DivideByZeroException this.spaceWidth = Math.Max(GetWidth(' ', lastFont), 1); // tab should have the width of 4*'x' this.wideSpaceWidth = Math.Max(spaceWidth, GetWidth('x', lastFont)); } #region Paint functions public override void Paint(Graphics g, Rectangle rect) { if (rect.Width <= 0 || rect.Height <= 0) { return; } // Just to ensure that fontHeight and char widths are always correct... if (lastFont != TextEditorProperties.FontContainer.RegularFont) { OptionsChanged(); textArea.Invalidate(); } int horizontalDelta = textArea.VirtualTop.X; if (horizontalDelta > 0) { g.SetClip(this.DrawingPosition); } for (int y = 0; y < (DrawingPosition.Height + VisibleLineDrawingRemainder) / fontHeight + 1; ++y) { Rectangle lineRectangle = new Rectangle(DrawingPosition.X - horizontalDelta, DrawingPosition.Top + y * fontHeight - VisibleLineDrawingRemainder, DrawingPosition.Width + horizontalDelta, fontHeight); if (rect.IntersectsWith(lineRectangle)) { int fvl = textArea.Document.GetVisibleLine(FirstVisibleLine); int currentLine = textArea.Document.GetFirstLogicalLine(textArea.Document.GetVisibleLine(FirstVisibleLine) + y); PaintDocumentLine(g, currentLine, lineRectangle); } } DrawMarkerDraw(g); if (horizontalDelta > 0) { g.ResetClip(); } textArea.Caret.PaintCaret(g); } void PaintDocumentLine(Graphics g, int lineNumber, Rectangle lineRectangle) { Debug.Assert(lineNumber >= 0); Brush bgColorBrush = GetBgColorBrush(lineNumber); Brush backgroundBrush = textArea.Enabled ? bgColorBrush : SystemBrushes.InactiveBorder; if (lineNumber >= textArea.Document.TotalNumberOfLines) { g.FillRectangle(backgroundBrush, lineRectangle); if (TextEditorProperties.ShowInvalidLines) { DrawInvalidLineMarker(g, lineRectangle.Left, lineRectangle.Top); } if (TextEditorProperties.ShowVerticalRuler) { DrawVerticalRuler(g, lineRectangle); } // bgColorBrush.Dispose(); return; } int physicalXPos = lineRectangle.X; // there can't be a folding wich starts in an above line and ends here, because the line is a new one, // there must be a return before this line. int column = 0; physicalColumn = 0; if (TextEditorProperties.EnableFolding) { while (true) { List starts = textArea.Document.FoldingManager.GetFoldedFoldingsWithStartAfterColumn(lineNumber, column - 1); if (starts == null || starts.Count <= 0) { if (lineNumber < textArea.Document.TotalNumberOfLines) { physicalXPos = PaintLinePart(g, lineNumber, column, textArea.Document.GetLineSegment(lineNumber).Length, lineRectangle, physicalXPos); } break; } // search the first starting folding FoldMarker firstFolding = (FoldMarker)starts[0]; foreach (FoldMarker fm in starts) { if (fm.StartColumn < firstFolding.StartColumn) { firstFolding = fm; } } starts.Clear(); physicalXPos = PaintLinePart(g, lineNumber, column, firstFolding.StartColumn, lineRectangle, physicalXPos); column = firstFolding.EndColumn; lineNumber = firstFolding.EndLine; if (lineNumber >= textArea.Document.TotalNumberOfLines) { Debug.Assert(false, "Folding ends after document end"); break; } ColumnRange selectionRange2 = textArea.SelectionManager.GetSelectionAtLine(lineNumber); bool drawSelected = ColumnRange.WholeColumn.Equals(selectionRange2) || firstFolding.StartColumn >= selectionRange2.StartColumn && firstFolding.EndColumn <= selectionRange2.EndColumn; physicalXPos = PaintFoldingText(g, lineNumber, physicalXPos, lineRectangle, firstFolding.FoldText, drawSelected); } } else { physicalXPos = PaintLinePart(g, lineNumber, 0, textArea.Document.GetLineSegment(lineNumber).Length, lineRectangle, physicalXPos); } if (lineNumber < textArea.Document.TotalNumberOfLines) { // Paint things after end of line ColumnRange selectionRange = textArea.SelectionManager.GetSelectionAtLine(lineNumber); LineSegment currentLine = textArea.Document.GetLineSegment(lineNumber); HighlightColor selectionColor = textArea.Document.HighlightingStrategy.GetColorFor("Selection"); bool selectionBeyondEOL = selectionRange.EndColumn > currentLine.Length || ColumnRange.WholeColumn.Equals(selectionRange); if (TextEditorProperties.ShowEOLMarker) { HighlightColor eolMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("EOLMarkers"); physicalXPos += DrawEOLMarker(g, eolMarkerColor.Color, selectionBeyondEOL ? bgColorBrush : backgroundBrush, physicalXPos, lineRectangle.Y); } else { if (selectionBeyondEOL) { g.FillRectangle(BrushRegistry.GetBrush(selectionColor.BackgroundColor), new RectangleF(physicalXPos, lineRectangle.Y, WideSpaceWidth, lineRectangle.Height)); physicalXPos += WideSpaceWidth; } } Brush fillBrush = selectionBeyondEOL && TextEditorProperties.AllowCaretBeyondEOL ? bgColorBrush : backgroundBrush; g.FillRectangle(fillBrush, new RectangleF(physicalXPos, lineRectangle.Y, lineRectangle.Width - physicalXPos + lineRectangle.X, lineRectangle.Height)); } if (TextEditorProperties.ShowVerticalRuler) { DrawVerticalRuler(g, lineRectangle); } // bgColorBrush.Dispose(); } bool DrawLineMarkerAtLine(int lineNumber) { return lineNumber == base.textArea.Caret.Line && textArea.MotherTextAreaControl.TextEditorProperties.LineViewerStyle == LineViewerStyle.FullRow; } Brush GetBgColorBrush(int lineNumber) { if (DrawLineMarkerAtLine(lineNumber)) { HighlightColor caretLine = textArea.Document.HighlightingStrategy.GetColorFor("CaretMarker"); return BrushRegistry.GetBrush(caretLine.Color); } HighlightColor background = textArea.Document.HighlightingStrategy.GetColorFor("Default"); Color bgColor = background.BackgroundColor; return BrushRegistry.GetBrush(bgColor); } const int additionalFoldTextSize = 1; int PaintFoldingText(Graphics g, int lineNumber, int physicalXPos, Rectangle lineRectangle, string text, bool drawSelected) { // TODO: get font and color from the highlighting file HighlightColor selectionColor = textArea.Document.HighlightingStrategy.GetColorFor("Selection"); Brush bgColorBrush = drawSelected ? BrushRegistry.GetBrush(selectionColor.BackgroundColor) : GetBgColorBrush(lineNumber); Brush backgroundBrush = textArea.Enabled ? bgColorBrush : SystemBrushes.InactiveBorder; Font font = textArea.TextEditorProperties.FontContainer.RegularFont; int wordWidth = MeasureStringWidth(g, text, font) + additionalFoldTextSize; Rectangle rect = new Rectangle(physicalXPos, lineRectangle.Y, wordWidth, lineRectangle.Height - 1); g.FillRectangle(backgroundBrush, rect); physicalColumn += text.Length; DrawString(g, text, font, drawSelected ? selectionColor.Color : Color.Gray, rect.X + 1, rect.Y); g.DrawRectangle(BrushRegistry.GetPen(drawSelected ? Color.DarkGray : Color.Gray), rect.X, rect.Y, rect.Width, rect.Height); return physicalXPos + wordWidth + 1; } struct MarkerToDraw { internal TextMarker marker; internal RectangleF drawingRect; public MarkerToDraw(TextMarker marker, RectangleF drawingRect) { this.marker = marker; this.drawingRect = drawingRect; } } List markersToDraw = new List(); void DrawMarker(Graphics g, TextMarker marker, RectangleF drawingRect) { // draw markers later so they can overdraw the following text markersToDraw.Add(new MarkerToDraw(marker, drawingRect)); } void DrawMarkerDraw(Graphics g) { foreach (MarkerToDraw m in markersToDraw) { TextMarker marker = m.marker; RectangleF drawingRect = m.drawingRect; float drawYPos = drawingRect.Bottom - 1; switch (marker.TextMarkerType) { case TextMarkerType.Underlined: g.DrawLine(BrushRegistry.GetPen(marker.Color), drawingRect.X, drawYPos, drawingRect.Right, drawYPos); break; case TextMarkerType.WaveLine: int reminder = ((int)drawingRect.X) % 6; for (float i = (int)drawingRect.X - reminder; i < drawingRect.Right; i += 6) { g.DrawLine(BrushRegistry.GetPen(marker.Color), i, drawYPos + 3 - 4, i + 3, drawYPos + 1 - 4); if (i + 3 < drawingRect.Right) { g.DrawLine(BrushRegistry.GetPen(marker.Color), i + 3, drawYPos + 1 - 4, i + 6, drawYPos + 3 - 4); } } break; case TextMarkerType.SolidBlock: g.FillRectangle(BrushRegistry.GetBrush(marker.Color), drawingRect); break; } } markersToDraw.Clear(); } /// /// Get the marker brush (for solid block markers) at a given position. /// /// The offset. /// The length. /// All markers that have been found. /// The Brush or null when no marker was found. Brush GetMarkerBrushAt(int offset, int length, ref Color foreColor, out IList markers) { markers = Document.MarkerStrategy.GetMarkers(offset, length); foreach (TextMarker marker in markers) { if (marker.TextMarkerType == TextMarkerType.SolidBlock) { if (marker.OverrideForeColor) { foreColor = marker.ForeColor; } return BrushRegistry.GetBrush(marker.Color); } } return null; } int PaintLinePart(Graphics g, int lineNumber, int startColumn, int endColumn, Rectangle lineRectangle, int physicalXPos) { bool drawLineMarker = DrawLineMarkerAtLine(lineNumber); Brush backgroundBrush = textArea.Enabled ? GetBgColorBrush(lineNumber) : SystemBrushes.InactiveBorder; HighlightColor selectionColor = textArea.Document.HighlightingStrategy.GetColorFor("Selection"); ColumnRange selectionRange = textArea.SelectionManager.GetSelectionAtLine(lineNumber); HighlightColor tabMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("TabMarkers"); HighlightColor spaceMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("SpaceMarkers"); LineSegment currentLine = textArea.Document.GetLineSegment(lineNumber); Brush selectionBackgroundBrush = BrushRegistry.GetBrush(selectionColor.BackgroundColor); if (currentLine.Words == null) { return physicalXPos; } int currentWordOffset = 0; // we cannot use currentWord.Offset because it is not set on space words TextWord currentWord; TextWord nextCurrentWord = null; FontContainer fontContainer = TextEditorProperties.FontContainer; for (int wordIdx = 0; wordIdx < currentLine.Words.Count; wordIdx++) { currentWord = currentLine.Words[wordIdx]; if (currentWordOffset < startColumn) { // TODO: maybe we need to split at startColumn when we support fold markers // inside words currentWordOffset += currentWord.Length; continue; } repeatDrawCurrentWord: //physicalXPos += 10; // leave room between drawn words - useful for debugging the drawing code if (currentWordOffset >= endColumn || physicalXPos >= lineRectangle.Right) { break; } int currentWordEndOffset = currentWordOffset + currentWord.Length - 1; TextWordType currentWordType = currentWord.Type; IList markers; Color wordForeColor; if (currentWordType == TextWordType.Space) wordForeColor = spaceMarkerColor.Color; else if (currentWordType == TextWordType.Tab) wordForeColor = tabMarkerColor.Color; else wordForeColor = currentWord.Color; Brush wordBackBrush = GetMarkerBrushAt(currentLine.Offset + currentWordOffset, currentWord.Length, ref wordForeColor, out markers); // It is possible that we have to split the current word because a marker/the selection begins/ends inside it if (currentWord.Length > 1) { int splitPos = int.MaxValue; if (highlight != null) { // split both before and after highlight if (highlight.OpenBrace.Y == lineNumber) { if (highlight.OpenBrace.X >= currentWordOffset && highlight.OpenBrace.X <= currentWordEndOffset) { splitPos = Math.Min(splitPos, highlight.OpenBrace.X - currentWordOffset); } } if (highlight.CloseBrace.Y == lineNumber) { if (highlight.CloseBrace.X >= currentWordOffset && highlight.CloseBrace.X <= currentWordEndOffset) { splitPos = Math.Min(splitPos, highlight.CloseBrace.X - currentWordOffset); } } if (splitPos == 0) { splitPos = 1; // split after highlight } } if (endColumn < currentWordEndOffset) { // split when endColumn is reached splitPos = Math.Min(splitPos, endColumn - currentWordOffset); } if (selectionRange.StartColumn > currentWordOffset && selectionRange.StartColumn <= currentWordEndOffset) { splitPos = Math.Min(splitPos, selectionRange.StartColumn - currentWordOffset); } else if (selectionRange.EndColumn > currentWordOffset && selectionRange.EndColumn <= currentWordEndOffset) { splitPos = Math.Min(splitPos, selectionRange.EndColumn - currentWordOffset); } foreach (TextMarker marker in markers) { int markerColumn = marker.Offset - currentLine.Offset; int markerEndColumn = marker.EndOffset - currentLine.Offset + 1; // make end offset exclusive if (markerColumn > currentWordOffset && markerColumn <= currentWordEndOffset) { splitPos = Math.Min(splitPos, markerColumn - currentWordOffset); } else if (markerEndColumn > currentWordOffset && markerEndColumn <= currentWordEndOffset) { splitPos = Math.Min(splitPos, markerEndColumn - currentWordOffset); } } if (splitPos != int.MaxValue) { if (nextCurrentWord != null) throw new ApplicationException("split part invalid: first part cannot be splitted further"); nextCurrentWord = TextWord.Split(ref currentWord, splitPos); goto repeatDrawCurrentWord; // get markers for first word part } } // get colors from selection status: if (ColumnRange.WholeColumn.Equals(selectionRange) || (selectionRange.StartColumn <= currentWordOffset && selectionRange.EndColumn > currentWordEndOffset)) { // word is completely selected wordBackBrush = selectionBackgroundBrush; if (selectionColor.HasForeground) { wordForeColor = selectionColor.Color; } } else if (drawLineMarker) { wordBackBrush = backgroundBrush; } if (wordBackBrush == null) { // use default background if no other background is set if (currentWord.SyntaxColor != null && currentWord.SyntaxColor.HasBackground) wordBackBrush = BrushRegistry.GetBrush(currentWord.SyntaxColor.BackgroundColor); else wordBackBrush = backgroundBrush; } RectangleF wordRectangle; if (currentWord.Type == TextWordType.Space) { ++physicalColumn; wordRectangle = new RectangleF(physicalXPos, lineRectangle.Y, SpaceWidth, lineRectangle.Height); g.FillRectangle(wordBackBrush, wordRectangle); if (TextEditorProperties.ShowSpaces) { DrawSpaceMarker(g, wordForeColor, physicalXPos, lineRectangle.Y); } physicalXPos += SpaceWidth; } else if (currentWord.Type == TextWordType.Tab) { physicalColumn += TextEditorProperties.TabIndent; physicalColumn = (physicalColumn / TextEditorProperties.TabIndent) * TextEditorProperties.TabIndent; // go to next tabstop int physicalTabEnd = ((physicalXPos + MinTabWidth - lineRectangle.X) / WideSpaceWidth / TextEditorProperties.TabIndent) * WideSpaceWidth * TextEditorProperties.TabIndent + lineRectangle.X; physicalTabEnd += WideSpaceWidth * TextEditorProperties.TabIndent; wordRectangle = new RectangleF(physicalXPos, lineRectangle.Y, physicalTabEnd - physicalXPos, lineRectangle.Height); g.FillRectangle(wordBackBrush, wordRectangle); if (TextEditorProperties.ShowTabs) { DrawTabMarker(g, wordForeColor, physicalXPos, lineRectangle.Y); } physicalXPos = physicalTabEnd; } else { int wordWidth = DrawDocumentWord(g, currentWord.Word, new Point(physicalXPos, lineRectangle.Y), currentWord.GetFont(fontContainer), wordForeColor, wordBackBrush); wordRectangle = new RectangleF(physicalXPos, lineRectangle.Y, wordWidth, lineRectangle.Height); physicalXPos += wordWidth; } foreach (TextMarker marker in markers) { if (marker.TextMarkerType != TextMarkerType.SolidBlock) { DrawMarker(g, marker, wordRectangle); } } // draw bracket highlight if (highlight != null) { if (highlight.OpenBrace.Y == lineNumber && highlight.OpenBrace.X == currentWordOffset || highlight.CloseBrace.Y == lineNumber && highlight.CloseBrace.X == currentWordOffset) { DrawBracketHighlight(g, new Rectangle((int)wordRectangle.X, lineRectangle.Y, (int)wordRectangle.Width - 1, lineRectangle.Height - 1)); } } currentWordOffset += currentWord.Length; if (nextCurrentWord != null) { currentWord = nextCurrentWord; nextCurrentWord = null; goto repeatDrawCurrentWord; } } if (physicalXPos < lineRectangle.Right && endColumn >= currentLine.Length) { // draw markers at line end IList markers = Document.MarkerStrategy.GetMarkers(currentLine.Offset + currentLine.Length); foreach (TextMarker marker in markers) { if (marker.TextMarkerType != TextMarkerType.SolidBlock) { DrawMarker(g, marker, new RectangleF(physicalXPos, lineRectangle.Y, WideSpaceWidth, lineRectangle.Height)); } } } return physicalXPos; } int DrawDocumentWord(Graphics g, string word, Point position, Font font, Color foreColor, Brush backBrush) { if (word == null || word.Length == 0) { return 0; } if (word.Length > MaximumWordLength) { int width = 0; for (int i = 0; i < word.Length; i += MaximumWordLength) { Point pos = position; pos.X += width; if (i + MaximumWordLength < word.Length) width += DrawDocumentWord(g, word.Substring(i, MaximumWordLength), pos, font, foreColor, backBrush); else width += DrawDocumentWord(g, word.Substring(i, word.Length - i), pos, font, foreColor, backBrush); } return width; } int wordWidth = MeasureStringWidth(g, word, font); //num = ++num % 3; g.FillRectangle(backBrush, //num == 0 ? Brushes.LightBlue : num == 1 ? Brushes.LightGreen : Brushes.Yellow, new RectangleF(position.X, position.Y, wordWidth + 1, FontHeight)); DrawString(g, word, font, foreColor, position.X, position.Y); return wordWidth; } struct WordFontPair { string word; Font font; public WordFontPair(string word, Font font) { this.word = word; this.font = font; } public override bool Equals(object obj) { WordFontPair myWordFontPair = (WordFontPair)obj; if (!word.Equals(myWordFontPair.word)) return false; return font.Equals(myWordFontPair.font); } public override int GetHashCode() { return word.GetHashCode() ^ font.GetHashCode(); } } Dictionary measureCache = new Dictionary(); // split words after 1000 characters. Fixes GDI+ crash on very longs words, for example // a 100 KB Base64-file without any line breaks. const int MaximumWordLength = 1000; const int MaximumCacheSize = 2000; int MeasureStringWidth(Graphics g, string word, Font font) { int width; if (word == null || word.Length == 0) return 0; if (word.Length > MaximumWordLength) { width = 0; for (int i = 0; i < word.Length; i += MaximumWordLength) { if (i + MaximumWordLength < word.Length) width += MeasureStringWidth(g, word.Substring(i, MaximumWordLength), font); else width += MeasureStringWidth(g, word.Substring(i, word.Length - i), font); } return width; } if (measureCache.TryGetValue(new WordFontPair(word, font), out width)) { return width; } if (measureCache.Count > MaximumCacheSize) { measureCache.Clear(); } // This code here provides better results than MeasureString! // Example line that is measured wrong: // txt.GetPositionFromCharIndex(txt.SelectionStart) // (Verdana 10, highlighting makes GetP... bold) -> note the space between 'x' and '(' // this also fixes "jumping" characters when selecting in non-monospace fonts // [...] // Replaced GDI+ measurement with GDI measurement: faster and even more exact width = TextRenderer.MeasureText(g, word, font, new Size(short.MaxValue, short.MaxValue), textFormatFlags).Width; measureCache.Add(new WordFontPair(word, font), width); return width; } // Important: Some flags combinations work on WinXP, but not on Win2000. // Make sure to test changes here on all operating systems. const TextFormatFlags textFormatFlags = TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix | TextFormatFlags.PreserveGraphicsClipping; #endregion #region Conversion Functions Dictionary> fontBoundCharWidth = new Dictionary>(); public int GetWidth(char ch, Font font) { if (!fontBoundCharWidth.ContainsKey(font)) { fontBoundCharWidth.Add(font, new Dictionary()); } if (!fontBoundCharWidth[font].ContainsKey(ch)) { using (Graphics g = textArea.CreateGraphics()) { return GetWidth(g, ch, font); } } return fontBoundCharWidth[font][ch]; } public int GetWidth(Graphics g, char ch, Font font) { if (!fontBoundCharWidth.ContainsKey(font)) { fontBoundCharWidth.Add(font, new Dictionary()); } if (!fontBoundCharWidth[font].ContainsKey(ch)) { //Console.WriteLine("Calculate character width: " + ch); fontBoundCharWidth[font].Add(ch, MeasureStringWidth(g, ch.ToString(), font)); } return fontBoundCharWidth[font][ch]; } public int GetVisualColumn(int logicalLine, int logicalColumn) { int column = 0; using (Graphics g = textArea.CreateGraphics()) { CountColumns(ref column, 0, logicalColumn, logicalLine, g); } return column; } public int GetVisualColumnFast(LineSegment line, int logicalColumn) { int lineOffset = line.Offset; int tabIndent = Document.TextEditorProperties.TabIndent; int guessedColumn = 0; for (int i = 0; i < logicalColumn; ++i) { char ch; if (i >= line.Length) { ch = ' '; } else { ch = Document.GetCharAt(lineOffset + i); } switch (ch) { case '\t': if (tabIndent != 0) { guessedColumn += tabIndent; guessedColumn = (guessedColumn / tabIndent) * tabIndent; } break; default: ++guessedColumn; break; } } return guessedColumn; } /// /// returns line/column for a visual point position /// public TextLocation GetLogicalPosition(Point mousePosition) { FoldMarker dummy; return GetLogicalColumn(GetLogicalLine(mousePosition.Y), mousePosition.X, out dummy); } /// /// returns line/column for a visual point position /// public TextLocation GetLogicalPosition(int visualPosX, int visualPosY) { FoldMarker dummy; return GetLogicalColumn(GetLogicalLine(visualPosY), visualPosX, out dummy); } /// /// returns line/column for a visual point position /// public FoldMarker GetFoldMarkerFromPosition(int visualPosX, int visualPosY) { FoldMarker foldMarker; GetLogicalColumn(GetLogicalLine(visualPosY), visualPosX, out foldMarker); return foldMarker; } /// /// returns logical line number for a visual point /// public int GetLogicalLine(int visualPosY) { int clickedVisualLine = Math.Max(0, (visualPosY + this.textArea.VirtualTop.Y) / fontHeight); return Document.GetFirstLogicalLine(clickedVisualLine); } internal TextLocation GetLogicalColumn(int lineNumber, int visualPosX, out FoldMarker inFoldMarker) { visualPosX += textArea.VirtualTop.X; inFoldMarker = null; if (lineNumber >= Document.TotalNumberOfLines) { return new TextLocation((int)(visualPosX / WideSpaceWidth), lineNumber); } if (visualPosX <= 0) { return new TextLocation(0, lineNumber); } int start = 0; // column int posX = 0; // visual position int result; using (Graphics g = textArea.CreateGraphics()) { // call GetLogicalColumnInternal to skip over text, // then skip over fold markers // and repeat as necessary. // The loop terminates once the correct logical column is reached in // GetLogicalColumnInternal or inside a fold marker. while (true) { LineSegment line = Document.GetLineSegment(lineNumber); FoldMarker nextFolding = FindNextFoldedFoldingOnLineAfterColumn(lineNumber, start-1); int end = nextFolding != null ? nextFolding.StartColumn : int.MaxValue; result = GetLogicalColumnInternal(g, line, start, end, ref posX, visualPosX); // break when GetLogicalColumnInternal found the result column if (result < end) break; // reached fold marker lineNumber = nextFolding.EndLine; start = nextFolding.EndColumn; int newPosX = posX + 1 + MeasureStringWidth(g, nextFolding.FoldText, TextEditorProperties.FontContainer.RegularFont); if (newPosX >= visualPosX) { inFoldMarker = nextFolding; if (IsNearerToAThanB(visualPosX, posX, newPosX)) return new TextLocation(nextFolding.StartColumn, nextFolding.StartLine); else return new TextLocation(nextFolding.EndColumn, nextFolding.EndLine); } posX = newPosX; } } return new TextLocation(result, lineNumber); } int GetLogicalColumnInternal(Graphics g, LineSegment line, int start, int end, ref int drawingPos, int targetVisualPosX) { if (start == end) return end; Debug.Assert(start < end); Debug.Assert(drawingPos < targetVisualPosX); int tabIndent = Document.TextEditorProperties.TabIndent; /*float spaceWidth = SpaceWidth; float drawingPos = 0; LineSegment currentLine = Document.GetLineSegment(logicalLine); List words = currentLine.Words; if (words == null) return 0; int wordCount = words.Count; int wordOffset = 0; FontContainer fontContainer = TextEditorProperties.FontContainer; */ FontContainer fontContainer = TextEditorProperties.FontContainer; List words = line.Words; if (words == null) return 0; int wordOffset = 0; for (int i = 0; i < words.Count; i++) { TextWord word = words[i]; if (wordOffset >= end) { return wordOffset; } if (wordOffset + word.Length >= start) { int newDrawingPos; switch (word.Type) { case TextWordType.Space: newDrawingPos = drawingPos + spaceWidth; if (newDrawingPos >= targetVisualPosX) return IsNearerToAThanB(targetVisualPosX, drawingPos, newDrawingPos) ? wordOffset : wordOffset+1; break; case TextWordType.Tab: // go to next tab position if (tabIndent == 0 || WideSpaceWidth == 0) return wordOffset; drawingPos = (int)((drawingPos + MinTabWidth) / tabIndent / WideSpaceWidth) * tabIndent * WideSpaceWidth; newDrawingPos = drawingPos + tabIndent * WideSpaceWidth; if (newDrawingPos >= targetVisualPosX) return IsNearerToAThanB(targetVisualPosX, drawingPos, newDrawingPos) ? wordOffset : wordOffset+1; break; case TextWordType.Word: int wordStart = Math.Max(wordOffset, start); int wordLength = Math.Min(wordOffset + word.Length, end) - wordStart; string text = Document.GetText(line.Offset + wordStart, wordLength); Font font = word.GetFont(fontContainer) ?? fontContainer.RegularFont; newDrawingPos = drawingPos + MeasureStringWidth(g, text, font); if (newDrawingPos >= targetVisualPosX) { for (int j = 0; j < text.Length; j++) { newDrawingPos = drawingPos + MeasureStringWidth(g, text[j].ToString(), font); if (newDrawingPos >= targetVisualPosX) { if (IsNearerToAThanB(targetVisualPosX, drawingPos, newDrawingPos)) return wordStart + j; else return wordStart + j + 1; } drawingPos = newDrawingPos; } return wordStart + text.Length; } break; default: throw new NotSupportedException(); } drawingPos = newDrawingPos; } wordOffset += word.Length; } return wordOffset; } static bool IsNearerToAThanB(int num, int a, int b) { return Math.Abs(a - num) < Math.Abs(b - num); } FoldMarker FindNextFoldedFoldingOnLineAfterColumn(int lineNumber, int column) { List list = Document.FoldingManager.GetFoldedFoldingsWithStartAfterColumn(lineNumber, column); if (list.Count != 0) return list[0]; else return null; } const int MinTabWidth = 4; float CountColumns(ref int column, int start, int end, int logicalLine, Graphics g) { if (start > end) throw new ArgumentException("start > end"); if (start == end) return 0; float spaceWidth = SpaceWidth; float drawingPos = 0; int tabIndent = Document.TextEditorProperties.TabIndent; LineSegment currentLine = Document.GetLineSegment(logicalLine); List words = currentLine.Words; if (words == null) return 0; int wordCount = words.Count; int wordOffset = 0; FontContainer fontContainer = TextEditorProperties.FontContainer; for (int i = 0; i < wordCount; i++) { TextWord word = words[i]; if (wordOffset >= end) break; if (wordOffset + word.Length >= start) { switch (word.Type) { case TextWordType.Space: drawingPos += spaceWidth; break; case TextWordType.Tab: // go to next tab position drawingPos = (int)((drawingPos + MinTabWidth) / tabIndent / WideSpaceWidth) * tabIndent * WideSpaceWidth; drawingPos += tabIndent * WideSpaceWidth; break; case TextWordType.Word: int wordStart = Math.Max(wordOffset, start); int wordLength = Math.Min(wordOffset + word.Length, end) - wordStart; string text = Document.GetText(currentLine.Offset + wordStart, wordLength); drawingPos += MeasureStringWidth(g, text, word.GetFont(fontContainer) ?? fontContainer.RegularFont); break; } } wordOffset += word.Length; } for (int j = currentLine.Length; j < end; j++) { drawingPos += WideSpaceWidth; } // add one pixel in column calculation to account for floating point calculation errors column += (int)((drawingPos + 1) / WideSpaceWidth); /* OLD Code (does not work for fonts like Verdana) for (int j = start; j < end; ++j) { char ch; if (j >= line.Length) { ch = ' '; } else { ch = Document.GetCharAt(line.Offset + j); } switch (ch) { case '\t': int oldColumn = column; column += tabIndent; column = (column / tabIndent) * tabIndent; drawingPos += (column - oldColumn) * spaceWidth; break; default: ++column; TextWord word = line.GetWord(j); if (word == null || word.Font == null) { drawingPos += GetWidth(ch, TextEditorProperties.Font); } else { drawingPos += GetWidth(ch, word.Font); } break; } } //*/ return drawingPos; } public int GetDrawingXPos(int logicalLine, int logicalColumn) { List foldings = Document.FoldingManager.GetTopLevelFoldedFoldings(); int i; FoldMarker f = null; // search the last folding that's interresting for (i = foldings.Count - 1; i >= 0; --i) { f = foldings[i]; if (f.StartLine < logicalLine || f.StartLine == logicalLine && f.StartColumn < logicalColumn) { break; } FoldMarker f2 = foldings[i / 2]; if (f2.StartLine > logicalLine || f2.StartLine == logicalLine && f2.StartColumn >= logicalColumn) { i /= 2; } } int lastFolding = 0; int firstFolding = 0; int column = 0; int tabIndent = Document.TextEditorProperties.TabIndent; float drawingPos; Graphics g = textArea.CreateGraphics(); // if no folding is interresting if (f == null || !(f.StartLine < logicalLine || f.StartLine == logicalLine && f.StartColumn < logicalColumn)) { drawingPos = CountColumns(ref column, 0, logicalColumn, logicalLine, g); return (int)(drawingPos - textArea.VirtualTop.X); } // if logicalLine/logicalColumn is in folding if (f.EndLine > logicalLine || f.EndLine == logicalLine && f.EndColumn > logicalColumn) { logicalColumn = f.StartColumn; logicalLine = f.StartLine; --i; } lastFolding = i; // search backwards until a new visible line is reched for (; i >= 0; --i) { f = (FoldMarker)foldings[i]; if (f.EndLine < logicalLine) { // reached the begin of a new visible line break; } } firstFolding = i + 1; if (lastFolding < firstFolding) { drawingPos = CountColumns(ref column, 0, logicalColumn, logicalLine, g); return (int)(drawingPos - textArea.VirtualTop.X); } int foldEnd = 0; drawingPos = 0; for (i = firstFolding; i <= lastFolding; ++i) { f = foldings[i]; drawingPos += CountColumns(ref column, foldEnd, f.StartColumn, f.StartLine, g); foldEnd = f.EndColumn; column += f.FoldText.Length; drawingPos += additionalFoldTextSize; drawingPos += MeasureStringWidth(g, f.FoldText, TextEditorProperties.FontContainer.RegularFont); } drawingPos += CountColumns(ref column, foldEnd, logicalColumn, logicalLine, g); g.Dispose(); return (int)(drawingPos - textArea.VirtualTop.X); } #endregion #region DrawHelper functions void DrawBracketHighlight(Graphics g, Rectangle rect) { g.FillRectangle(BrushRegistry.GetBrush(Color.FromArgb(50, 0, 0, 255)), rect); g.DrawRectangle(Pens.Blue, rect); } void DrawString(Graphics g, string text, Font font, Color color, int x, int y) { TextRenderer.DrawText(g, text, font, new Point(x, y), color, textFormatFlags); } void DrawInvalidLineMarker(Graphics g, int x, int y) { HighlightColor invalidLinesColor = textArea.Document.HighlightingStrategy.GetColorFor("InvalidLines"); DrawString(g, "~", invalidLinesColor.GetFont(TextEditorProperties.FontContainer), invalidLinesColor.Color, x, y); } void DrawSpaceMarker(Graphics g, Color color, int x, int y) { HighlightColor spaceMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("SpaceMarkers"); DrawString(g, "\u00B7", spaceMarkerColor.GetFont(TextEditorProperties.FontContainer), color, x, y); } void DrawTabMarker(Graphics g, Color color, int x, int y) { HighlightColor tabMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("TabMarkers"); DrawString(g, "\u00BB", tabMarkerColor.GetFont(TextEditorProperties.FontContainer), color, x, y); } int DrawEOLMarker(Graphics g, Color color, Brush backBrush, int x, int y) { HighlightColor eolMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("EOLMarkers"); int width = GetWidth('\u00B6', eolMarkerColor.GetFont(TextEditorProperties.FontContainer)); g.FillRectangle(backBrush, new RectangleF(x, y, width, fontHeight)); DrawString(g, "\u00B6", eolMarkerColor.GetFont(TextEditorProperties.FontContainer), color, x, y); return width; } void DrawVerticalRuler(Graphics g, Rectangle lineRectangle) { int xpos = WideSpaceWidth * TextEditorProperties.VerticalRulerRow - textArea.VirtualTop.X; if (xpos <= 0) { return; } HighlightColor vRulerColor = textArea.Document.HighlightingStrategy.GetColorFor("VRuler"); g.DrawLine(BrushRegistry.GetPen(vRulerColor.Color), drawingPosition.Left + xpos, lineRectangle.Top, drawingPosition.Left + xpos, lineRectangle.Bottom); } #endregion } }