for WinForms
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

552 lines
21 KiB

// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Mike Krüger" email="mike@icsharpcode.net"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using ICSharpCode.TextEditor.Document;
using ICSharpCode.TextEditor.Util;
namespace ICSharpCode.TextEditor
{
/// <summary>
/// This class paints the textarea.
/// </summary>
[ToolboxItem(defaultType: false)]
public class TextAreaControl : Panel
{
private const int LineLengthCacheAdditionalSize = 100;
private readonly MouseWheelHandler mouseWheelHandler = new MouseWheelHandler();
private readonly int scrollMarginHeight = 3;
private bool adjustScrollBarsOnNextUpdate;
private bool disposed;
private HRuler hRuler;
private int[] lineLengthCache;
private TextEditorControl motherTextEditorControl;
private Point scrollToPosOnNextUpdate;
public TextAreaControl(TextEditorControl motherTextEditorControl)
{
this.motherTextEditorControl = motherTextEditorControl;
TextArea = new TextArea(motherTextEditorControl, this);
Controls.Add(TextArea);
VScrollBar.ValueChanged += VScrollBarValueChanged;
Controls.Add(VScrollBar);
HScrollBar.ValueChanged += HScrollBarValueChanged;
Controls.Add(HScrollBar);
ResizeRedraw = true;
Document.TextContentChanged += DocumentTextContentChanged;
Document.DocumentChanged += AdjustScrollBarsOnDocumentChange;
Document.UpdateCommited += DocumentUpdateCommitted;
}
public TextArea TextArea { get; }
public SelectionManager SelectionManager => TextArea.SelectionManager;
public Caret Caret => TextArea.Caret;
[Browsable(browsable: false)]
public IDocument Document => motherTextEditorControl?.Document;
public ITextEditorProperties TextEditorProperties => motherTextEditorControl?.TextEditorProperties;
public VScrollBar VScrollBar { get; private set; } = new VScrollBar();
public HScrollBar HScrollBar { get; private set; } = new HScrollBar();
public bool DoHandleMousewheel { get; set; } = true;
protected override void Dispose(bool disposing)
{
if (disposing)
if (!disposed)
{
disposed = true;
Document.TextContentChanged -= DocumentTextContentChanged;
Document.DocumentChanged -= AdjustScrollBarsOnDocumentChange;
Document.UpdateCommited -= DocumentUpdateCommitted;
motherTextEditorControl = null;
if (VScrollBar != null)
{
VScrollBar.Dispose();
VScrollBar = null;
}
if (HScrollBar != null)
{
HScrollBar.Dispose();
HScrollBar = null;
}
if (hRuler != null)
{
hRuler.Dispose();
hRuler = null;
}
}
base.Dispose(disposing);
}
private void DocumentTextContentChanged(object sender, EventArgs e)
{
// after the text content is changed abruptly, we need to validate the
// caret position - otherwise the caret position is invalid for a short amount
// of time, which can break client code that expects that the caret position is always valid
Caret.ValidateCaretPos();
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
UpdateLayout();
}
private void AdjustScrollBarsOnDocumentChange(object sender, DocumentEventArgs e)
{
if (motherTextEditorControl.IsInUpdate == false)
{
AdjustScrollBarsClearCache();
UpdateLayout();
}
else
{
adjustScrollBarsOnNextUpdate = true;
}
}
private void DocumentUpdateCommitted(object sender, EventArgs e)
{
if (motherTextEditorControl.IsInUpdate == false)
{
Caret.ValidateCaretPos();
// AdjustScrollBarsOnCommittedUpdate
if (!scrollToPosOnNextUpdate.IsEmpty)
ScrollTo(scrollToPosOnNextUpdate.Y, scrollToPosOnNextUpdate.X);
if (adjustScrollBarsOnNextUpdate)
{
AdjustScrollBarsClearCache();
UpdateLayout();
}
}
}
private void AdjustScrollBarsClearCache()
{
if (lineLengthCache != null)
{
if (lineLengthCache.Length < Document.TotalNumberOfLines + 2*LineLengthCacheAdditionalSize)
lineLengthCache = null;
else
Array.Clear(lineLengthCache, index: 0, lineLengthCache.Length);
}
}
public void UpdateLayout()
{
if (TextArea == null)
return;
adjustScrollBarsOnNextUpdate = false;
var view = TextArea.TextView;
var currentVisibilites = GetScrollVisibilities(HScrollBar.Visible, VScrollBar.Visible);
var visited = new HashSet<ScrollVisibilities>();
// Start walking through any layout state transitions and see whether it ends up in a stable state.
var fromVisibilities = currentVisibilites;
while (true)
{
if (!visited.Add(fromVisibilities))
// Returning to a visited state -- unstable, so make no change
break;
var bounds = Measure(fromVisibilities);
var to = ComputeScrollBarVisibilities(bounds.textArea.Size);
if (to.visibilities == fromVisibilities)
{
// Layout is stable -- apply it
ApplyLayout(fromVisibilities, to.maxLength, bounds);
break;
}
fromVisibilities = to.visibilities;
}
ScrollVisibilities GetScrollVisibilities(bool h, bool v)
{
return (h ? ScrollVisibilities.H : ScrollVisibilities.None)
| (v ? ScrollVisibilities.V : ScrollVisibilities.None);
}
(Rectangle hRule, Rectangle textControl, Rectangle textArea, Rectangle hScroll, Rectangle vScroll)
Measure(ScrollVisibilities scrollVisibilities)
{
var v = scrollVisibilities.HasFlag(ScrollVisibilities.V);
var h = scrollVisibilities.HasFlag(ScrollVisibilities.H);
var vScrollSize = v ? SystemInformation.VerticalScrollBarArrowHeight : 0;
var hScrollSize = h ? SystemInformation.HorizontalScrollBarArrowWidth : 0;
var x0 = TextArea.LeftMargins.Where(margin => margin.IsVisible).Sum(margin => margin.Width);
var hRuleBounds = hRuler != null
? new Rectangle(
x: 0,
y: 0,
Width - vScrollSize,
TextArea.TextView.FontHeight)
: default;
var textControlBounds = new Rectangle(
x: 0,
hRuleBounds.Bottom,
Width - vScrollSize,
Height - hRuleBounds.Bottom - hScrollSize);
var textAreaBounds = new Rectangle(
x0,
hRuleBounds.Bottom,
Width - x0 - vScrollSize,
Height - hRuleBounds.Bottom - hScrollSize);
var vScrollBounds = v
? new Rectangle(
textAreaBounds.Right,
y: 0,
SystemInformation.HorizontalScrollBarArrowWidth,
Height - hScrollSize)
: default;
var hScrollBounds = h
? new Rectangle(
x: 0,
textAreaBounds.Bottom,
Width - vScrollSize,
SystemInformation.VerticalScrollBarArrowHeight)
: default;
return (hRuleBounds, textControlBounds, textAreaBounds, hScrollBounds, vScrollBounds);
}
(ScrollVisibilities visibilities, int maxLength) ComputeScrollBarVisibilities(Size size)
{
var visibleLineCount = 1 + size.Height/view.FontHeight;
var visibleColumnCount = size.Width/view.WideSpaceWidth - 1;
var firstLine = view.FirstVisibleLine;
var lastLine = Document.GetFirstLogicalLine(firstLine + visibleLineCount);
if (lastLine >= Document.TotalNumberOfLines)
lastLine = Document.TotalNumberOfLines - 1;
if (lineLengthCache == null || lineLengthCache.Length <= lastLine)
lineLengthCache = new int[lastLine + LineLengthCacheAdditionalSize];
var maxLength = 0;
for (var lineNumber = firstLine; lineNumber <= lastLine; lineNumber++)
{
var lineSegment = Document.GetLineSegment(lineNumber);
if (Document.FoldingManager.IsLineVisible(lineNumber))
{
if (lineLengthCache[lineNumber] > 0)
{
maxLength = Math.Max(maxLength, lineLengthCache[lineNumber]);
}
else
{
var visualLength = view.GetVisualColumnFast(lineSegment, lineSegment.Length);
lineLengthCache[lineNumber] = Math.Max(1, visualLength);
maxLength = Math.Max(maxLength, visualLength);
}
}
}
var vScrollBarVisible = VScrollBar.Value != 0 || TextArea.Document.TotalNumberOfLines >= visibleLineCount;
var hScrollBarVisible = HScrollBar.Value != 0 || maxLength > visibleColumnCount;
return (GetScrollVisibilities(hScrollBarVisible, vScrollBarVisible), maxLength);
}
void ApplyLayout(ScrollVisibilities scrollVisibilities, int maxColumn, (Rectangle hRule, Rectangle textControl, Rectangle textArea, Rectangle hScroll, Rectangle vScroll) bounds)
{
var visibleColumnCount = bounds.textArea.Width/view.WideSpaceWidth - 1;
VScrollBar.Minimum = 0;
VScrollBar.Maximum = TextArea.MaxVScrollValue; // number of visible lines in document (folding!)
VScrollBar.LargeChange = Math.Max(0, bounds.textArea.Height);
VScrollBar.SmallChange = Math.Max(0, view.FontHeight);
VScrollBar.Visible = scrollVisibilities.HasFlag(ScrollVisibilities.V);
VScrollBar.Bounds = bounds.vScroll;
HScrollBar.Minimum = 0;
HScrollBar.Maximum = Math.Max(maxColumn, visibleColumnCount - 1);
HScrollBar.LargeChange = Math.Max(0, visibleColumnCount - 1);
HScrollBar.SmallChange = 4;
HScrollBar.Visible = scrollVisibilities.HasFlag(ScrollVisibilities.H);
HScrollBar.Bounds = bounds.hScroll;
if (hRuler != null)
hRuler.Bounds = bounds.hRule;
TextArea.Bounds = bounds.textControl;
}
}
public void OptionsChanged()
{
TextArea.OptionsChanged();
if (TextArea.TextEditorProperties.ShowHorizontalRuler)
{
if (hRuler == null)
{
hRuler = new HRuler(TextArea);
Controls.Add(hRuler);
UpdateLayout();
}
else
{
hRuler.Invalidate();
}
}
else
{
if (hRuler != null)
{
Controls.Remove(hRuler);
hRuler.Dispose();
hRuler = null;
UpdateLayout();
}
}
UpdateLayout();
}
private void VScrollBarValueChanged(object sender, EventArgs e)
{
TextArea.VirtualTop = new Point(TextArea.VirtualTop.X, VScrollBar.Value);
TextArea.Invalidate();
UpdateLayout();
}
private void HScrollBarValueChanged(object sender, EventArgs e)
{
TextArea.VirtualTop = new Point(HScrollBar.Value*TextArea.TextView.WideSpaceWidth, TextArea.VirtualTop.Y);
TextArea.Invalidate();
}
public void HandleMouseWheel(MouseEventArgs e)
{
var scrollDistance = mouseWheelHandler.GetScrollAmount(e);
if (scrollDistance == 0)
return;
if (ModifierKeys.HasFlag(Keys.Control) && TextEditorProperties.MouseWheelTextZoom)
{
if (scrollDistance > 0)
motherTextEditorControl.Font = new Font(
motherTextEditorControl.Font.Name,
motherTextEditorControl.Font.Size + 1);
else
motherTextEditorControl.Font = new Font(
motherTextEditorControl.Font.Name,
Math.Max(6, motherTextEditorControl.Font.Size - 1));
}
else
{
if (TextEditorProperties.MouseWheelScrollDown)
scrollDistance = -scrollDistance;
if (ModifierKeys.HasFlag(Keys.Shift))
{
var newValue = HScrollBar.Value + HScrollBar.SmallChange*scrollDistance;
HScrollBar.Value = Math.Max(HScrollBar.Minimum, Math.Min(HScrollBar.Maximum - HScrollBar.LargeChange + 1, newValue));
}
else
{
var newValue = VScrollBar.Value + VScrollBar.SmallChange*scrollDistance;
VScrollBar.Value = Math.Max(VScrollBar.Minimum, Math.Min(VScrollBar.Maximum - VScrollBar.LargeChange + 1, newValue));
}
}
}
protected override void OnMouseWheel(MouseEventArgs e)
{
base.OnMouseWheel(e);
if (DoHandleMousewheel)
HandleMouseWheel(e);
}
public void ScrollToCaret()
{
ScrollTo(TextArea.Caret.Line, TextArea.Caret.Column);
}
public void ScrollTo(int line, int column)
{
if (motherTextEditorControl.IsInUpdate)
{
scrollToPosOnNextUpdate = new Point(column, line);
return;
}
scrollToPosOnNextUpdate = Point.Empty;
ScrollTo(line);
var curCharMin = HScrollBar.Value - HScrollBar.Minimum;
var curCharMax = curCharMin + TextArea.TextView.VisibleColumnCount;
var pos = TextArea.TextView.GetVisualColumn(line, column);
if (TextArea.TextView.VisibleColumnCount < 0)
{
HScrollBar.Value = 0;
}
else
{
if (pos < curCharMin)
{
HScrollBar.Value = Math.Max(0, pos - scrollMarginHeight);
}
else
{
if (pos > curCharMax)
HScrollBar.Value = Math.Max(0, Math.Min(HScrollBar.Maximum, pos - TextArea.TextView.VisibleColumnCount + scrollMarginHeight));
}
}
}
/// <summary>
/// Ensure that <paramref name="line" /> is visible.
/// </summary>
public void ScrollTo(int line)
{
line = Math.Max(0, Math.Min(Document.TotalNumberOfLines - 1, line));
line = Document.GetVisibleLine(line);
var curLineMin = TextArea.TextView.FirstPhysicalLine;
if (TextArea.TextView.LineHeightRemainder > 0)
curLineMin++;
if (line - scrollMarginHeight + 3 < curLineMin)
{
VScrollBar.Value = Math.Max(0, Math.Min(VScrollBar.Maximum, (line - scrollMarginHeight + 3)*TextArea.TextView.FontHeight));
VScrollBarValueChanged(this, EventArgs.Empty);
}
else
{
var curLineMax = curLineMin + TextArea.TextView.VisibleLineCount;
if (line + scrollMarginHeight - 1 > curLineMax)
{
if (TextArea.TextView.VisibleLineCount == 1)
VScrollBar.Value = Math.Max(0, Math.Min(VScrollBar.Maximum, (line - scrollMarginHeight - 1)*TextArea.TextView.FontHeight));
else
VScrollBar.Value = Math.Min(
VScrollBar.Maximum,
(line - TextArea.TextView.VisibleLineCount + scrollMarginHeight - 1)*TextArea.TextView.FontHeight);
VScrollBarValueChanged(this, EventArgs.Empty);
}
}
}
/// <summary>
/// Scroll so that the specified line is centered.
/// </summary>
/// <param name="line">Line to center view on</param>
/// <param name="treshold">
/// If this action would cause scrolling by less than or equal to
/// <paramref name="treshold" /> lines in any direction, don't scroll.
/// Use -1 to always center the view.
/// </param>
public void CenterViewOn(int line, int treshold)
{
line = Math.Max(0, Math.Min(Document.TotalNumberOfLines - 1, line));
// convert line to visible line:
line = Document.GetVisibleLine(line);
// subtract half the visible line count
line -= TextArea.TextView.VisibleLineCount/2;
var curLineMin = TextArea.TextView.FirstPhysicalLine;
if (TextArea.TextView.LineHeightRemainder > 0)
curLineMin++;
if (Math.Abs(curLineMin - line) > treshold)
{
// scroll:
VScrollBar.Value = Math.Max(0, Math.Min(VScrollBar.Maximum, (line - scrollMarginHeight + 3)*TextArea.TextView.FontHeight));
VScrollBarValueChanged(this, EventArgs.Empty);
}
}
public void JumpTo(int line)
{
line = Math.Max(0, Math.Min(line, Document.TotalNumberOfLines - 1));
var text = Document.GetText(Document.GetLineSegment(line));
JumpTo(line, text.Length - text.TrimStart().Length);
}
public void JumpTo(int line, int column)
{
TextArea.Focus();
TextArea.SelectionManager.ClearSelection();
TextArea.Caret.Position = new TextLocation(column, line);
TextArea.SetDesiredColumn();
ScrollToCaret();
}
public event MouseEventHandler ShowContextMenu;
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x007B)
if (ShowContextMenu != null)
{
Point location = m.LParam.ToPoint();
if (location.X == -1 && location.Y == -1)
{
var pos = Caret.ScreenPosition;
ShowContextMenu?.Invoke(this, new MouseEventArgs(MouseButtons.None, clicks: 0, pos.X, pos.Y + TextArea.TextView.FontHeight, delta: 0));
}
else
{
var pos = PointToClient(location);
ShowContextMenu?.Invoke(this, new MouseEventArgs(MouseButtons.Right, clicks: 1, pos.X, pos.Y, delta: 0));
}
}
base.WndProc(ref m);
}
protected override void OnEnter(EventArgs e)
{
// SD2-1072 - Make sure the caret line is valid if anyone
// has handlers for the Enter event.
Caret.ValidateCaretPos();
base.OnEnter(e);
}
[Flags]
private enum ScrollVisibilities
{
None = 0,
H = 1,
V = 2
}
}
}