// // 2002-2005 AlphaSierraPapa // GNU General Public License // // $Revision$ // using System; using System.Diagnostics; using System.Collections; using System.Collections.Generic; using System.IO; using System.ComponentModel; using System.Drawing; using System.Threading; using System.Drawing.Text; using System.Drawing.Drawing2D; using System.Drawing.Printing; using System.Windows.Forms; using System.Runtime.Remoting; using System.Runtime.InteropServices; using System.Text; using System.Xml; using ICSharpCode.TextEditor.Document; namespace ICSharpCode.TextEditor { /// /// This class paints the textarea. /// public class TextView : AbstractMargin { 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 Highlight Highlight { get { return highlight; } set { highlight = value; } } public override Cursor Cursor { get { return Cursors.IBeam; } } 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) { measureStringFormat.LineAlignment = StringAlignment.Near; measureStringFormat.FormatFlags = StringFormatFlags.MeasureTrailingSpaces | StringFormatFlags.FitBlackBox | StringFormatFlags.NoWrap | StringFormatFlags.NoClip; OptionsChanged(); } static int GetFontHeight(Font font) { int h = font.Height; return (h < 16) ? h + 1 : h; } float spaceWidth; /// /// Gets the width of a space character. /// This value can be quite small in some fonts - consider using WideSpaceWidth instead. /// public float SpaceWidth { get { return spaceWidth; } } float 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 float WideSpaceWidth { get { return wideSpaceWidth; } } Font lastFont; public void OptionsChanged() { this.lastFont = TextEditorProperties.Font; this.fontHeight = GetFontHeight(lastFont); // use mininum 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.Font) { OptionsChanged(); base.TextArea.BeginInvoke(new MethodInvoker(base.TextArea.Refresh)); } int horizontalDelta = (int)(textArea.VirtualTop.X * WideSpaceWidth); 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); } } if (horizontalDelta > 0) { g.ResetClip(); } } 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; } float 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; 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); } HighlightBackground background = (HighlightBackground)textArea.Document.HighlightingStrategy.GetColorFor("Default"); return BrushRegistry.GetBrush(background.BackgroundColor); } float PaintFoldingText(Graphics g, int lineNumber, float 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; float wordWidth = MeasureStringWidth(g, text, textArea.Font); RectangleF rect = new RectangleF(physicalXPos, lineRectangle.Y, wordWidth, lineRectangle.Height - 1); g.FillRectangle(backgroundBrush, rect); physicalColumn += text.Length; g.DrawString(text, textArea.Font, BrushRegistry.GetBrush(drawSelected ? selectionColor.Color : Color.Gray), rect, measureStringFormat); g.DrawRectangle(BrushRegistry.GetPen(drawSelected ? Color.DarkGray : Color.Gray), rect.X, rect.Y, rect.Width, rect.Height); // Bugfix for the problem - of overdrawn right rectangle lines. float ceiling = (float)Math.Ceiling(physicalXPos + wordWidth); if (ceiling - (physicalXPos + wordWidth) < 0.5) { ++ceiling; } return ceiling; } void DrawMarker(Graphics g, TextMarker marker, RectangleF 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 = drawingRect.X - reminder; i < drawingRect.Right + reminder; i+= 6) { g.DrawLine(BrushRegistry.GetPen(marker.Color), i, drawYPos + 3 - 4, i + 3, drawYPos + 1 - 4); 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; } } /// /// 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 List 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; } float PaintLinePart(Graphics g, int lineNumber, int startColumn, int endColumn, Rectangle lineRectangle, float physicalXPos) { bool drawLineMarker = DrawLineMarkerAtLine(lineNumber); Brush bgColorBrush = GetBgColorBrush(lineNumber); Brush backgroundBrush = textArea.Enabled ? bgColorBrush : 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); int logicalColumn = startColumn; Brush selectionBackgroundBrush = BrushRegistry.GetBrush(selectionColor.BackgroundColor); Brush unselectedBackgroundBrush = backgroundBrush; if (currentLine.Words != null) { int startword = 0; // search the first word after startColumn and update physicalColumn if a word is Tab int wordOffset = 0; for (; startword < currentLine.Words.Count; ++startword) { if (wordOffset >= startColumn) { break; } TextWord currentWord = ((TextWord)currentLine.Words[startword]); if (currentWord.Type == TextWordType.Tab) { ++wordOffset; } else if (currentWord.Type == TextWordType.Space) { ++wordOffset; } else { wordOffset += currentWord.Length; } } for (int i = startword; i < currentLine.Words.Count && physicalXPos < lineRectangle.Right; ++i) { // if already all words before endColumn are drawen: break if (logicalColumn >= endColumn) { break; } List markers = Document.MarkerStrategy.GetMarkers(currentLine.Offset + wordOffset); foreach (TextMarker marker in markers) { if (marker.TextMarkerType == TextMarkerType.SolidBlock) { unselectedBackgroundBrush = BrushRegistry.GetBrush(marker.Color); break; } } // TODO: cut the word if startColumn or endColimn is in the word; // needed for foldings wich can start or end in the middle of a word TextWord currentWord = ((TextWord)currentLine.Words[i]); switch (currentWord.Type) { case TextWordType.Space: RectangleF spaceRectangle = new RectangleF(physicalXPos, lineRectangle.Y, (float)Math.Ceiling(SpaceWidth), lineRectangle.Height); Brush spaceBackgroundBrush; Color spaceMarkerForeColor = spaceMarkerColor.Color; if (ColumnRange.WholeColumn.Equals(selectionRange) || logicalColumn >= selectionRange.StartColumn && logicalColumn < selectionRange.EndColumn) { spaceBackgroundBrush = selectionBackgroundBrush; } else { Brush markerBrush = GetMarkerBrushAt(currentLine.Offset + logicalColumn, 1, ref spaceMarkerForeColor, out markers); if (!drawLineMarker && markerBrush != null) { spaceBackgroundBrush = markerBrush; } else if (!drawLineMarker && currentWord.SyntaxColor != null && currentWord.SyntaxColor.HasBackground) { spaceBackgroundBrush = BrushRegistry.GetBrush(currentWord.SyntaxColor.BackgroundColor); } else { spaceBackgroundBrush = unselectedBackgroundBrush; } } g.FillRectangle(spaceBackgroundBrush, spaceRectangle); if (TextEditorProperties.ShowSpaces) { DrawSpaceMarker(g, spaceMarkerForeColor, physicalXPos, lineRectangle.Y); } foreach (TextMarker marker in markers) { if (marker.TextMarkerType != TextMarkerType.SolidBlock) { DrawMarker(g, marker, spaceRectangle); } } physicalXPos += SpaceWidth; ++logicalColumn; ++physicalColumn; break; case TextWordType.Tab: physicalColumn += TextEditorProperties.TabIndent; physicalColumn = (physicalColumn / TextEditorProperties.TabIndent) * TextEditorProperties.TabIndent; // go to next tabstop float physicalTabEnd = (int)((physicalXPos + MinTabWidth - lineRectangle.X) / WideSpaceWidth / TextEditorProperties.TabIndent) * WideSpaceWidth * TextEditorProperties.TabIndent + lineRectangle.X; physicalTabEnd += WideSpaceWidth * TextEditorProperties.TabIndent; RectangleF tabRectangle = new RectangleF(physicalXPos, lineRectangle.Y, (float)Math.Ceiling(physicalTabEnd - physicalXPos), lineRectangle.Height); Color tabMarkerForeColor = tabMarkerColor.Color; if (ColumnRange.WholeColumn.Equals(selectionRange) || logicalColumn >= selectionRange.StartColumn && logicalColumn <= selectionRange.EndColumn - 1) { spaceBackgroundBrush = selectionBackgroundBrush; } else { Brush markerBrush = GetMarkerBrushAt(currentLine.Offset + logicalColumn, 1, ref tabMarkerForeColor, out markers); if (!drawLineMarker && markerBrush != null) { spaceBackgroundBrush = markerBrush; } else if (!drawLineMarker && currentWord.SyntaxColor != null && currentWord.SyntaxColor.HasBackground) { spaceBackgroundBrush = BrushRegistry.GetBrush(currentWord.SyntaxColor.BackgroundColor); } else { spaceBackgroundBrush = unselectedBackgroundBrush; } } g.FillRectangle(spaceBackgroundBrush, tabRectangle); if (TextEditorProperties.ShowTabs) { DrawTabMarker(g, tabMarkerForeColor, physicalXPos, lineRectangle.Y); } foreach (TextMarker marker in markers) { if (marker.TextMarkerType != TextMarkerType.SolidBlock) { DrawMarker(g, marker, tabRectangle); } } physicalXPos = physicalTabEnd; ++logicalColumn; break; case TextWordType.Word: string word = currentWord.Word; float lastPos = physicalXPos; Color wordForeColor = currentWord.Color; Brush bgMarkerBrush = GetMarkerBrushAt(currentLine.Offset + logicalColumn, word.Length, ref wordForeColor, out markers); Brush wordBackgroundBrush; if (!drawLineMarker && bgMarkerBrush != null) { wordBackgroundBrush = bgMarkerBrush; } else if (!drawLineMarker && currentWord.SyntaxColor.HasBackground) { wordBackgroundBrush = BrushRegistry.GetBrush(currentWord.SyntaxColor.BackgroundColor); } else { wordBackgroundBrush = unselectedBackgroundBrush; } if (ColumnRange.WholeColumn.Equals(selectionRange) || selectionRange.EndColumn - 1 >= word.Length + logicalColumn && selectionRange.StartColumn <= logicalColumn) { physicalXPos += DrawDocumentWord(g, word, new Point((int)physicalXPos, lineRectangle.Y), currentWord.Font, selectionColor.HasForgeground ? selectionColor.Color : wordForeColor, selectionBackgroundBrush); } else { if (ColumnRange.NoColumn.Equals(selectionRange) /* || selectionRange.StartColumn > logicalColumn + word.Length || selectionRange.EndColumn - 1 <= logicalColumn */) { physicalXPos += DrawDocumentWord(g, word, new Point((int)physicalXPos, lineRectangle.Y), currentWord.Font, wordForeColor, wordBackgroundBrush); } else { int offset1 = Math.Min(word.Length, Math.Max(0, selectionRange.StartColumn - logicalColumn )); int offset2 = Math.Max(offset1, Math.Min(word.Length, selectionRange.EndColumn - logicalColumn)); physicalXPos += DrawDocumentWord(g, word.Substring(0, offset1), new Point((int)physicalXPos, lineRectangle.Y), currentWord.Font, wordForeColor, wordBackgroundBrush); physicalXPos += DrawDocumentWord(g, word.Substring(offset1, offset2 - offset1), new Point((int)physicalXPos, lineRectangle.Y), currentWord.Font, selectionColor.HasForgeground ? selectionColor.Color : wordForeColor, selectionBackgroundBrush); physicalXPos += DrawDocumentWord(g, word.Substring(offset2), new Point((int)physicalXPos, lineRectangle.Y), currentWord.Font, wordForeColor, wordBackgroundBrush); } } foreach (TextMarker marker in markers) { if (marker.TextMarkerType != TextMarkerType.SolidBlock) { DrawMarker(g, marker, new RectangleF(lastPos, lineRectangle.Y, (physicalXPos - lastPos), lineRectangle.Height)); } } // draw bracket highlight if (highlight != null) { if (highlight.OpenBrace.Y == lineNumber && highlight.OpenBrace.X == logicalColumn || highlight.CloseBrace.Y == lineNumber && highlight.CloseBrace.X == logicalColumn) { DrawBracketHighlight(g, new Rectangle((int)lastPos, lineRectangle.Y, (int)(physicalXPos - lastPos) - 1, lineRectangle.Height - 1)); } } physicalColumn += word.Length; logicalColumn += word.Length; break; } } } return physicalXPos; } //int num; float DrawDocumentWord(Graphics g, string word, Point position, Font font, Color foreColor, Brush backBrush) { if (word == null || word.Length == 0) { return 0f; } float 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, (float)Math.Ceiling(wordWidth + 1), FontHeight)); g.DrawString(word, font, BrushRegistry.GetBrush(foreColor), position.X, position.Y, measureStringFormat); 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(); float MeasureStringWidth(Graphics g, string word, Font font) { if (word == null || word.Length == 0) return 0; float width; if (measureCache.TryGetValue(new WordFontPair(word, font), out width)) { return width; } if (measureCache.Count > 1000) { 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 Rectangle rect = new Rectangle(0, 0, 32768, 1000); CharacterRange[] ranges = { new CharacterRange(0, word.Length) }; Region[] regions = new Region[1]; measureStringFormat.SetMeasurableCharacterRanges (ranges); regions = g.MeasureCharacterRanges (word, font, rect, measureStringFormat); width = regions[0].GetBounds(g).Right; measureCache.Add(new WordFontPair(word, font), width); return width; } #endregion #region Conversion Functions Dictionary> fontBoundCharWidth = new Dictionary>(); public float 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 (float)fontBoundCharWidth[font][ch]; } public float 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 (float)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': guessedColumn += tabIndent; guessedColumn = (guessedColumn / tabIndent) * tabIndent; break; default: ++guessedColumn; break; } } return guessedColumn; } /// /// returns line/column for a visual point position /// public Point GetLogicalPosition(int xPos, int yPos) { xPos += (int)(textArea.VirtualTop.X * WideSpaceWidth); int clickedVisualLine = Math.Max(0, (yPos + this.textArea.VirtualTop.Y) / fontHeight); int logicalLine = Document.GetFirstLogicalLine(clickedVisualLine); Point pos = GetLogicalColumn(logicalLine, xPos); return pos; } /// /// returns logical line number for a visual point /// public int GetLogicalLine(Point mousepos) { int physicalLine = FirstPhysicalLine + (int)(mousepos.Y / FontHeight); return Document.GetFirstLogicalLine(physicalLine); } public Point GetLogicalColumn(int firstLogicalLine, int xPos) { float spaceWidth = WideSpaceWidth; LineSegment line = firstLogicalLine < Document.TotalNumberOfLines ? Document.GetLineSegment(firstLogicalLine) : null; if (line == null) { return new Point((int)(xPos / spaceWidth), firstLogicalLine); } int lineNumber = firstLogicalLine; int tabIndent = Document.TextEditorProperties.TabIndent; int column = 0; int logicalColumn = 0; float paintPos = 0; List starts = textArea.Document.FoldingManager.GetFoldedFoldingsWithStart(lineNumber); while (true) { // save current paint position float oldPaintPos = paintPos; // search for folding if (starts.Count > 0) { foreach (FoldMarker folding in starts) { if (folding.IsFolded && logicalColumn >= folding.StartColumn && (logicalColumn < folding.EndColumn || lineNumber != folding.EndLine)) { column += folding.FoldText.Length; paintPos += folding.FoldText.Length * spaceWidth; // special case when xPos is inside the fold marker if (xPos <= paintPos - (paintPos - oldPaintPos) / 2) { return new Point(logicalColumn, lineNumber); } logicalColumn = folding.EndColumn; if (lineNumber != folding.EndLine) { lineNumber = folding.EndLine; line = Document.GetLineSegment(lineNumber); starts = textArea.Document.FoldingManager.GetFoldedFoldingsWithStart(lineNumber); } break; } } } // --> no folding, going on with the count char ch = logicalColumn >= line.Length ? ' ' : Document.GetCharAt(line.Offset + logicalColumn); switch (ch) { case '\t': int oldColumn = column; column += tabIndent; column = (column / tabIndent) * tabIndent; paintPos += (column - oldColumn) * spaceWidth; break; default: paintPos += GetWidth(ch, TextEditorProperties.Font); ++column; break; } // when the paint position is reached, give it back otherwise advance to the next char if (xPos <= paintPos - (paintPos - oldPaintPos) / 2) { return new Point(logicalColumn, lineNumber); } ++logicalColumn; } } /// /// returns line/column for a visual point position /// public FoldMarker GetFoldMarkerFromPosition(int xPos, int yPos) { xPos += (int)(textArea.VirtualTop.X * WideSpaceWidth); int clickedVisualLine = (yPos + this.textArea.VirtualTop.Y) / fontHeight; int logicalLine = Document.GetFirstLogicalLine(clickedVisualLine); return GetFoldMarkerFromColumn(logicalLine, xPos); } FoldMarker GetFoldMarkerFromColumn(int firstLogicalLine, int xPos) { LineSegment line = firstLogicalLine < Document.TotalNumberOfLines ? Document.GetLineSegment(firstLogicalLine) : null; if (line == null) { return null; } int lineNumber = firstLogicalLine; int tabIndent = Document.TextEditorProperties.TabIndent; int column = 0; int logicalColumn = 0; float paintPos = 0; List starts = textArea.Document.FoldingManager.GetFoldedFoldingsWithStart(lineNumber); while (true) { // save current paint position float oldPaintPos = paintPos; // search for folding if (starts.Count > 0) { foreach (FoldMarker folding in starts) { if (folding.IsFolded && logicalColumn >= folding.StartColumn && (logicalColumn < folding.EndColumn || lineNumber != folding.EndLine)) { column += folding.FoldText.Length; paintPos += folding.FoldText.Length * WideSpaceWidth; // special case when xPos is inside the fold marker if (xPos <= paintPos) { return folding; } logicalColumn = folding.EndColumn; if (lineNumber != folding.EndLine) { lineNumber = folding.EndLine; line = Document.GetLineSegment(lineNumber); starts = textArea.Document.FoldingManager.GetFoldedFoldingsWithStart(lineNumber); } break; } } } // --> no folding, going on with the count char ch = logicalColumn >= line.Length ? ' ' : Document.GetCharAt(line.Offset + logicalColumn); switch (ch) { case '\t': int oldColumn = column; column += tabIndent; column = (column / tabIndent) * tabIndent; paintPos += (column - oldColumn) * WideSpaceWidth; break; default: paintPos += GetWidth(ch, TextEditorProperties.Font); ++column; break; } // when the paint position is reached, give it back otherwise advance to the next char if (xPos <= paintPos - (paintPos - oldPaintPos) / 2) { return null; } ++logicalColumn; } } const float MinTabWidth = 4; float CountColumns(ref int column, int start, int end, int logicalLine, Graphics g) { if (start > end) throw new ArgumentException("start > end"); 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; for (int i = 0; i < wordCount; i++) { TextWord word = words[i]; if (wordOffset >= end) break; if (wordOffset + word.Length < start) continue; 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.Font ?? TextEditorProperties.Font); 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 * WideSpaceWidth); } // 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 * WideSpaceWidth); } 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 += MeasureStringWidth(g, f.FoldText, TextEditorProperties.Font); } drawingPos += CountColumns(ref column, foldEnd, logicalColumn, logicalLine, g); g.Dispose(); return (int)(drawingPos - textArea.VirtualTop.X * WideSpaceWidth); } #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 DrawInvalidLineMarker(Graphics g, float x, float y) { HighlightColor invalidLinesColor = textArea.Document.HighlightingStrategy.GetColorFor("InvalidLines"); g.DrawString("~", invalidLinesColor.Font, BrushRegistry.GetBrush(invalidLinesColor.Color), x, y, measureStringFormat); } void DrawSpaceMarker(Graphics g, Color color, float x, float y) { HighlightColor spaceMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("SpaceMarkers"); g.DrawString("\u00B7", spaceMarkerColor.Font, BrushRegistry.GetBrush(color), x, y, measureStringFormat); } void DrawTabMarker(Graphics g, Color color, float x, float y) { HighlightColor tabMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("TabMarkers"); g.DrawString("\u00BB", tabMarkerColor.Font, BrushRegistry.GetBrush(color), x, y, measureStringFormat); } float DrawEOLMarker(Graphics g, Color color, Brush backBrush, float x, float y) { float width = GetWidth('\u00B6', TextEditorProperties.Font); g.FillRectangle(backBrush, new RectangleF(x, y, width, fontHeight)); HighlightColor eolMarkerColor = textArea.Document.HighlightingStrategy.GetColorFor("EOLMarkers"); g.DrawString("\u00B6", eolMarkerColor.Font, BrushRegistry.GetBrush(color), x, y, measureStringFormat); return width; } void DrawVerticalRuler(Graphics g, Rectangle lineRectangle) { if (TextEditorProperties.VerticalRulerRow < textArea.VirtualTop.X) { return; } HighlightColor vRulerColor = textArea.Document.HighlightingStrategy.GetColorFor("VRuler"); int xpos = (int)(drawingPosition.Left + WideSpaceWidth * (TextEditorProperties.VerticalRulerRow - textArea.VirtualTop.X)); g.DrawLine(BrushRegistry.GetPen(vRulerColor.Color), xpos, lineRectangle.Top, xpos, lineRectangle.Bottom); } #endregion } }