// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Indentation; using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.AvalonEdit.Utils; using ICSharpCode.NRefactory; using ICSharpCode.NRefactory.Editor; namespace ICSharpCode.AvalonEdit.Editing { /// /// Control that wraps a TextView and adds support for user input and the caret. /// public class TextArea : Control, IScrollInfo, IWeakEventListener, ITextEditorComponent, IServiceProvider { internal readonly ImeSupport ime; #region Constructor static TextArea() { DefaultStyleKeyProperty.OverrideMetadata(typeof(TextArea), new FrameworkPropertyMetadata(typeof(TextArea))); KeyboardNavigation.IsTabStopProperty.OverrideMetadata( typeof(TextArea), new FrameworkPropertyMetadata(Boxes.True)); KeyboardNavigation.TabNavigationProperty.OverrideMetadata( typeof(TextArea), new FrameworkPropertyMetadata(KeyboardNavigationMode.None)); FocusableProperty.OverrideMetadata( typeof(TextArea), new FrameworkPropertyMetadata(Boxes.True)); } /// /// Creates a new TextArea instance. /// public TextArea() : this(new TextView()) { } /// /// Creates a new TextArea instance. /// protected TextArea(TextView textView) { if (textView == null) throw new ArgumentNullException("textView"); this.textView = textView; this.Options = textView.Options; selection = emptySelection = new EmptySelection(this); textView.Services.AddService(typeof(TextArea), this); textView.LineTransformers.Add(new SelectionColorizer(this)); textView.InsertLayer(new SelectionLayer(this), KnownLayer.Selection, LayerInsertionPosition.Replace); caret = new Caret(this); caret.PositionChanged += (sender, e) => RequestSelectionValidation(); caret.PositionChanged += CaretPositionChanged; AttachTypingEvents(); ime = new ImeSupport(this); leftMargins.CollectionChanged += leftMargins_CollectionChanged; this.DefaultInputHandler = new TextAreaDefaultInputHandler(this); this.ActiveInputHandler = this.DefaultInputHandler; } #endregion #region InputHandler management /// /// Gets the default input handler. /// /// public TextAreaDefaultInputHandler DefaultInputHandler { get; private set; } ITextAreaInputHandler activeInputHandler; bool isChangingInputHandler; /// /// Gets/Sets the active input handler. /// This property does not return currently active stacked input handlers. Setting this property detached all stacked input handlers. /// /// public ITextAreaInputHandler ActiveInputHandler { get { return activeInputHandler; } set { if (value != null && value.TextArea != this) throw new ArgumentException("The input handler was created for a different text area than this one."); if (isChangingInputHandler) throw new InvalidOperationException("Cannot set ActiveInputHandler recursively"); if (activeInputHandler != value) { isChangingInputHandler = true; try { // pop the whole stack PopStackedInputHandler(stackedInputHandlers.LastOrDefault()); Debug.Assert(stackedInputHandlers.IsEmpty); if (activeInputHandler != null) activeInputHandler.Detach(); activeInputHandler = value; if (value != null) value.Attach(); } finally { isChangingInputHandler = false; } if (ActiveInputHandlerChanged != null) ActiveInputHandlerChanged(this, EventArgs.Empty); } } } /// /// Occurs when the ActiveInputHandler property changes. /// public event EventHandler ActiveInputHandlerChanged; ImmutableStack stackedInputHandlers = ImmutableStack.Empty; /// /// Gets the list of currently active stacked input handlers. /// /// public ImmutableStack StackedInputHandlers { get { return stackedInputHandlers; } } /// /// Pushes an input handler onto the list of stacked input handlers. /// /// public void PushStackedInputHandler(TextAreaStackedInputHandler inputHandler) { if (inputHandler == null) throw new ArgumentNullException("inputHandler"); stackedInputHandlers = stackedInputHandlers.Push(inputHandler); inputHandler.Attach(); } /// /// Pops the stacked input handler (and all input handlers above it). /// If is not found in the currently stacked input handlers, or is null, this method /// does nothing. /// /// public void PopStackedInputHandler(TextAreaStackedInputHandler inputHandler) { if (stackedInputHandlers.Any(i => i == inputHandler)) { ITextAreaInputHandler oldHandler; do { oldHandler = stackedInputHandlers.Peek(); stackedInputHandlers = stackedInputHandlers.Pop(); oldHandler.Detach(); } while (oldHandler != inputHandler); } } #endregion #region Document property /// /// Document property. /// public static readonly DependencyProperty DocumentProperty = TextView.DocumentProperty.AddOwner(typeof(TextArea), new FrameworkPropertyMetadata(OnDocumentChanged)); /// /// Gets/Sets the document displayed by the text editor. /// public TextDocument Document { get { return (TextDocument)GetValue(DocumentProperty); } set { SetValue(DocumentProperty, value); } } /// public event EventHandler DocumentChanged; static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) { ((TextArea)dp).OnDocumentChanged((TextDocument)e.OldValue, (TextDocument)e.NewValue); } void OnDocumentChanged(TextDocument oldValue, TextDocument newValue) { if (oldValue != null) { TextDocumentWeakEventManager.Changing.RemoveListener(oldValue, this); TextDocumentWeakEventManager.Changed.RemoveListener(oldValue, this); TextDocumentWeakEventManager.UpdateStarted.RemoveListener(oldValue, this); TextDocumentWeakEventManager.UpdateFinished.RemoveListener(oldValue, this); } textView.Document = newValue; if (newValue != null) { TextDocumentWeakEventManager.Changing.AddListener(newValue, this); TextDocumentWeakEventManager.Changed.AddListener(newValue, this); TextDocumentWeakEventManager.UpdateStarted.AddListener(newValue, this); TextDocumentWeakEventManager.UpdateFinished.AddListener(newValue, this); } // Reset caret location and selection: this is necessary because the caret/selection might be invalid // in the new document (e.g. if new document is shorter than the old document). caret.Location = new TextLocation(1, 1); this.ClearSelection(); if (DocumentChanged != null) DocumentChanged(this, EventArgs.Empty); CommandManager.InvalidateRequerySuggested(); } #endregion #region Options property /// /// Options property. /// public static readonly DependencyProperty OptionsProperty = TextView.OptionsProperty.AddOwner(typeof(TextArea), new FrameworkPropertyMetadata(OnOptionsChanged)); /// /// Gets/Sets the document displayed by the text editor. /// public TextEditorOptions Options { get { return (TextEditorOptions)GetValue(OptionsProperty); } set { SetValue(OptionsProperty, value); } } /// /// Occurs when a text editor option has changed. /// public event PropertyChangedEventHandler OptionChanged; /// /// Raises the event. /// protected virtual void OnOptionChanged(PropertyChangedEventArgs e) { if (OptionChanged != null) { OptionChanged(this, e); } } static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) { ((TextArea)dp).OnOptionsChanged((TextEditorOptions)e.OldValue, (TextEditorOptions)e.NewValue); } void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue) { if (oldValue != null) { PropertyChangedWeakEventManager.RemoveListener(oldValue, this); } textView.Options = newValue; if (newValue != null) { PropertyChangedWeakEventManager.AddListener(newValue, this); } OnOptionChanged(new PropertyChangedEventArgs(null)); } #endregion #region ReceiveWeakEvent /// protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { if (managerType == typeof(TextDocumentWeakEventManager.Changing)) { OnDocumentChanging(); return true; } else if (managerType == typeof(TextDocumentWeakEventManager.Changed)) { OnDocumentChanged((DocumentChangeEventArgs)e); return true; } else if (managerType == typeof(TextDocumentWeakEventManager.UpdateStarted)) { OnUpdateStarted(); return true; } else if (managerType == typeof(TextDocumentWeakEventManager.UpdateFinished)) { OnUpdateFinished(); return true; } else if (managerType == typeof(PropertyChangedWeakEventManager)) { OnOptionChanged((PropertyChangedEventArgs)e); return true; } return false; } bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { return ReceiveWeakEvent(managerType, sender, e); } #endregion #region Caret handling on document changes void OnDocumentChanging() { caret.OnDocumentChanging(); } void OnDocumentChanged(DocumentChangeEventArgs e) { caret.OnDocumentChanged(e); this.Selection = selection.UpdateOnDocumentChange(e); } void OnUpdateStarted() { Document.UndoStack.PushOptional(new RestoreCaretAndSelectionUndoAction(this)); } void OnUpdateFinished() { caret.OnDocumentUpdateFinished(); } sealed class RestoreCaretAndSelectionUndoAction : IUndoableOperation { // keep textarea in weak reference because the IUndoableOperation is stored with the document WeakReference textAreaReference; TextViewPosition caretPosition; Selection selection; public RestoreCaretAndSelectionUndoAction(TextArea textArea) { this.textAreaReference = new WeakReference(textArea); // Just save the old caret position, no need to validate here. // If we restore it, we'll validate it anyways. this.caretPosition = textArea.Caret.NonValidatedPosition; this.selection = textArea.Selection; } public void Undo() { TextArea textArea = (TextArea)textAreaReference.Target; if (textArea != null) { textArea.Caret.Position = caretPosition; textArea.Selection = selection; } } public void Redo() { // redo=undo: we just restore the caret/selection state Undo(); } } #endregion #region TextView property readonly TextView textView; IScrollInfo scrollInfo; /// /// Gets the text view used to display text in this text area. /// public TextView TextView { get { return textView; } } /// public override void OnApplyTemplate() { base.OnApplyTemplate(); scrollInfo = textView; ApplyScrollInfo(); } #endregion #region Selection property internal readonly Selection emptySelection; Selection selection; /// /// Occurs when the selection has changed. /// public event EventHandler SelectionChanged; /// /// Gets/Sets the selection in this text area. /// public Selection Selection { get { return selection; } set { if (value == null) throw new ArgumentNullException("value"); if (value.textArea != this) throw new ArgumentException("Cannot use a Selection instance that belongs to another text area."); if (!object.Equals(selection, value)) { // Debug.WriteLine("Selection change from " + selection + " to " + value); if (textView != null) { ISegment oldSegment = selection.SurroundingSegment; ISegment newSegment = value.SurroundingSegment; if (!Selection.EnableVirtualSpace && (selection is SimpleSelection && value is SimpleSelection && oldSegment != null && newSegment != null)) { // perf optimization: // When a simple selection changes, don't redraw the whole selection, but only the changed parts. int oldSegmentOffset = oldSegment.Offset; int newSegmentOffset = newSegment.Offset; if (oldSegmentOffset != newSegmentOffset) { textView.Redraw(Math.Min(oldSegmentOffset, newSegmentOffset), Math.Abs(oldSegmentOffset - newSegmentOffset), DispatcherPriority.Background); } int oldSegmentEndOffset = oldSegment.EndOffset; int newSegmentEndOffset = newSegment.EndOffset; if (oldSegmentEndOffset != newSegmentEndOffset) { textView.Redraw(Math.Min(oldSegmentEndOffset, newSegmentEndOffset), Math.Abs(oldSegmentEndOffset - newSegmentEndOffset), DispatcherPriority.Background); } } else { textView.Redraw(oldSegment, DispatcherPriority.Background); textView.Redraw(newSegment, DispatcherPriority.Background); } } selection = value; if (SelectionChanged != null) SelectionChanged(this, EventArgs.Empty); // a selection change causes commands like copy/paste/etc. to change status CommandManager.InvalidateRequerySuggested(); } } } /// /// Clears the current selection. /// public void ClearSelection() { this.Selection = emptySelection; } /// /// The property. /// public static readonly DependencyProperty SelectionBrushProperty = DependencyProperty.Register("SelectionBrush", typeof(Brush), typeof(TextArea)); /// /// Gets/Sets the background brush used for the selection. /// public Brush SelectionBrush { get { return (Brush)GetValue(SelectionBrushProperty); } set { SetValue(SelectionBrushProperty, value); } } /// /// The property. /// public static readonly DependencyProperty SelectionForegroundProperty = DependencyProperty.Register("SelectionForeground", typeof(Brush), typeof(TextArea)); /// /// Gets/Sets the foreground brush used selected text. /// public Brush SelectionForeground { get { return (Brush)GetValue(SelectionForegroundProperty); } set { SetValue(SelectionForegroundProperty, value); } } /// /// The property. /// public static readonly DependencyProperty SelectionBorderProperty = DependencyProperty.Register("SelectionBorder", typeof(Pen), typeof(TextArea)); /// /// Gets/Sets the background brush used for the selection. /// public Pen SelectionBorder { get { return (Pen)GetValue(SelectionBorderProperty); } set { SetValue(SelectionBorderProperty, value); } } /// /// The property. /// public static readonly DependencyProperty SelectionCornerRadiusProperty = DependencyProperty.Register("SelectionCornerRadius", typeof(double), typeof(TextArea), new FrameworkPropertyMetadata(3.0)); /// /// Gets/Sets the corner radius of the selection. /// public double SelectionCornerRadius { get { return (double)GetValue(SelectionCornerRadiusProperty); } set { SetValue(SelectionCornerRadiusProperty, value); } } #endregion #region Force caret to stay inside selection bool ensureSelectionValidRequested; int allowCaretOutsideSelection; void RequestSelectionValidation() { if (!ensureSelectionValidRequested && allowCaretOutsideSelection == 0) { ensureSelectionValidRequested = true; Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(EnsureSelectionValid)); } } /// /// Code that updates only the caret but not the selection can cause confusion when /// keys like 'Delete' delete the (possibly invisible) selected text and not the /// text around the caret. /// /// So we'll ensure that the caret is inside the selection. /// (when the caret is not in the selection, we'll clear the selection) /// /// This method is invoked using the Dispatcher so that code may temporarily violate this rule /// (e.g. most 'extend selection' methods work by first setting the caret, then the selection), /// it's sufficient to fix it after any event handlers have run. /// void EnsureSelectionValid() { ensureSelectionValidRequested = false; if (allowCaretOutsideSelection == 0) { if (!selection.IsEmpty && !selection.Contains(caret.Offset)) { Debug.WriteLine("Resetting selection because caret is outside"); this.ClearSelection(); } } } /// /// Temporarily allows positioning the caret outside the selection. /// Dispose the returned IDisposable to revert the allowance. /// /// /// The text area only forces the caret to be inside the selection when other events /// have finished running (using the dispatcher), so you don't have to use this method /// for temporarily positioning the caret in event handlers. /// This method is only necessary if you want to run the WPF dispatcher, e.g. if you /// perform a drag'n'drop operation. /// public IDisposable AllowCaretOutsideSelection() { VerifyAccess(); allowCaretOutsideSelection++; return new CallbackOnDispose( delegate { VerifyAccess(); allowCaretOutsideSelection--; RequestSelectionValidation(); }); } #endregion #region Properties readonly Caret caret; /// /// Gets the Caret used for this text area. /// public Caret Caret { get { return caret; } } void CaretPositionChanged(object sender, EventArgs e) { if (textView == null) return; this.textView.HighlightedLine = this.Caret.Line; } ObservableCollection leftMargins = new ObservableCollection(); /// /// Gets the collection of margins displayed to the left of the text view. /// public ObservableCollection LeftMargins { get { return leftMargins; } } void leftMargins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.OldItems != null) { foreach (ITextViewConnect c in e.OldItems.OfType()) { c.RemoveFromTextView(textView); } } if (e.NewItems != null) { foreach (ITextViewConnect c in e.NewItems.OfType()) { c.AddToTextView(textView); } } } IReadOnlySectionProvider readOnlySectionProvider = NoReadOnlySections.Instance; /// /// Gets/Sets an object that provides read-only sections for the text area. /// public IReadOnlySectionProvider ReadOnlySectionProvider { get { return readOnlySectionProvider; } set { if (value == null) throw new ArgumentNullException("value"); readOnlySectionProvider = value; CommandManager.InvalidateRequerySuggested(); // the read-only status effects Paste.CanExecute and the IME } } #endregion #region IScrollInfo implementation ScrollViewer scrollOwner; bool canVerticallyScroll, canHorizontallyScroll; void ApplyScrollInfo() { if (scrollInfo != null) { scrollInfo.ScrollOwner = scrollOwner; scrollInfo.CanVerticallyScroll = canVerticallyScroll; scrollInfo.CanHorizontallyScroll = canHorizontallyScroll; scrollOwner = null; } } bool IScrollInfo.CanVerticallyScroll { get { return scrollInfo != null ? scrollInfo.CanVerticallyScroll : false; } set { canVerticallyScroll = value; if (scrollInfo != null) scrollInfo.CanVerticallyScroll = value; } } bool IScrollInfo.CanHorizontallyScroll { get { return scrollInfo != null ? scrollInfo.CanHorizontallyScroll : false; } set { canHorizontallyScroll = value; if (scrollInfo != null) scrollInfo.CanHorizontallyScroll = value; } } double IScrollInfo.ExtentWidth { get { return scrollInfo != null ? scrollInfo.ExtentWidth : 0; } } double IScrollInfo.ExtentHeight { get { return scrollInfo != null ? scrollInfo.ExtentHeight : 0; } } double IScrollInfo.ViewportWidth { get { return scrollInfo != null ? scrollInfo.ViewportWidth : 0; } } double IScrollInfo.ViewportHeight { get { return scrollInfo != null ? scrollInfo.ViewportHeight : 0; } } double IScrollInfo.HorizontalOffset { get { return scrollInfo != null ? scrollInfo.HorizontalOffset : 0; } } double IScrollInfo.VerticalOffset { get { return scrollInfo != null ? scrollInfo.VerticalOffset : 0; } } ScrollViewer IScrollInfo.ScrollOwner { get { return scrollInfo != null ? scrollInfo.ScrollOwner : null; } set { if (scrollInfo != null) scrollInfo.ScrollOwner = value; else scrollOwner = value; } } void IScrollInfo.LineUp() { if (scrollInfo != null) scrollInfo.LineUp(); } void IScrollInfo.LineDown() { if (scrollInfo != null) scrollInfo.LineDown(); } void IScrollInfo.LineLeft() { if (scrollInfo != null) scrollInfo.LineLeft(); } void IScrollInfo.LineRight() { if (scrollInfo != null) scrollInfo.LineRight(); } void IScrollInfo.PageUp() { if (scrollInfo != null) scrollInfo.PageUp(); } void IScrollInfo.PageDown() { if (scrollInfo != null) scrollInfo.PageDown(); } void IScrollInfo.PageLeft() { if (scrollInfo != null) scrollInfo.PageLeft(); } void IScrollInfo.PageRight() { if (scrollInfo != null) scrollInfo.PageRight(); } void IScrollInfo.MouseWheelUp() { if (scrollInfo != null) scrollInfo.MouseWheelUp(); } void IScrollInfo.MouseWheelDown() { if (scrollInfo != null) scrollInfo.MouseWheelDown(); } void IScrollInfo.MouseWheelLeft() { if (scrollInfo != null) scrollInfo.MouseWheelLeft(); } void IScrollInfo.MouseWheelRight() { if (scrollInfo != null) scrollInfo.MouseWheelRight(); } void IScrollInfo.SetHorizontalOffset(double offset) { if (scrollInfo != null) scrollInfo.SetHorizontalOffset(offset); } void IScrollInfo.SetVerticalOffset(double offset) { if (scrollInfo != null) scrollInfo.SetVerticalOffset(offset); } Rect IScrollInfo.MakeVisible(System.Windows.Media.Visual visual, Rect rectangle) { if (scrollInfo != null) return scrollInfo.MakeVisible(visual, rectangle); else return Rect.Empty; } #endregion #region Focus Handling (Show/Hide Caret) /// protected override void OnMouseDown(MouseButtonEventArgs e) { base.OnMouseDown(e); Focus(); } /// protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) { base.OnGotKeyboardFocus(e); // First activate IME, then show caret ime.OnGotKeyboardFocus(e); caret.Show(); } /// protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) { base.OnLostKeyboardFocus(e); caret.Hide(); ime.OnLostKeyboardFocus(e); } #endregion #region OnTextInput / RemoveSelectedText / ReplaceSelectionWithText /// /// Occurs when the TextArea receives text input. /// This is like the event, /// but occurs immediately before the TextArea handles the TextInput event. /// public event TextCompositionEventHandler TextEntering; /// /// Occurs when the TextArea receives text input. /// This is like the event, /// but occurs immediately after the TextArea handles the TextInput event. /// public event TextCompositionEventHandler TextEntered; /// /// Raises the TextEntering event. /// protected virtual void OnTextEntering(TextCompositionEventArgs e) { if (TextEntering != null) { TextEntering(this, e); } } /// /// Raises the TextEntered event. /// protected virtual void OnTextEntered(TextCompositionEventArgs e) { if (TextEntered != null) { TextEntered(this, e); } } /// protected override void OnTextInput(TextCompositionEventArgs e) { //Debug.WriteLine("TextInput: Text='" + e.Text + "' SystemText='" + e.SystemText + "' ControlText='" + e.ControlText + "'"); base.OnTextInput(e); if (!e.Handled && this.Document != null) { if (string.IsNullOrEmpty(e.Text) || e.Text == "\x1b" || e.Text == "\b") { // ASCII 0x1b = ESC. // WPF produces a TextInput event with that old ASCII control char // when Escape is pressed. We'll just ignore it. // A deadkey followed by backspace causes a textinput event for the BS character. // Similarly, some shortcuts like Alt+Space produce an empty TextInput event. // We have to ignore those (not handle them) to keep the shortcut working. return; } HideMouseCursor(); PerformTextInput(e); e.Handled = true; } } /// /// Performs text input. /// This raises the event, replaces the selection with the text, /// and then raises the event. /// public void PerformTextInput(string text) { TextComposition textComposition = new TextComposition(InputManager.Current, this, text); TextCompositionEventArgs e = new TextCompositionEventArgs(Keyboard.PrimaryDevice, textComposition); e.RoutedEvent = TextInputEvent; PerformTextInput(e); } /// /// Performs text input. /// This raises the event, replaces the selection with the text, /// and then raises the event. /// public void PerformTextInput(TextCompositionEventArgs e) { if (e == null) throw new ArgumentNullException("e"); if (this.Document == null) throw ThrowUtil.NoDocumentAssigned(); OnTextEntering(e); if (!e.Handled) { if (e.Text == "\n" || e.Text == "\r" || e.Text == "\r\n") ReplaceSelectionWithNewLine(); else { if (OverstrikeMode && Selection.IsEmpty && Document.GetLineByNumber(Caret.Line).EndOffset > Caret.Offset) EditingCommands.SelectRightByCharacter.Execute(null, this); ReplaceSelectionWithText(e.Text); } OnTextEntered(e); caret.BringCaretToView(); } } void ReplaceSelectionWithNewLine() { string newLine = TextUtilities.GetNewLineFromDocument(this.Document, this.Caret.Line); using (this.Document.RunUpdate()) { ReplaceSelectionWithText(newLine); if (this.IndentationStrategy != null) { DocumentLine line = this.Document.GetLineByNumber(this.Caret.Line); ISegment[] deletable = GetDeletableSegments(line); if (deletable.Length == 1 && deletable[0].Offset == line.Offset && deletable[0].Length == line.Length) { // use indentation strategy only if the line is not read-only this.IndentationStrategy.IndentLine(this.Document, line); } } } } internal void RemoveSelectedText() { if (this.Document == null) throw ThrowUtil.NoDocumentAssigned(); selection.ReplaceSelectionWithText(string.Empty); #if DEBUG if (!selection.IsEmpty) { foreach (ISegment s in selection.Segments) { Debug.Assert(this.ReadOnlySectionProvider.GetDeletableSegments(s).Count() == 0); } } #endif } internal void ReplaceSelectionWithText(string newText) { if (newText == null) throw new ArgumentNullException("newText"); if (this.Document == null) throw ThrowUtil.NoDocumentAssigned(); selection.ReplaceSelectionWithText(newText); } internal ISegment[] GetDeletableSegments(ISegment segment) { var deletableSegments = this.ReadOnlySectionProvider.GetDeletableSegments(segment); if (deletableSegments == null) throw new InvalidOperationException("ReadOnlySectionProvider.GetDeletableSegments returned null"); var array = deletableSegments.ToArray(); int lastIndex = segment.Offset; for (int i = 0; i < array.Length; i++) { if (array[i].Offset < lastIndex) throw new InvalidOperationException("ReadOnlySectionProvider returned incorrect segments (outside of input segment / wrong order)"); lastIndex = array[i].EndOffset; } if (lastIndex > segment.EndOffset) throw new InvalidOperationException("ReadOnlySectionProvider returned incorrect segments (outside of input segment / wrong order)"); return array; } #endregion #region IndentationStrategy property /// /// IndentationStrategy property. /// public static readonly DependencyProperty IndentationStrategyProperty = DependencyProperty.Register("IndentationStrategy", typeof(IIndentationStrategy), typeof(TextArea), new FrameworkPropertyMetadata(new DefaultIndentationStrategy())); /// /// Gets/Sets the indentation strategy used when inserting new lines. /// public IIndentationStrategy IndentationStrategy { get { return (IIndentationStrategy)GetValue(IndentationStrategyProperty); } set { SetValue(IndentationStrategyProperty, value); } } #endregion #region OnKeyDown/OnKeyUp /// protected override void OnPreviewKeyDown(KeyEventArgs e) { base.OnPreviewKeyDown(e); if (!e.Handled && e.Key == Key.Insert && this.Options.AllowToggleOverstrikeMode) { this.OverstrikeMode = !this.OverstrikeMode; e.Handled = true; return; } foreach (TextAreaStackedInputHandler h in stackedInputHandlers) { if (e.Handled) break; h.OnPreviewKeyDown(e); } } /// protected override void OnPreviewKeyUp(KeyEventArgs e) { base.OnPreviewKeyUp(e); foreach (TextAreaStackedInputHandler h in stackedInputHandlers) { if (e.Handled) break; h.OnPreviewKeyUp(e); } } // Make life easier for text editor extensions that use a different cursor based on the pressed modifier keys. /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); TextView.InvalidateCursorIfMouseWithinTextView(); } /// protected override void OnKeyUp(KeyEventArgs e) { base.OnKeyUp(e); TextView.InvalidateCursorIfMouseWithinTextView(); } #endregion #region Hide Mouse Cursor While Typing bool isMouseCursorHidden; void AttachTypingEvents() { // Use the PreviewMouseMove event in case some other editor layer consumes the MouseMove event (e.g. SD's InsertionCursorLayer) this.MouseEnter += delegate { ShowMouseCursor(); }; this.MouseLeave += delegate { ShowMouseCursor(); }; this.PreviewMouseMove += delegate { ShowMouseCursor(); }; #if DOTNET4 this.TouchEnter += delegate { ShowMouseCursor(); }; this.TouchLeave += delegate { ShowMouseCursor(); }; this.PreviewTouchMove += delegate { ShowMouseCursor(); }; #endif } void ShowMouseCursor() { if (this.isMouseCursorHidden) { System.Windows.Forms.Cursor.Show(); this.isMouseCursorHidden = false; } } void HideMouseCursor() { if (Options.HideCursorWhileTyping && !this.isMouseCursorHidden && this.IsMouseOver) { this.isMouseCursorHidden = true; System.Windows.Forms.Cursor.Hide(); } } #endregion #region Overstrike mode /// /// The dependency property. /// public static readonly DependencyProperty OverstrikeModeProperty = DependencyProperty.Register("OverstrikeMode", typeof(bool), typeof(TextArea), new FrameworkPropertyMetadata(Boxes.False)); /// /// Gets/Sets whether overstrike mode is active. /// public bool OverstrikeMode { get { return (bool)GetValue(OverstrikeModeProperty); } set { SetValue(OverstrikeModeProperty, value); } } #endregion /// protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) { // accept clicks even where the text area draws no background return new PointHitTestResult(this, hitTestParameters.HitPoint); } /// protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { base.OnPropertyChanged(e); if (e.Property == SelectionBrushProperty || e.Property == SelectionBorderProperty || e.Property == SelectionForegroundProperty || e.Property == SelectionCornerRadiusProperty) { textView.Redraw(); } else if (e.Property == OverstrikeModeProperty) { caret.UpdateIfVisible(); } } /// /// Gets the requested service. /// /// Returns the requested service instance, or null if the service cannot be found. public virtual object GetService(Type serviceType) { return textView.GetService(serviceType); } /// /// Occurs when text inside the TextArea was copied. /// public event EventHandler TextCopied; internal void OnTextCopied(TextEventArgs e) { if (TextCopied != null) TextCopied(this, e); } } /// /// EventArgs with text. /// [Serializable] public class TextEventArgs : EventArgs { string text; /// /// Gets the text. /// public string Text { get { return text; } } /// /// Creates a new TextEventArgs instance. /// public TextEventArgs(string text) { if (text == null) throw new ArgumentNullException("text"); this.text = text; } } }