// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Text; using System.Windows.Forms; using System.Windows.Forms.VisualStyles; using System.Runtime.InteropServices; using System.IO; namespace UnrealControls { /// /// This class represents a view into an output document. /// public partial class OutputWindowView : UserControl { const TextFormatFlags TXT_FLAGS = TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix | TextFormatFlags.PreserveGraphicsClipping; static readonly Color INVALID_SEARCH_BACKCOLOR = Color.FromArgb(255, 102, 102); static readonly Color INVALID_SEARCH_FORECOLOR = Color.White; static readonly Color HAS_FINDTEXT_BACKCOLOR = Color.FromArgb(239, 248, 255); //event delegates EventHandler mOnDocChanged; EventHandler mOnSelectionChanged; // controls XButton mXButtonFind; CheckBox mCheckBox_MatchCase; //regular local variables OutputWindowDocument mDocument; Size mFontDimensions; TextLocation mCaretPosition; TextLocation mSelectionStart; TextLocation mCurrentDocumentSize; Point mLastMousePos; Color mFindTextLineHighlight = HAS_FINDTEXT_BACKCOLOR; Color mFindTextBackColor = Color.Yellow; Color mFindTextForeColor = Color.Black; Size mLastSize; bool mIsSelecting; bool mAutoScroll; const int VK_SHIFT = 0x10; #if !__MonoCS__ [DllImport("user32.dll")] extern static ushort GetAsyncKeyState(int vKey); #endif /// /// Gets the maximum number of lines that can be drawn in the control. /// private int DisplayableVerticalLines { get { int ClientHeight = (this.Size.IsEmpty ? mLastSize.Height : this.Height) - mHScrollBar.Height; if(mToolStrip_Find.Visible) { ClientHeight -= mToolStrip_Find.Height; } return ClientHeight / this.Font.Height; } } /// /// Gets the width of the actual client area in pixels. /// private int WidthInDisplayablePixels { get { return (this.Size.IsEmpty ? mLastSize.Width : this.Width) - mVScrollBar.Width; } } /// /// Gets the maximum number of columns/characters that can be drawn in the control. /// private int DisplayableCharacters { get { return this.WidthInDisplayablePixels / mFontDimensions.Width; } } /// /// Gets/Sets the document associated with the view. /// public OutputWindowDocument Document { get { return mDocument; } set { if(mDocument != value) { if(mDocument != null) { mDocument.Modified -= new EventHandler(mDocument_Modified); } mDocument = value; if(mDocument != null) { mDocument.Modified += new EventHandler(mDocument_Modified); } // must have this here to explicitly reset scroll bar position mHScrollBar.Value = 0; mVScrollBar.Value = 0; mCaretPosition = mSelectionStart = new TextLocation(0, 0); OnDocumentChanged(new EventArgs()); } } } /// /// Gets/Sets the font used to draw text. /// public override Font Font { get { return base.Font; } set { base.Font = value; using(Graphics Gfx = this.CreateGraphics()) { mFontDimensions = TextRenderer.MeasureText(Gfx, " ", this.Font, new Size(short.MaxValue, short.MaxValue), TXT_FLAGS); Size Temp = TextRenderer.MeasureText(Gfx, "x", this.Font, new Size(short.MaxValue, short.MaxValue), TXT_FLAGS); mFontDimensions.Width = Math.Max(mFontDimensions.Width, Temp.Width); } } } /// /// Gets/Sets whether or not the control supports autoscrolling output. /// [Description("Controls whether or not the control autoscrolls when text is appended.")] public override bool AutoScroll { get { return mAutoScroll; } set { mAutoScroll = value; } } /// /// Gets/Sets the start location of the currently selected text. /// [Browsable(false)] public TextLocation SelectionStart { get { return mSelectionStart; } set { if(IsValidTextLocation(value)) { mSelectionStart = value; OnSelectionChanged(new EventArgs()); Invalidate(); } } } /// /// Gets/Sets the end location of the currently selected text. /// [Browsable(false)] public TextLocation SelectionEnd { get { return mCaretPosition; } set { if(IsValidTextLocation(value)) { mCaretPosition = value; OnSelectionChanged(new EventArgs()); UpdateCaret(); Invalidate(); } } } /// /// Gets the currently selected text. /// [Browsable(false)] public string SelectedText { get { if(mDocument == null || mSelectionStart == mCaretPosition) { return ""; } StringBuilder Bldr = new StringBuilder(); TextLocation StartLoc; TextLocation EndLoc; if(mSelectionStart < mCaretPosition) { StartLoc = mSelectionStart; EndLoc = mCaretPosition; } else { StartLoc = mCaretPosition; EndLoc = mSelectionStart; } for(int LineIndex = StartLoc.Line; LineIndex <= EndLoc.Line; ++LineIndex) { string CurLine = mDocument.GetLine(LineIndex, true); int EndCol = LineIndex == EndLoc.Line ? EndLoc.Column : CurLine.Length; for(int StartCol = LineIndex == StartLoc.Line ? StartLoc.Column : 0; StartCol < EndCol; ++StartCol) { Bldr.Append(CurLine[StartCol]); } } return Bldr.ToString(); } } /// /// Gets whether or not there's currently selected text. /// [Browsable(false)] public bool HasSelectedText { get { return mDocument != null && mCaretPosition != mSelectionStart; } } /// /// Gets/Sets the location of the caret within the source document. /// [Browsable(false)] public TextLocation CaretLocation { get { return mCaretPosition; } set { if(IsValidTextLocation(value)) { bool bHadSelection = this.HasSelectedText; mSelectionStart = mCaretPosition = value; if(bHadSelection) { OnSelectionChanged(new EventArgs()); } UpdateCaret(); } } } /// /// Gets/Sets the text for the window. /// public override string Text { get { if(mDocument != null) { return mDocument.Text; } return ""; } set { if(mDocument != null) { mDocument.Text = value; } } } /// /// Gets whether or not the view is in "find" mode. /// public bool IsInFindMode { get { return mToolStrip_Find.Visible; } } /// /// Gets/Sets the highlight color used on lines that contain "find" text. /// public Color FindTextLineHighlight { get { return mFindTextLineHighlight; } set { if(mFindTextLineHighlight != value) { mFindTextLineHighlight = value; Invalidate(); } } } /// /// Gets/Sets the background color used to draw "find" text. /// public Color FindTextBackColor { get { return mFindTextBackColor; } set { if(mFindTextBackColor != value) { mFindTextBackColor = value; Invalidate(); } } } /// /// Gets/Sets the foreground color used to draw "find" text. /// public Color FindTextForeColor { get { return mFindTextForeColor; } set { if(mFindTextForeColor != value) { mFindTextForeColor = value; Invalidate(); } } } /// /// Triggered when the document associated with the view has changed. /// public event EventHandler DocumentChanged { add { mOnDocChanged += value; } remove { mOnDocChanged -= value; } } /// /// Triggered when the selected text has changed. /// public event EventHandler SelectionChanged { add { mOnSelectionChanged += value; } remove { mOnSelectionChanged -= value; } } #region PInvoke Imports #if !__MonoCS__ [DllImport("user32.dll")] static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight); [DllImport("user32.dll")] static extern bool DestroyCaret(); [DllImport("user32.dll")] static extern bool ShowCaret(IntPtr hWnd); [DllImport("user32.dll")] static extern bool SetCaretPos(int X, int Y); [DllImport("user32.dll")] static extern bool HideCaret(IntPtr hWnd); #endif #endregion /// /// Constructor. /// public OutputWindowView() { InitializeComponent(); mXButtonFind = new XButton(); mXButtonFind.Size = new Size(16, 15); mXButtonFind.Anchor = AnchorStyles.None; mXButtonFind.BackColor = SystemColors.MenuBar; mXButtonFind.Click += new EventHandler(mXButtonFind_Click); mXButtonFind.MouseEnter += new EventHandler(ChildControl_MouseEnter); mCheckBox_MatchCase = new CheckBox(); mCheckBox_MatchCase.Text = "Match Case"; mCheckBox_MatchCase.BackColor = SystemColors.MenuBar; mCheckBox_MatchCase.CheckedChanged += new EventHandler(mToolStripTextBox_Find_TextChanged); mCheckBox_MatchCase.MouseEnter += new EventHandler(ChildControl_MouseEnter); TableLayoutPanel XButtonPanel = new TableLayoutPanel(); XButtonPanel.BackColor = SystemColors.MenuBar; XButtonPanel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); XButtonPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); XButtonPanel.Controls.Add(mXButtonFind); XButtonPanel.MouseEnter += new EventHandler(ChildControl_MouseEnter); ToolStripControlHost XButtonFind_Host = new ToolStripControlHost(XButtonPanel); XButtonFind_Host.Padding = new Padding(0, 0, 6, 0); mToolStrip_Find.Items.Insert(0, XButtonFind_Host); mToolStrip_Find.Items.Add(new ToolStripControlHost(mCheckBox_MatchCase)); // NOTE: We do this here instead of in the designer because we still want it to show up in the designer mToolStrip_Find.Visible = false; // get the initial font measurement this.Font = this.Font; this.Document = new OutputWindowDocument(); mLastSize = this.Size; } /// /// Event handler for when the X button on the "Find" toolstrip is clicked. /// /// The object that initiated the event. /// Additional information about the event. void mXButtonFind_Click(object sender, EventArgs e) { ExitFindMode(); } /// /// Draws the control. /// /// Information about the event. protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); try { if(mDocument != null) { ColoredDocumentLineSegment[] LineSegmentList = null; string FindTxt = mToolStripTextBox_Find.Text.Trim(); int SelectionStart; int SelectionLength; int YLineOffset = 0; int DisplayableVLines = this.DisplayableVerticalLines; System.Drawing.Size MaxLineSize = new System.Drawing.Size(short.MaxValue, short.MaxValue); // NOTE: We have to draw background highlights first to prevent drawing over overhanging glyphs from the previous line if(this.IsInFindMode && FindTxt.Length > 0) { using(SolidBrush HighlightBrush = new SolidBrush(mFindTextLineHighlight)) { for(int DocLineIndex = mVScrollBar.Value, VisibleLineIndex = 0; VisibleLineIndex < DisplayableVLines && DocLineIndex < mDocument.LineCount; ++DocLineIndex, ++VisibleLineIndex, YLineOffset += this.Font.Height) { string FullLineText = mDocument.GetLine(DocLineIndex, false); if(FullLineText.IndexOf(FindTxt, mCheckBox_MatchCase.Checked ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) != -1) { e.Graphics.FillRectangle(HighlightBrush, 0, YLineOffset, this.WidthInDisplayablePixels, this.Font.Height); } } } } // reset the y offset to begin drawing the lines YLineOffset = 0; for(int DocLineIndex = mVScrollBar.Value, VisibleLineIndex = 0; VisibleLineIndex < DisplayableVLines && DocLineIndex < mDocument.LineCount; ++DocLineIndex, ++VisibleLineIndex, YLineOffset += this.Font.Height) { int LineLength = mDocument.GetLineLength(DocLineIndex); int XOffset = 0; if(mHScrollBar.Value >= LineLength || LineLength == 0) { continue; } else if(GetSelectionForLine(DocLineIndex, out SelectionStart, out SelectionLength)) { // first check to see if there's txt to be drawn before the selection if(SelectionStart > mHScrollBar.Value) { LineSegmentList = GetSegmentsForLine(DocLineIndex, mHScrollBar.Value, SelectionStart - mHScrollBar.Value); foreach(ColoredDocumentLineSegment CurSegment in LineSegmentList) { DrawLineSegment(e.Graphics, YLineOffset, ref MaxLineSize, ref XOffset, CurSegment); } } else if(SelectionStart + SelectionLength >= mHScrollBar.Value) { SelectionLength -= mHScrollBar.Value - SelectionStart; SelectionStart = mHScrollBar.Value; } else { SelectionStart = mHScrollBar.Value; SelectionLength = 0; } // now draw the selected text if(SelectionLength > 0) { string LineSegment = mDocument.GetLine(DocLineIndex, SelectionStart, SelectionLength); Size SelectionRect = TextRenderer.MeasureText(e.Graphics, LineSegment, this.Font, new Size(short.MaxValue, short.MaxValue), TXT_FLAGS); e.Graphics.FillRectangle(SystemBrushes.Highlight, XOffset, YLineOffset, SelectionRect.Width, this.Font.Height); TextRenderer.DrawText(e.Graphics, LineSegment, this.Font, new Point(XOffset, YLineOffset), SystemColors.HighlightText, TXT_FLAGS); XOffset += TextRenderer.MeasureText(e.Graphics, LineSegment, this.Font, MaxLineSize, TXT_FLAGS).Width; SelectionStart += SelectionLength; } // draw the last segment of unselected text if it exists if(SelectionStart < LineLength) { LineSegmentList = GetSegmentsForLine(DocLineIndex, SelectionStart, LineLength - SelectionStart); foreach(ColoredDocumentLineSegment CurSegment in LineSegmentList) { DrawLineSegment(e.Graphics, YLineOffset, ref MaxLineSize, ref XOffset, CurSegment); } } } else { if(mHScrollBar.Value > 0) { LineSegmentList = GetSegmentsForLine(DocLineIndex, mHScrollBar.Value, LineLength - mHScrollBar.Value); } else { LineSegmentList = GetSegmentsForLine(DocLineIndex, 0, LineLength); } foreach(ColoredDocumentLineSegment CurSegment in LineSegmentList) { DrawLineSegment(e.Graphics, YLineOffset, ref MaxLineSize, ref XOffset, CurSegment); } } } } // fill in the space between scroll bars e.Graphics.FillRectangle(SystemBrushes.Control, mVScrollBar.Location.X, mHScrollBar.Location.Y, mVScrollBar.Width, mHScrollBar.Height); } catch(Exception ex) { System.Diagnostics.Debug.WriteLine(ex.ToString()); } } /// /// Gets the segments for a line so that it can be drawn. /// /// The index of the line whose segments are being retrieved. /// The column to start retrieving segments at. /// The number of characters from to return segments for. /// An array of segments associated with the line. private ColoredDocumentLineSegment[] GetSegmentsForLine(int LineIndex, int ColIndex, int Length) { System.Diagnostics.Debug.Assert(mDocument != null); bool bHasFindText; ColoredDocumentLineSegment[] Segments; if(this.IsInFindMode && mToolStripTextBox_Find.Text.Trim().Length > 0) { Segments = mDocument.GetLineSegmentsWithFindString(LineIndex, ColIndex, Length, mToolStripTextBox_Find.Text, new ColorPair(mFindTextBackColor, mFindTextForeColor), mCheckBox_MatchCase.Checked ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase, out bHasFindText); } else { Segments = mDocument.GetLineSegments(LineIndex, ColIndex, Length); } return Segments; } /// /// Draws the text for a line segment. /// /// The object to use for drawing. /// The Y location to begin drawing. /// The maximum allowed line size. /// The X location to begin drawing. /// The segment to be drawn. private void DrawLineSegment(Graphics Gfx, int YLineOffset, ref System.Drawing.Size MaxLineSize, ref int XOffset, ColoredDocumentLineSegment CurSegment) { ColorPair Colors = CurSegment.Color; int SegmentWidth = TextRenderer.MeasureText(Gfx, CurSegment.Text, this.Font, MaxLineSize, TXT_FLAGS).Width; if(Colors.BackColor.HasValue) { TextRenderer.DrawText(Gfx, CurSegment.Text, this.Font, new Point(XOffset, YLineOffset), CurSegment.Color.ForeColor.HasValue ? CurSegment.Color.ForeColor.Value : this.ForeColor, CurSegment.Color.BackColor.Value, TXT_FLAGS); } else { TextRenderer.DrawText(Gfx, CurSegment.Text, this.Font, new Point(XOffset, YLineOffset), CurSegment.Color.ForeColor.HasValue ? CurSegment.Color.ForeColor.Value : this.ForeColor, TXT_FLAGS); } XOffset += SegmentWidth; } /// /// Event handler for when the document associated with the view has been changed. /// /// Information about the event. virtual protected void OnDocumentChanged(EventArgs e) { if(mDocument != null) { mCurrentDocumentSize.Line = mDocument.LineCount; mCurrentDocumentSize.Column = mDocument.GetLineLength(mCurrentDocumentSize.Line - 1); } else { mCurrentDocumentSize.Column = 0; mCurrentDocumentSize.Line = 0; } UpdateCaret(); UpdateScrollbars(); Invalidate(); if(mOnDocChanged != null) { mOnDocChanged(this, e); } } /// /// Event handler for when the selected text has changed. /// /// Information about the event. virtual protected void OnSelectionChanged(EventArgs e) { if(mOnSelectionChanged != null) { mOnSelectionChanged(this, e); } } /// /// Updates the vertical and horizontal scrollbar values. /// private void UpdateScrollbars() { if(mDocument == null) { mVScrollBar.Value = 0; mVScrollBar.Maximum = 0; mVScrollBar.Enabled = false; mHScrollBar.Value = 0; mHScrollBar.Maximum = 0; mHScrollBar.Enabled = false; } else { // update horizontal scrollbar int HorizontalDisplayableCount = this.DisplayableCharacters; mHScrollBar.Maximum = Math.Max(0, mDocument.LongestLineLength - 1); mHScrollBar.LargeChange = Math.Max(0, HorizontalDisplayableCount); int MaxPos = mHScrollBar.Maximum - HorizontalDisplayableCount; if(MaxPos < 0) { MaxPos = 0; } if(mHScrollBar.Value > MaxPos) { mHScrollBar.Value = MaxPos; } mHScrollBar.Enabled = HorizontalDisplayableCount < mDocument.LongestLineLength; // update vertical scrollbar int VerticalDisplayableCount = this.DisplayableVerticalLines; // must subtract 1 or else it displays an extra line mVScrollBar.Maximum = Math.Max(0, mDocument.LineCount - 1); mVScrollBar.LargeChange = Math.Max(0, VerticalDisplayableCount); MaxPos = GetMaximumVScrollValue(); if(MaxPos < 0) { MaxPos = 0; } if(mVScrollBar.Value > MaxPos) { mVScrollBar.Value = MaxPos; } mVScrollBar.Enabled = VerticalDisplayableCount < mDocument.LineCount; } } /// /// Event handler for when the control is resizing. /// /// Information about the event. protected override void OnResize(EventArgs e) { UpdateScrollbarPositions(); UpdateScrollbars(); base.OnResize(e); UpdateCaret(); Invalidate(); // NOTE: Important this is performed last if(!this.Size.IsEmpty) { mLastSize = this.Size; } } private void UpdateScrollbarPositions() { if(mToolStrip_Find.Visible) { mVScrollBar.Location = new Point(this.Width - mVScrollBar.Width, 0); mVScrollBar.Height = this.Height - mHScrollBar.Height - mToolStrip_Find.Height; mHScrollBar.Location = new Point(0, this.Height - mHScrollBar.Height - mToolStrip_Find.Height); mHScrollBar.Width = this.Width - mVScrollBar.Width; } else { mVScrollBar.Location = new Point(this.Width - mVScrollBar.Width, 0); mVScrollBar.Height = this.Height - mHScrollBar.Height; mHScrollBar.Location = new Point(0, this.Height - mHScrollBar.Height); mHScrollBar.Width = this.Width - mVScrollBar.Width; } } /// /// Event handler for when the vertical scrollbar has been modified. /// /// Object that generated the event. /// Information about the event. private void mVScrollBar_ValueChanged(object sender, EventArgs e) { UpdateCaret(); Invalidate(); } /// /// Event handler for when the horizontal scrollbar has been modified. /// /// Object that generated the event. /// Information about the event. private void mHScrollBar_ValueChanged(object sender, EventArgs e) { UpdateCaret(); Invalidate(); } /// /// Event handler for when the control has received focus. /// /// Information about the event. protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); InternalCreateCaret(); } /// /// Event handler for when the control has lost focus. /// /// Information about the event. protected override void OnLostFocus(EventArgs e) { base.OnLostFocus(e); InternalDestroyCaret(); mIsSelecting = false; mScrollSelectedTextTimer.Enabled = false; } /// /// Creates the caret. /// private void InternalCreateCaret() { UpdateCaret(); } /// /// Destroys the caret. /// private void InternalDestroyCaret() { #if !__MonoCS__ DestroyCaret(); #endif } /// /// Updates the position and view state of the caret. /// private void UpdateCaret() { // make sure the caret is within the bounds of the document if(mDocument == null) { mCaretPosition.Column = 0; mCaretPosition.Line = 0; } else { if(mDocument.LineCount <= mCaretPosition.Line) { mCaretPosition.Line = Math.Max(0, mDocument.LineCount - 1); } if(mDocument.GetLineLength(mCaretPosition.Line) < mCaretPosition.Column) { mCaretPosition.Column = mDocument.GetLineLength(mCaretPosition.Line); } } if(this.Focused) { #if !__MonoCS__ // only draw the caret if it's visible if(mCaretPosition.Line >= mVScrollBar.Value && mCaretPosition.Line <= mVScrollBar.Value + this.DisplayableVerticalLines && mCaretPosition.Column >= mHScrollBar.Value && mCaretPosition.Column <= mHScrollBar.Value + this.DisplayableCharacters) { CreateCaret(this.Handle, IntPtr.Zero, 0, this.Font.Height); SetCaretPos((mCaretPosition.Column - mHScrollBar.Value) * mFontDimensions.Width, (mCaretPosition.Line - mVScrollBar.Value) * this.Font.Height); ShowCaret(this.Handle); } else { HideCaret(this.Handle); } #endif } } /// /// Event handler for when a mouse button has been pressed. /// /// Information about the event. protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if((e.Button & MouseButtons.Left) == MouseButtons.Left || (e.Button & MouseButtons.Right) == MouseButtons.Right) { bool bHadSelection = this.HasSelectedText; bool bUpdateCaret = true; TextLocation CharacterPosition = GetCharacterPosFromCoord(e.X, e.Y); mIsSelecting = e.Button == MouseButtons.Left; if(!mIsSelecting) { int SelectionStart; int SelectionLength; if(GetSelectionForLine(CharacterPosition.Line, out SelectionStart, out SelectionLength) && CharacterPosition.Column >= SelectionStart && CharacterPosition.Column < SelectionStart + SelectionLength) { bUpdateCaret = false; } } if(bUpdateCaret && (mCaretPosition != CharacterPosition || bHadSelection)) { #if !__MonoCS__ mCaretPosition = CharacterPosition; ushort KeyState = GetAsyncKeyState(VK_SHIFT); // if shift key is not pressed if((KeyState & 0x8000) == 0) { mSelectionStart = mCaretPosition; if(bHadSelection) { OnSelectionChanged(new EventArgs()); } } else { // if shift is pressed then we must be changing the selection bHadSelection = true; OnSelectionChanged(new EventArgs()); } #endif } // NOTE: This is very important. If you do not set the ActiveControl to null then // child controls such as the "find" toolstrip container won't give focus back! // This means the caret may never come back and babies will cry. this.ActiveControl = null; UpdateCaret(); if(bHadSelection) { Invalidate(); } } } /// /// Event handler for when a mouse button has been released. /// /// Information about the event. protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); mScrollSelectedTextTimer.Enabled = false; mIsSelecting = false; } /// /// Event handler for when the mouse cursor has been moved. /// /// Information about the event. protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if(mIsSelecting) { Rectangle TextArea = GetTextAreaRect(); if(TextArea.Contains(e.Location)) { mScrollSelectedTextTimer.Enabled = false; TextLocation NewCaretPos = GetCharacterPosFromCoord(e.X, e.Y); if(mCaretPosition != NewCaretPos) { mCaretPosition = NewCaretPos; UpdateCaret(); Invalidate(); } } else { mScrollSelectedTextTimer.Enabled = true; } } mLastMousePos = e.Location; } /// /// Generates a rectangle for the area within the client rectangle that's used for drawing text. /// /// The rectangle containing the area within the client rectangle that's used for drawing text. private Rectangle GetTextAreaRect() { Rectangle TextArea = this.ClientRectangle; TextArea.Width -= mVScrollBar.Width; TextArea.Height -= mHScrollBar.Height; if(mToolStrip_Find.Visible) { TextArea.Height -= mToolStrip_Find.Height; } return TextArea; } /// /// Event handler for when the mouse wheel has been moved. /// /// Information about the event. protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); if(mDocument != null) { int LinesToScroll = SystemInformation.MouseWheelScrollLines * -(e.Delta / SystemInformation.MouseWheelScrollDelta); if(LinesToScroll < 0) { mVScrollBar.Value = Math.Max(0, mVScrollBar.Value + LinesToScroll); } else { mVScrollBar.Value = Math.Min(GetMaximumVScrollValue(), mVScrollBar.Value + LinesToScroll); } } } /// /// Retrieves the maximum value that the vertical scrollbar can be set to. /// /// The maximum value the vertical scrollbar can be set to. private int GetMaximumVScrollValue() { // add 1 to account for max - 1 (otherwise an extra line is displayed) return mVScrollBar.Maximum - mVScrollBar.LargeChange + 1; } /// /// Converts a screen coordinate into a character position. /// /// The X coordinate. /// The Y coordinate. /// The location within the document of the screen coordinate. TextLocation GetCharacterPosFromCoord(int X, int Y) { if(mDocument == null || mDocument.LineCount == 0) { return new TextLocation(0, 0); } // remove negative coordinates Y = Math.Max(0, Y); X = Math.Max(0, X); int LineIndex = Y / this.Font.Height + mVScrollBar.Value; if(LineIndex >= mDocument.LineCount) { LineIndex = Math.Max(0, mDocument.LineCount - 1); } // If the mouse clicked in the right half of the character the caret will be moved to its right side, otherwise the left int ColumnIndex = (int)Math.Round((double)X / mFontDimensions.Width) + mHScrollBar.Value; int LineLength = mDocument.GetLineLength(LineIndex); if(ColumnIndex >= LineLength) { ColumnIndex = LineLength; } return new TextLocation(LineIndex, ColumnIndex); } /// /// Determines whether or not a line contains selected text. /// /// The index of the line to check. /// Receives the start location of the selected text - if any. /// Receives the length of the selected text on the current line - if any. /// True if the line contains selected text. bool GetSelectionForLine(int LineIndex, out int SelectionStart, out int SelectionLength) { SelectionStart = 0; SelectionLength = 0; if(mDocument == null || mCaretPosition == mSelectionStart) { return false; } bool bRet = true; if(mCaretPosition.Line == mSelectionStart.Line && mCaretPosition.Line == LineIndex) { if(mCaretPosition.Column > mSelectionStart.Column) { SelectionStart = mSelectionStart.Column; SelectionLength = mCaretPosition.Column - mSelectionStart.Column; } else { SelectionStart = mCaretPosition.Column; SelectionLength = mSelectionStart.Column - mCaretPosition.Column; } } else if(mCaretPosition.Line == LineIndex) { if(mCaretPosition.Line > mSelectionStart.Line) { SelectionLength = mCaretPosition.Column; } else { SelectionStart = mCaretPosition.Column; SelectionLength = mDocument.GetLineLength(LineIndex) - SelectionStart; } } else if(mSelectionStart.Line == LineIndex) { if(mSelectionStart.Line > mCaretPosition.Line) { SelectionLength = mSelectionStart.Column; } else { SelectionStart = mSelectionStart.Column; SelectionLength = mDocument.GetLineLength(LineIndex) - SelectionStart; } } else if(LineIndex > Math.Min(mCaretPosition.Line, mSelectionStart.Line) && LineIndex < Math.Max(mCaretPosition.Line, mSelectionStart.Line)) { SelectionLength = mDocument.GetLineLength(LineIndex); } else { bRet = false; } return bRet; } /// /// Event handler for when a key has been pressed. /// /// Information about the event. protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if(mDocument != null) { bool bHadSelection = (mCaretPosition != mSelectionStart); TextLocation OldCaretPosition = !bHadSelection ? mCaretPosition : mSelectionStart; bool bHandleShift = false; if((e.KeyCode == Keys.Insert || e.KeyCode == Keys.C) && e.Control) { CopySelectedText(); } else if(e.KeyCode == Keys.A && e.Control) { e.Handled = true; SelectAll(); } else if(e.KeyCode == Keys.F && e.Control) { EnterFindMode(); } else if(e.KeyCode == Keys.F3) { if(e.Shift) { FindPrevious(); } else { FindNext(); } } else if(e.KeyCode == Keys.End) { if(e.Control) { mCaretPosition.Line = Math.Max(0, mDocument.LineCount - 1); } mCaretPosition.Column = mDocument.GetLineLength(mCaretPosition.Line); bHandleShift = true; ScrollToCaret(); } else if(e.KeyCode == Keys.Home) { mCaretPosition.Column = 0; if(e.Control) { mCaretPosition.Line = 0; } bHandleShift = true; ScrollToCaret(); } else if(e.KeyCode == Keys.Up) { MoveCaretUpLines(1, false); bHandleShift = true; ScrollToCaret(); } else if(e.KeyCode == Keys.Down) { MoveCaretDownLines(1, false); bHandleShift = true; ScrollToCaret(); } else if(e.KeyCode == Keys.Right) { ++mCaretPosition.Column; if(mCaretPosition.Column >= mDocument.GetLineLength(mCaretPosition.Line)) { ++mCaretPosition.Line; if(mCaretPosition.Line >= mDocument.LineCount) { --mCaretPosition.Line; --mCaretPosition.Column; } else { mCaretPosition.Column = 0; } } bHandleShift = true; ScrollToCaret(); } else if(e.KeyCode == Keys.Left) { --mCaretPosition.Column; if(mCaretPosition.Column < 0) { --mCaretPosition.Line; if(mCaretPosition.Line < 0) { mCaretPosition.Line = 0; mCaretPosition.Column = 0; } else { mCaretPosition.Column = mDocument.GetLineLength(mCaretPosition.Line); } } bHandleShift = true; } else if(e.KeyCode == Keys.PageUp) { mCaretPosition.Line = Math.Max(0, mCaretPosition.Line - mVScrollBar.LargeChange); int LineLength = mDocument.GetLineLength(mCaretPosition.Line); if(mCaretPosition.Column > LineLength) { mCaretPosition.Column = LineLength; } bHandleShift = true; } else if(e.KeyCode == Keys.PageDown) { mCaretPosition.Line = Math.Min(mDocument.LineCount - 1, mCaretPosition.Line + mVScrollBar.LargeChange); int LineLength = mDocument.GetLineLength(mCaretPosition.Line); if(mCaretPosition.Column > LineLength) { mCaretPosition.Column = LineLength; } bHandleShift = true; } if(bHandleShift) { // prevent the key from propagating to children e.Handled = true; if(e.Shift) { mSelectionStart = OldCaretPosition; } else { mSelectionStart = mCaretPosition; } if(mSelectionStart == mCaretPosition) { if(bHadSelection) { OnSelectionChanged(new EventArgs()); } } else { OnSelectionChanged(new EventArgs()); } ScrollToCaretOrInvalidate(); } } } /// /// Moves the caret down in the document. /// /// The number of lines to move the caret up. /// Pass true if the selection start is moved with the caret. private void MoveCaretDownLines(int NumLines, bool bMoveSelectionStart) { if(NumLines < 0) { throw new ArgumentOutOfRangeException("NumLines", "NumLines must be greater than 0."); } if(mDocument == null) { return; } mCaretPosition.Line = Math.Min(mCaretPosition.Line + NumLines, Math.Max(0, mDocument.LineCount - 1)); if(mCaretPosition.Column > mDocument.GetLineLength(mCaretPosition.Line)) { mCaretPosition.Column = mDocument.GetLineLength(mCaretPosition.Line); } if(bMoveSelectionStart) { mSelectionStart = mCaretPosition; } } /// /// Moves the caret up in the document. /// /// The number of lines to move the caret up. /// Pass true if the selection start is moved with the caret. private void MoveCaretUpLines(int NumLines, bool bMoveSelectionStart) { if(NumLines < 0) { throw new ArgumentOutOfRangeException("NumLines", "NumLines must be greater than 0."); } if(mDocument == null) { return; } mCaretPosition.Line = Math.Max(mCaretPosition.Line - NumLines, 0); if(mCaretPosition.Column > mDocument.GetLineLength(mCaretPosition.Line)) { mCaretPosition.Column = mDocument.GetLineLength(mCaretPosition.Line); } if(bMoveSelectionStart) { mSelectionStart = mCaretPosition; } } /// /// Copies the currently selected text to the clipboard. /// /// The currently selected text that was copied to the clipboard. public string CopySelectedText() { string Txt = this.SelectedText; if(Txt.Length > 0) { Clipboard.SetText(Txt, TextDataFormat.UnicodeText); } return Txt; } /// /// Selects all text and scrolls to the caret. /// public void SelectAll() { mSelectionStart = mDocument.BeginningOfDocument; mCaretPosition = mDocument.EndOfDocument; OnSelectionChanged(new EventArgs()); if(!ScrollToCaret()) { UpdateCaret(); Invalidate(); } } /// /// Scrolls the control so that the caret is visible. /// /// True if the control had to be scrolled. public bool ScrollToCaret() { bool bInvalidate = false; if(mCaretPosition.Line < mVScrollBar.Value) { mVScrollBar.Value = mCaretPosition.Line; bInvalidate = true; } else if(mCaretPosition.Line >= mVScrollBar.Value + this.DisplayableVerticalLines) { // since mVScrollBar.Value is an index if its equal to the total # of lines we still need to add 1 to move the scrollbar const int INDEX_PADDING = 1; mVScrollBar.Value = Math.Min(mVScrollBar.Maximum, mVScrollBar.Value + mCaretPosition.Line - (mVScrollBar.Value + this.DisplayableVerticalLines) + INDEX_PADDING); bInvalidate = true; } if(mCaretPosition.Column < mHScrollBar.Value) { mHScrollBar.Value = mCaretPosition.Column; bInvalidate = true; } else if(mCaretPosition.Column >= mHScrollBar.Value + this.DisplayableCharacters) { mHScrollBar.Value = Math.Min(mHScrollBar.Maximum, mHScrollBar.Value + mCaretPosition.Column - (mHScrollBar.Value + this.DisplayableCharacters)); bInvalidate = true; } if(bInvalidate) { UpdateCaret(); Invalidate(); return true; } return false; } /// /// Allows us to handle the arrow keys. /// /// Information about the key to be checked. /// True if the key is valid input. protected override bool IsInputKey(Keys keyData) { if((keyData & Keys.Left) == Keys.Left || (keyData & Keys.Right) == Keys.Right || (keyData & Keys.Up) == Keys.Up || (keyData & Keys.Down) == Keys.Down) { return true; } return base.IsInputKey(keyData); } /// /// Determines whether a location within the current document is valid. /// /// The location to validate. /// True if the supplied location is within the bounds of the document. bool IsValidTextLocation(TextLocation Loc) { bool bResult = false; if(mDocument != null) { bResult = mDocument.IsValidTextLocation(Loc); } return bResult; } /// /// Selects a range of text and brings the selection into view. /// /// The start location of the selection. /// The end location (and caret location) of the selection. public void Select(TextLocation StartLoc, TextLocation EndLoc) { if(mDocument != null) { if(!IsValidTextLocation(StartLoc)) { throw new ArgumentOutOfRangeException("StartLoc"); } if(!IsValidTextLocation(EndLoc)) { throw new ArgumentOutOfRangeException("EndLoc"); } mSelectionStart = StartLoc; mCaretPosition = EndLoc; OnSelectionChanged(new EventArgs()); ScrollToCaretOrInvalidate(); } } /// /// Selects a range of text and brings the selection into view. /// /// The location of the selection and its length. public void Select(FindResult Range) { if(mDocument != null) { TextLocation StartLoc = new TextLocation(Range.Line, Range.Column); if(!IsValidTextLocation(StartLoc)) { throw new ArgumentOutOfRangeException("Range"); } TextLocation EndLoc = new TextLocation(Range.Line, Math.Min(Range.Column + Range.Length, mDocument.GetLineLength(Range.Line))); Select(StartLoc, EndLoc); } } /// /// Finds the first occurence of the specified string within the document if it exists. /// /// The string to find. /// The position within the document to begin searching. /// The position within the document to end searching. /// Flags telling the document how to conduct its search. /// The location within the document of the supplied text. /// True if the text was found. public bool Find(string Txt, TextLocation StartLoc, TextLocation EndLoc, RichTextBoxFinds Flags, out FindResult Result) { bool bResult = false; if(mDocument == null) { Result = FindResult.Empty; } else { bResult = mDocument.Find(Txt, StartLoc, EndLoc, Flags, out Result); } if(bResult && (Flags & RichTextBoxFinds.NoHighlight) != RichTextBoxFinds.NoHighlight) { Select(Result); } return bResult; } /// /// Clears the text in the source document. /// public void Clear() { if(mDocument != null) { mDocument.Clear(); } } /// /// Saves the source document to a file. /// /// The name of the file to write the document to. public void SaveToFile(string Name) { if(mDocument != null) { mDocument.SaveToFile(Name); } } /// /// Scrolls the caret to the end of the document. /// public void ScrollToEnd() { if(mDocument != null) { bool bHadSelection = this.HasSelectedText; mSelectionStart = mCaretPosition = mDocument.EndOfDocument; if(bHadSelection) { OnSelectionChanged(new EventArgs()); } ScrollToCaretOrInvalidate(); } } /// /// Scrolls the caret to the beginning of the document. /// public void ScrollToHome() { if(mDocument != null) { bool bHadSelection = this.HasSelectedText; mSelectionStart = mCaretPosition = mDocument.BeginningOfDocument; if(bHadSelection) { OnSelectionChanged(new EventArgs()); } ScrollToCaretOrInvalidate(); } } /// /// Scrolls to the location of the caret. If the caret is already in view then it forces a redraw. /// private void ScrollToCaretOrInvalidate() { if(!ScrollToCaret()) { UpdateCaret(); Invalidate(); } } /// /// Event handler for copying text. /// /// The object that initiated the event. /// Additional information about the event. private void mCtxDefault_Copy_Click(object sender, EventArgs e) { CopySelectedText(); } /// /// Event handler for clearing the document. /// /// The object that initiated the event. /// Additional information about the event. private void ToolStripMenuItem_Clear_Click( object sender, EventArgs e ) { if( mDocument != null ) { mDocument.Clear(); } } /// /// Event handler for scrolling to the end of the document. /// /// The object that initiated the event. /// Additional information about the event. private void mCtxDefault_ScrollToEnd_Click(object sender, EventArgs e) { ScrollToEnd(); } /// /// Event handler for scrolling to the beginning of the document. /// /// The object that initiated the event. /// Additional information about the event. private void mCtxDefault_ScrollToHome_Click(object sender, EventArgs e) { ScrollToHome(); } /// /// Event handler for auto-scrolling selected text. /// /// The object that initiated the event. /// Additional information about the event. private void mScrollSelectedTextTimer_Tick(object sender, EventArgs e) { if(mDocument == null) { return; } TextLocation NewCaretPos = GetCharacterPosFromCoord(mLastMousePos.X, mLastMousePos.Y); Rectangle TextArea = GetTextAreaRect(); bool bRequiresRedraw = true; if(mLastMousePos.Y >= TextArea.Bottom) { mCaretPosition.Line = Math.Min(mDocument.LineCount - 1, mVScrollBar.Value + this.DisplayableVerticalLines - 1); MoveCaretDownLines(2, false); } else if(mLastMousePos.Y <= TextArea.Top) { mCaretPosition.Line = mVScrollBar.Value; MoveCaretUpLines(2, false); } else { bRequiresRedraw = mCaretPosition.Line != NewCaretPos.Line; mCaretPosition.Line = NewCaretPos.Line; } int NewColumn = 0; // now that we have an updated line position get an updated column position NewCaretPos = GetCharacterPosFromCoord(mLastMousePos.X, mLastMousePos.Y); if(mLastMousePos.X >= TextArea.Right) { // NOTE: mCaretPosition.Line has already been updated so don't use NewCaretPos.Line NewColumn = Math.Min(mDocument.GetLineLength(mCaretPosition.Line), mHScrollBar.Value + this.DisplayableCharacters + 2); } else if(mLastMousePos.X <= TextArea.Left) { NewColumn = Math.Max(0, mHScrollBar.Value - 2); } else { NewColumn = NewCaretPos.Column; } if(!bRequiresRedraw) { bRequiresRedraw = mCaretPosition.Column != NewColumn; } mCaretPosition.Column = NewColumn; if(!ScrollToCaret() && bRequiresRedraw) { UpdateCaret(); Invalidate(); } } /// /// Event handler for when the document has been modified. /// /// The object that initiated the event. /// Additional information about the event. void mDocument_Modified(object sender, EventArgs e) { TextLocation PrevDocSize = mCurrentDocumentSize; mCurrentDocumentSize.Line = mDocument.LineCount; mCurrentDocumentSize.Column = mDocument.GetLineLength(mCurrentDocumentSize.Line - 1); bool bInvalidate = true; bool bHadSelection = this.HasSelectedText; if(mCurrentDocumentSize > PrevDocSize) { // we have to do these checks before we update he scrollbars if(mAutoScroll && !bHadSelection && mCaretPosition.Column == PrevDocSize.Column && mCaretPosition.Line == Math.Max(0, PrevDocSize.Line - 1) && mVScrollBar.Value == Math.Max(0, GetMaximumVScrollValue())) { // ScrollToCaret() is dependent on scrollbar location so update that now instead of later UpdateScrollbars(); mCaretPosition.Line = Math.Max(0, mDocument.LineCount - 1); mCaretPosition.Column = mDocument.GetLineLength(mCaretPosition.Line); mSelectionStart = mCaretPosition; bInvalidate = !ScrollToCaret(); } else { UpdateScrollbars(); } } else { // if doc size shrunk we need to make sure the caret is still within range if(mCaretPosition > mCurrentDocumentSize) { mCaretPosition = mCurrentDocumentSize; if(bHadSelection) { if(mCaretPosition > mSelectionStart) { mSelectionStart = mCaretPosition; } OnSelectionChanged(new EventArgs()); } else { mSelectionStart = mCaretPosition; } } else if(bHadSelection && mSelectionStart > mCurrentDocumentSize) { mSelectionStart = mCurrentDocumentSize; OnSelectionChanged(new EventArgs()); } UpdateScrollbars(); } if(bInvalidate) { UpdateCaret(); Invalidate(); } } /// /// Event handler for when the visibility of the "Find" toolstrip changes. /// /// The object that initiated the event. /// Additional information about the event. private void mToolStrip_Find_VisibleChanged(object sender, EventArgs e) { UpdateScrollbarPositions(); UpdateScrollbars(); UpdateCaret(); Invalidate(); } /// /// Event handler for when the mouse movers over the control. /// /// The object that initiated the event. /// Additional information about the event. private void OutputWindowView_MouseMove(object sender, MouseEventArgs e) { Control Child = GetChildAtPoint(e.Location, GetChildAtPointSkip.Invisible); Rectangle ScrollSpace = new Rectangle(mHScrollBar.Width, mHScrollBar.Location.Y, mVScrollBar.Width, mHScrollBar.Height); if(ScrollSpace.Contains(e.Location)) { if(this.Cursor != Cursors.Arrow) { this.Cursor = Cursors.Arrow; } } else if(Child != null && this.Cursor != Cursors.Arrow) { this.Cursor = Cursors.Arrow; } else if(Child == null && this.Cursor != Cursors.IBeam) { this.Cursor = Cursors.IBeam; } } /// /// Event handler for when the mouse movers over a child control. /// /// The object that initiated the event. /// Additional information about the event. private void ChildControl_MouseEnter(object sender, EventArgs e) { if(this.Cursor != Cursors.Arrow) { this.Cursor = Cursors.Arrow; } } /// /// Tells the view to display the "find" tool strip. /// public void EnterFindMode() { if(this.HasSelectedText) { mToolStripTextBox_Find.Text = this.SelectedText; } mToolStrip_Find.Visible = true; mToolStripTextBox_Find.Focus(); } /// /// Tells the view to close the "find" tool strip. /// public void ExitFindMode() { mToolStrip_Find.Visible = false; } /// /// Searches down within the text box for the specified search string. /// public void FindNext() { if(!this.IsInFindMode) { EnterFindMode(); // Give focus back to our edit control so we can perform the find this.ActiveControl = null; } FindNext(null); } /// /// Searches down within the text box for the specified search string. /// /// The location within the document to begin searching. void FindNext(TextLocation? DownSearchStart) { if(this.mDocument != null) { FindResult Result; RichTextBoxFinds Flags = mCheckBox_MatchCase.Checked ? RichTextBoxFinds.MatchCase : RichTextBoxFinds.None; if(!DownSearchStart.HasValue) { DownSearchStart = this.SelectionStart >= this.SelectionEnd ? this.SelectionStart : this.SelectionEnd; } bool bFound = true; string TxtToSearchFor = mToolStripTextBox_Find.Text.Trim(); if(!this.Find(TxtToSearchFor, DownSearchStart.Value, this.mDocument.EndOfDocument, Flags, out Result)) { if(!this.Find(TxtToSearchFor, this.mDocument.BeginningOfDocument, new TextLocation(DownSearchStart.Value.Line, mDocument.GetLineLength(DownSearchStart.Value.Line)), Flags, out Result)) { //MessageBox.Show(this, "The specified search string does not exist!", mToolStripTextBox_Find.Text); bFound = false; } } UpdateFindBoxColor(bFound); } } /// /// Searches up within the text box for the specified search string. /// public void FindPrevious() { if(!this.IsInFindMode) { EnterFindMode(); // Give focus back to our edit control so we can perform the find this.ActiveControl = null; } FindPrevious(null); } /// /// Searches up within the text box for the specified search string. /// /// The location within the document to begin searching. void FindPrevious(TextLocation? UpSearchStart) { if(this.mDocument != null) { FindResult Result; RichTextBoxFinds Flags = (mCheckBox_MatchCase.Checked ? RichTextBoxFinds.MatchCase : RichTextBoxFinds.None) | RichTextBoxFinds.Reverse; if(!UpSearchStart.HasValue) { UpSearchStart = this.SelectionStart <= this.SelectionEnd ? this.SelectionStart : this.SelectionEnd; } bool bFound = true; string TxtToSearchFor = mToolStripTextBox_Find.Text.Trim(); if(!this.Find(TxtToSearchFor, UpSearchStart.Value, this.mDocument.BeginningOfDocument, Flags, out Result)) { if(!this.Find(TxtToSearchFor, this.mDocument.EndOfDocument, new TextLocation(UpSearchStart.Value.Line, 0), Flags, out Result)) { bFound = false; } } UpdateFindBoxColor(bFound); } } /// /// Updates the color of the "Find" text box. /// /// True if the text in the text box is valid. private void UpdateFindBoxColor(bool bFound) { if(bFound) { if(mToolStripTextBox_Find.BackColor != SystemColors.Window) { mToolStripTextBox_Find.BackColor = SystemColors.Window; mToolStripTextBox_Find.ForeColor = SystemColors.ControlText; } } else { if(mToolStripTextBox_Find.BackColor != INVALID_SEARCH_BACKCOLOR) { mToolStripTextBox_Find.BackColor = INVALID_SEARCH_BACKCOLOR; mToolStripTextBox_Find.ForeColor = INVALID_SEARCH_FORECOLOR; // We need to force a redraw if the combobox is just switching to an invalid state as it's possible // that "find" search items were highlighted and they need to be redrawn Invalidate(); } System.Media.SystemSounds.Beep.Play(); } } /// /// Event handler for when the "Find Next" button has been clicked. /// /// The object that initiated the event. /// Additional information about the event. private void mToolStripButton_FindNext_Click(object sender, EventArgs e) { FindNext(); } /// /// Event handler for when the "Find Previous" button has been clicked. /// /// The object that initiated the event. /// Additional information about the event. private void mToolStripButton_FindPrevious_Click(object sender, EventArgs e) { FindPrevious(); } /// /// Event handler or when the text in the "find" text box has changed or the state of the "match case" checkbox has changed. /// /// The object that initiated the event. /// Additional information about the event. private void mToolStripTextBox_Find_TextChanged(object sender, EventArgs e) { if(mToolStripTextBox_Find.Text.Trim().Length == 0) { mCaretPosition = mSelectionStart; UpdateFindBoxColor(true); UpdateCaret(); Invalidate(); } else { TextLocation StartLocation = mCaretPosition <= mSelectionStart ? mCaretPosition : mSelectionStart; FindNext(StartLocation); } } } }