diff options
| author | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2019-04-09 01:47:48 +0300 |
|---|---|---|
| committer | Roy Ben Shabat <Roy.mail.net@gmail.com> | 2019-04-09 01:47:48 +0300 |
| commit | 080f1697e97e13461ec6df4d31c8924d01257a1b (patch) | |
| tree | b1fe0285de7bc9bc52e9e2195e66fe022bf8f5b3 /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing | |
| parent | 1608e69a417bc5e40a607c3958c4a60f19f66f1a (diff) | |
| download | Tango-080f1697e97e13461ec6df4d31c8924d01257a1b.tar.gz Tango-080f1697e97e13461ec6df4d31c8924d01257a1b.zip | |
MERGE
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing')
25 files changed, 5650 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/AbstractMargin.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/AbstractMargin.cs new file mode 100644 index 000000000..c82ff94a0 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/AbstractMargin.cs @@ -0,0 +1,102 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Diagnostics; +using System.Windows; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Base class for margins. + /// Margins don't have to derive from this class, it just helps maintaining a reference to the TextView + /// and the TextDocument. + /// AbstractMargin derives from FrameworkElement, so if you don't want to handle visual children and rendering + /// on your own, choose another base class for your margin! + /// </summary> + public abstract class AbstractMargin : FrameworkElement, ITextViewConnect + { + /// <summary> + /// TextView property. + /// </summary> + public static readonly DependencyProperty TextViewProperty = + DependencyProperty.Register("TextView", typeof(TextView), typeof(AbstractMargin), + new FrameworkPropertyMetadata(OnTextViewChanged)); + + /// <summary> + /// Gets/sets the text view for which line numbers are displayed. + /// </summary> + /// <remarks>Adding a margin to <see cref="TextArea.LeftMargins"/> will automatically set this property to the text area's TextView.</remarks> + public TextView TextView { + get { return (TextView)GetValue(TextViewProperty); } + set { SetValue(TextViewProperty, value); } + } + + static void OnTextViewChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) + { + AbstractMargin margin = (AbstractMargin)dp; + margin.wasAutoAddedToTextView = false; + margin.OnTextViewChanged((TextView)e.OldValue, (TextView)e.NewValue); + } + + // automatically set/unset TextView property using ITextViewConnect + bool wasAutoAddedToTextView; + + void ITextViewConnect.AddToTextView(TextView textView) + { + if (this.TextView == null) { + this.TextView = textView; + wasAutoAddedToTextView = true; + } else if (this.TextView != textView) { + throw new InvalidOperationException("This margin belongs to a different TextView."); + } + } + + void ITextViewConnect.RemoveFromTextView(TextView textView) + { + if (wasAutoAddedToTextView && this.TextView == textView) { + this.TextView = null; + Debug.Assert(!wasAutoAddedToTextView); // setting this.TextView should have unset this flag + } + } + + TextDocument document; + + /// <summary> + /// Gets the document associated with the margin. + /// </summary> + public TextDocument Document { + get { return document; } + } + + /// <summary> + /// Called when the <see cref="TextView"/> is changing. + /// </summary> + protected virtual void OnTextViewChanged(TextView oldTextView, TextView newTextView) + { + if (oldTextView != null) { + oldTextView.DocumentChanged -= TextViewDocumentChanged; + } + if (newTextView != null) { + newTextView.DocumentChanged += TextViewDocumentChanged; + } + TextViewDocumentChanged(null, null); + } + + void TextViewDocumentChanged(object sender, EventArgs e) + { + OnDocumentChanged(document, TextView != null ? TextView.Document : null); + } + + /// <summary> + /// Called when the <see cref="Document"/> is changing. + /// </summary> + protected virtual void OnDocumentChanged(TextDocument oldDocument, TextDocument newDocument) + { + document = newDocument; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/Caret.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/Caret.cs new file mode 100644 index 000000000..fb21448e5 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/Caret.cs @@ -0,0 +1,472 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; +using System.Windows.Threading; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Helper class with caret-related methods. + /// </summary> + public sealed class Caret + { + readonly TextArea textArea; + readonly TextView textView; + readonly CaretLayer caretAdorner; + bool visible; + + internal Caret(TextArea textArea) + { + this.textArea = textArea; + this.textView = textArea.TextView; + position = new TextViewPosition(1, 1, 0); + + caretAdorner = new CaretLayer(textView); + textView.InsertLayer(caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); + textView.VisualLinesChanged += TextView_VisualLinesChanged; + textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged; + } + + void TextView_VisualLinesChanged(object sender, EventArgs e) + { + if (visible) { + Show(); + } + // required because the visual columns might have changed if the + // element generators did something differently than on the last run + // (e.g. a FoldingSection was collapsed) + InvalidateVisualColumn(); + } + + void TextView_ScrollOffsetChanged(object sender, EventArgs e) + { + if (caretAdorner != null) { + caretAdorner.InvalidateVisual(); + } + } + + double desiredXPos = double.NaN; + TextViewPosition position; + + /// <summary> + /// Gets/Sets the position of the caret. + /// Retrieving this property will validate the visual column (which can be expensive). + /// Use the <see cref="Location"/> property instead if you don't need the visual column. + /// </summary> + public TextViewPosition Position { + get { + ValidateVisualColumn(); + return position; + } + set { + if (position != value) { + position = value; + + storedCaretOffset = -1; + + //Debug.WriteLine("Caret position changing to " + value); + + ValidatePosition(); + InvalidateVisualColumn(); + RaisePositionChanged(); + Log("Caret position changed to " + value); + if (visible) + Show(); + } + } + } + + /// <summary> + /// Gets the caret position without validating it. + /// </summary> + internal TextViewPosition NonValidatedPosition { + get { + return position; + } + } + + /// <summary> + /// Gets/Sets the location of the caret. + /// The getter of this property is faster than <see cref="Position"/> because it doesn't have + /// to validate the visual column. + /// </summary> + public TextLocation Location { + get { + return position.Location; + } + set { + this.Position = new TextViewPosition(value); + } + } + + /// <summary> + /// Gets/Sets the caret line. + /// </summary> + public int Line { + get { return position.Line; } + set { + this.Position = new TextViewPosition(value, position.Column); + } + } + + /// <summary> + /// Gets/Sets the caret column. + /// </summary> + public int Column { + get { return position.Column; } + set { + this.Position = new TextViewPosition(position.Line, value); + } + } + + /// <summary> + /// Gets/Sets the caret visual column. + /// </summary> + public int VisualColumn { + get { + ValidateVisualColumn(); + return position.VisualColumn; + } + set { + this.Position = new TextViewPosition(position.Line, position.Column, value); + } + } + + bool isInVirtualSpace; + + /// <summary> + /// Gets whether the caret is in virtual space. + /// </summary> + public bool IsInVirtualSpace { + get { + ValidateVisualColumn(); + return isInVirtualSpace; + } + } + + int storedCaretOffset; + + internal void OnDocumentChanging() + { + storedCaretOffset = this.Offset; + InvalidateVisualColumn(); + } + + internal void OnDocumentChanged(DocumentChangeEventArgs e) + { + InvalidateVisualColumn(); + if (storedCaretOffset >= 0) { + int newCaretOffset = e.GetNewOffset(storedCaretOffset, AnchorMovementType.Default); + TextDocument document = textArea.Document; + if (document != null) { + // keep visual column + this.Position = new TextViewPosition(document.GetLocation(newCaretOffset), position.VisualColumn); + } + } + storedCaretOffset = -1; + } + + /// <summary> + /// Gets/Sets the caret offset. + /// Setting the caret offset has the side effect of setting the <see cref="DesiredXPos"/> to NaN. + /// </summary> + public int Offset { + get { + TextDocument document = textArea.Document; + if (document == null) { + return 0; + } else { + return document.GetOffset(position.Location); + } + } + set { + TextDocument document = textArea.Document; + if (document != null) { + this.Position = new TextViewPosition(document.GetLocation(value)); + this.DesiredXPos = double.NaN; + } + } + } + + /// <summary> + /// Gets/Sets the desired x-position of the caret, in device-independent pixels. + /// This property is NaN if the caret has no desired position. + /// </summary> + public double DesiredXPos { + get { return desiredXPos; } + set { desiredXPos = value; } + } + + void ValidatePosition() + { + if (position.Line < 1) + position.Line = 1; + if (position.Column < 1) + position.Column = 1; + if (position.VisualColumn < -1) + position.VisualColumn = -1; + TextDocument document = textArea.Document; + if (document != null) { + if (position.Line > document.LineCount) { + position.Line = document.LineCount; + position.Column = document.GetLineByNumber(position.Line).Length + 1; + position.VisualColumn = -1; + } else { + DocumentLine line = document.GetLineByNumber(position.Line); + if (position.Column > line.Length + 1) { + position.Column = line.Length + 1; + position.VisualColumn = -1; + } + } + } + } + + /// <summary> + /// Event raised when the caret position has changed. + /// If the caret position is changed inside a document update (between BeginUpdate/EndUpdate calls), + /// the PositionChanged event is raised only once at the end of the document update. + /// </summary> + public event EventHandler PositionChanged; + + bool raisePositionChangedOnUpdateFinished; + + void RaisePositionChanged() + { + if (textArea.Document != null && textArea.Document.IsInUpdate) { + raisePositionChangedOnUpdateFinished = true; + } else { + if (PositionChanged != null) { + PositionChanged(this, EventArgs.Empty); + } + } + } + + internal void OnDocumentUpdateFinished() + { + if (raisePositionChangedOnUpdateFinished) { + if (PositionChanged != null) { + PositionChanged(this, EventArgs.Empty); + } + } + } + + bool visualColumnValid; + + void ValidateVisualColumn() + { + if (!visualColumnValid) { + TextDocument document = textArea.Document; + if (document != null) { + //Debug.WriteLine("Explicit validation of caret column"); + var documentLine = document.GetLineByNumber(position.Line); + RevalidateVisualColumn(textView.GetOrConstructVisualLine(documentLine)); + } + } + } + + void InvalidateVisualColumn() + { + visualColumnValid = false; + } + + /// <summary> + /// Validates the visual column of the caret using the specified visual line. + /// The visual line must contain the caret offset. + /// </summary> + void RevalidateVisualColumn(VisualLine visualLine) + { + if (visualLine == null) + throw new ArgumentNullException("visualLine"); + + // mark column as validated + visualColumnValid = true; + + int caretOffset = textView.Document.GetOffset(position.Location); + int firstDocumentLineOffset = visualLine.FirstDocumentLine.Offset; + position.VisualColumn = visualLine.ValidateVisualColumn(position, textArea.Selection.EnableVirtualSpace); + + // search possible caret positions + int newVisualColumnForwards = visualLine.GetNextCaretPosition(position.VisualColumn - 1, LogicalDirection.Forward, CaretPositioningMode.Normal, textArea.Selection.EnableVirtualSpace); + // If position.VisualColumn was valid, we're done with validation. + if (newVisualColumnForwards != position.VisualColumn) { + // also search backwards so that we can pick the better match + int newVisualColumnBackwards = visualLine.GetNextCaretPosition(position.VisualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.Normal, textArea.Selection.EnableVirtualSpace); + + if (newVisualColumnForwards < 0 && newVisualColumnBackwards < 0) + throw ThrowUtil.NoValidCaretPosition(); + + // determine offsets for new visual column positions + int newOffsetForwards; + if (newVisualColumnForwards >= 0) + newOffsetForwards = visualLine.GetRelativeOffset(newVisualColumnForwards) + firstDocumentLineOffset; + else + newOffsetForwards = -1; + int newOffsetBackwards; + if (newVisualColumnBackwards >= 0) + newOffsetBackwards = visualLine.GetRelativeOffset(newVisualColumnBackwards) + firstDocumentLineOffset; + else + newOffsetBackwards = -1; + + int newVisualColumn, newOffset; + // if there's only one valid position, use it + if (newVisualColumnForwards < 0) { + newVisualColumn = newVisualColumnBackwards; + newOffset = newOffsetBackwards; + } else if (newVisualColumnBackwards < 0) { + newVisualColumn = newVisualColumnForwards; + newOffset = newOffsetForwards; + } else { + // two valid positions: find the better match + if (Math.Abs(newOffsetBackwards - caretOffset) < Math.Abs(newOffsetForwards - caretOffset)) { + // backwards is better + newVisualColumn = newVisualColumnBackwards; + newOffset = newOffsetBackwards; + } else { + // forwards is better + newVisualColumn = newVisualColumnForwards; + newOffset = newOffsetForwards; + } + } + this.Position = new TextViewPosition(textView.Document.GetLocation(newOffset), newVisualColumn); + } + isInVirtualSpace = (position.VisualColumn > visualLine.VisualLength); + } + + Rect CalcCaretRectangle(VisualLine visualLine) + { + if (!visualColumnValid) { + RevalidateVisualColumn(visualLine); + } + + TextLine textLine = visualLine.GetTextLine(position.VisualColumn); + double xPos = visualLine.GetTextLineVisualXPosition(textLine, position.VisualColumn); + double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); + double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); + + return new Rect(xPos, + lineTop, + SystemParameters.CaretWidth, + lineBottom - lineTop); + } + + /// <summary> + /// Returns the caret rectangle. The coordinate system is in device-independent pixels from the top of the document. + /// </summary> + public Rect CalculateCaretRectangle() + { + if (textView != null && textView.Document != null) { + VisualLine visualLine = textView.GetOrConstructVisualLine(textView.Document.GetLineByNumber(position.Line)); + return CalcCaretRectangle(visualLine); + } else { + return Rect.Empty; + } + } + + /// <summary> + /// Minimum distance of the caret to the view border. + /// </summary> + internal const double MinimumDistanceToViewBorder = 30; + + /// <summary> + /// Scrolls the text view so that the caret is visible. + /// </summary> + public void BringCaretToView() + { + BringCaretToView(MinimumDistanceToViewBorder); + } + + internal void BringCaretToView(double border) + { + Rect caretRectangle = CalculateCaretRectangle(); + if (!caretRectangle.IsEmpty) { + caretRectangle.Inflate(border, border); + textView.MakeVisible(caretRectangle); + } + } + + /// <summary> + /// Makes the caret visible and updates its on-screen position. + /// </summary> + public void Show() + { + Log("Caret.Show()"); + visible = true; + if (!showScheduled) { + showScheduled = true; + textArea.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(ShowInternal)); + } + } + + bool showScheduled; + bool hasWin32Caret; + + void ShowInternal() + { + showScheduled = false; + + // if show was scheduled but caret hidden in the meantime + if (!visible) + return; + + if (caretAdorner != null && textView != null) { + VisualLine visualLine = textView.GetVisualLine(position.Line); + if (visualLine != null) { + Rect caretRect = CalcCaretRectangle(visualLine); + // Create Win32 caret so that Windows knows where our managed caret is. This is necessary for + // features like 'Follow text editing' in the Windows Magnifier. + if (!hasWin32Caret) { + hasWin32Caret = Win32.CreateCaret(textView, caretRect.Size); + } + if (hasWin32Caret) { + Win32.SetCaretPosition(textView, caretRect.Location - textView.ScrollOffset); + } + caretAdorner.Show(caretRect); + textArea.ime.UpdateCompositionWindow(); + } else { + caretAdorner.Hide(); + } + } + } + + /// <summary> + /// Makes the caret invisible. + /// </summary> + public void Hide() + { + Log("Caret.Hide()"); + visible = false; + if (hasWin32Caret) { + Win32.DestroyCaret(); + hasWin32Caret = false; + } + if (caretAdorner != null) { + caretAdorner.Hide(); + } + } + + [Conditional("DEBUG")] + static void Log(string text) + { + // commented out to make debug output less noisy - add back if there are any problems with the caret + //Debug.WriteLine(text); + } + + /// <summary> + /// Gets/Sets the color of the caret. + /// </summary> + public Brush CaretBrush { + get { return caretAdorner.CaretBrush; } + set { caretAdorner.CaretBrush = value; } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretLayer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretLayer.cs new file mode 100644 index 000000000..105f38d16 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretLayer.cs @@ -0,0 +1,86 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Threading; + +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + sealed class CaretLayer : Layer + { + bool isVisible; + Rect caretRectangle; + + DispatcherTimer caretBlinkTimer = new DispatcherTimer(); + bool blink; + + public CaretLayer(TextView textView) : base(textView, KnownLayer.Caret) + { + this.IsHitTestVisible = false; + caretBlinkTimer.Tick += new EventHandler(caretBlinkTimer_Tick); + } + + void caretBlinkTimer_Tick(object sender, EventArgs e) + { + blink = !blink; + InvalidateVisual(); + } + + public void Show(Rect caretRectangle) + { + this.caretRectangle = caretRectangle; + this.isVisible = true; + StartBlinkAnimation(); + InvalidateVisual(); + } + + public void Hide() + { + if (isVisible) { + isVisible = false; + StopBlinkAnimation(); + InvalidateVisual(); + } + } + + void StartBlinkAnimation() + { + TimeSpan blinkTime = Win32.CaretBlinkTime; + blink = true; // the caret should visible initially + // This is important if blinking is disabled (system reports a negative blinkTime) + if (blinkTime.TotalMilliseconds > 0) { + caretBlinkTimer.Interval = blinkTime; + caretBlinkTimer.Start(); + } + } + + void StopBlinkAnimation() + { + caretBlinkTimer.Stop(); + } + + internal Brush CaretBrush; + + protected override void OnRender(DrawingContext drawingContext) + { + base.OnRender(drawingContext); + if (isVisible && blink) { + Brush caretBrush = this.CaretBrush; + if (caretBrush == null) + caretBrush = (Brush)textView.GetValue(TextBlock.ForegroundProperty); + Rect r = new Rect(caretRectangle.X - textView.HorizontalOffset, + caretRectangle.Y - textView.VerticalOffset, + caretRectangle.Width, + caretRectangle.Height); + drawingContext.DrawRectangle(caretBrush, null, PixelSnapHelpers.Round(r, PixelSnapHelpers.GetPixelSize(this))); + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretNavigationCommandHandler.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretNavigationCommandHandler.cs new file mode 100644 index 000000000..d1b812b66 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretNavigationCommandHandler.cs @@ -0,0 +1,375 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media.TextFormatting; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + static class CaretNavigationCommandHandler + { + /// <summary> + /// Creates a new <see cref="TextAreaInputHandler"/> for the text area. + /// </summary> + public static TextAreaInputHandler Create(TextArea textArea) + { + TextAreaInputHandler handler = new TextAreaInputHandler(textArea); + handler.CommandBindings.AddRange(CommandBindings); + handler.InputBindings.AddRange(InputBindings); + return handler; + } + + static readonly List<CommandBinding> CommandBindings = new List<CommandBinding>(); + static readonly List<InputBinding> InputBindings = new List<InputBinding>(); + + static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler) + { + CommandBindings.Add(new CommandBinding(command, handler)); + InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(command, modifiers, key)); + } + + static CaretNavigationCommandHandler() + { + const ModifierKeys None = ModifierKeys.None; + const ModifierKeys Ctrl = ModifierKeys.Control; + const ModifierKeys Shift = ModifierKeys.Shift; + const ModifierKeys Alt = ModifierKeys.Alt; + + AddBinding(EditingCommands.MoveLeftByCharacter, None, Key.Left, OnMoveCaret(CaretMovementType.CharLeft)); + AddBinding(EditingCommands.SelectLeftByCharacter, Shift, Key.Left, OnMoveCaretExtendSelection(CaretMovementType.CharLeft)); + AddBinding(RectangleSelection.BoxSelectLeftByCharacter, Alt | Shift, Key.Left, OnMoveCaretBoxSelection(CaretMovementType.CharLeft)); + AddBinding(EditingCommands.MoveRightByCharacter, None, Key.Right, OnMoveCaret(CaretMovementType.CharRight)); + AddBinding(EditingCommands.SelectRightByCharacter, Shift, Key.Right, OnMoveCaretExtendSelection(CaretMovementType.CharRight)); + AddBinding(RectangleSelection.BoxSelectRightByCharacter, Alt | Shift, Key.Right, OnMoveCaretBoxSelection(CaretMovementType.CharRight)); + + AddBinding(EditingCommands.MoveLeftByWord, Ctrl, Key.Left, OnMoveCaret(CaretMovementType.WordLeft)); + AddBinding(EditingCommands.SelectLeftByWord, Ctrl | Shift, Key.Left, OnMoveCaretExtendSelection(CaretMovementType.WordLeft)); + AddBinding(RectangleSelection.BoxSelectLeftByWord, Ctrl | Alt | Shift, Key.Left, OnMoveCaretBoxSelection(CaretMovementType.WordLeft)); + AddBinding(EditingCommands.MoveRightByWord, Ctrl, Key.Right, OnMoveCaret(CaretMovementType.WordRight)); + AddBinding(EditingCommands.SelectRightByWord, Ctrl | Shift, Key.Right, OnMoveCaretExtendSelection(CaretMovementType.WordRight)); + AddBinding(RectangleSelection.BoxSelectRightByWord, Ctrl | Alt | Shift, Key.Right, OnMoveCaretBoxSelection(CaretMovementType.WordRight)); + + AddBinding(EditingCommands.MoveUpByLine, None, Key.Up, OnMoveCaret(CaretMovementType.LineUp)); + AddBinding(EditingCommands.SelectUpByLine, Shift, Key.Up, OnMoveCaretExtendSelection(CaretMovementType.LineUp)); + AddBinding(RectangleSelection.BoxSelectUpByLine, Alt | Shift, Key.Up, OnMoveCaretBoxSelection(CaretMovementType.LineUp)); + AddBinding(EditingCommands.MoveDownByLine, None, Key.Down, OnMoveCaret(CaretMovementType.LineDown)); + AddBinding(EditingCommands.SelectDownByLine, Shift, Key.Down, OnMoveCaretExtendSelection(CaretMovementType.LineDown)); + AddBinding(RectangleSelection.BoxSelectDownByLine, Alt | Shift, Key.Down, OnMoveCaretBoxSelection(CaretMovementType.LineDown)); + + AddBinding(EditingCommands.MoveDownByPage, None, Key.PageDown, OnMoveCaret(CaretMovementType.PageDown)); + AddBinding(EditingCommands.SelectDownByPage, Shift, Key.PageDown, OnMoveCaretExtendSelection(CaretMovementType.PageDown)); + AddBinding(EditingCommands.MoveUpByPage, None, Key.PageUp, OnMoveCaret(CaretMovementType.PageUp)); + AddBinding(EditingCommands.SelectUpByPage, Shift, Key.PageUp, OnMoveCaretExtendSelection(CaretMovementType.PageUp)); + + AddBinding(EditingCommands.MoveToLineStart, None, Key.Home, OnMoveCaret(CaretMovementType.LineStart)); + AddBinding(EditingCommands.SelectToLineStart, Shift, Key.Home, OnMoveCaretExtendSelection(CaretMovementType.LineStart)); + AddBinding(RectangleSelection.BoxSelectToLineStart, Alt | Shift, Key.Home, OnMoveCaretBoxSelection(CaretMovementType.LineStart)); + AddBinding(EditingCommands.MoveToLineEnd, None, Key.End, OnMoveCaret(CaretMovementType.LineEnd)); + AddBinding(EditingCommands.SelectToLineEnd, Shift, Key.End, OnMoveCaretExtendSelection(CaretMovementType.LineEnd)); + AddBinding(RectangleSelection.BoxSelectToLineEnd, Alt | Shift, Key.End, OnMoveCaretBoxSelection(CaretMovementType.LineEnd)); + + AddBinding(EditingCommands.MoveToDocumentStart, Ctrl, Key.Home, OnMoveCaret(CaretMovementType.DocumentStart)); + AddBinding(EditingCommands.SelectToDocumentStart, Ctrl | Shift, Key.Home, OnMoveCaretExtendSelection(CaretMovementType.DocumentStart)); + AddBinding(EditingCommands.MoveToDocumentEnd, Ctrl, Key.End, OnMoveCaret(CaretMovementType.DocumentEnd)); + AddBinding(EditingCommands.SelectToDocumentEnd, Ctrl | Shift, Key.End, OnMoveCaretExtendSelection(CaretMovementType.DocumentEnd)); + + CommandBindings.Add(new CommandBinding(ApplicationCommands.SelectAll, OnSelectAll)); + + TextAreaDefaultInputHandler.WorkaroundWPFMemoryLeak(InputBindings); + } + + static void OnSelectAll(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + args.Handled = true; + textArea.Caret.Offset = textArea.Document.TextLength; + textArea.Selection = SimpleSelection.Create(textArea, 0, textArea.Document.TextLength); + } + } + + static TextArea GetTextArea(object target) + { + return target as TextArea; + } + + enum CaretMovementType + { + CharLeft, + CharRight, + WordLeft, + WordRight, + LineUp, + LineDown, + PageUp, + PageDown, + LineStart, + LineEnd, + DocumentStart, + DocumentEnd + } + + static ExecutedRoutedEventHandler OnMoveCaret(CaretMovementType direction) + { + return (target, args) => { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + args.Handled = true; + textArea.ClearSelection(); + MoveCaret(textArea, direction); + textArea.Caret.BringCaretToView(); + } + }; + } + + static ExecutedRoutedEventHandler OnMoveCaretExtendSelection(CaretMovementType direction) + { + return (target, args) => { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + args.Handled = true; + TextViewPosition oldPosition = textArea.Caret.Position; + MoveCaret(textArea, direction); + textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldPosition, textArea.Caret.Position); + if (!textArea.Document.IsInUpdate) // if we're inside a larger update (e.g. called by EditingCommandHandler.OnDelete()), avoid calculating the caret rectangle now + textArea.Caret.BringCaretToView(); + } + }; + } + + static ExecutedRoutedEventHandler OnMoveCaretBoxSelection(CaretMovementType direction) + { + return (target, args) => { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + args.Handled = true; + // First, convert the selection into a rectangle selection + // (this is required so that virtual space gets enabled for the caret movement) + if (textArea.Options.EnableRectangularSelection && !(textArea.Selection is RectangleSelection)) { + if (textArea.Selection.IsEmpty) { + textArea.Selection = new RectangleSelection(textArea, textArea.Caret.Position, textArea.Caret.Position); + } else { + // Convert normal selection to rectangle selection + textArea.Selection = new RectangleSelection(textArea, textArea.Selection.StartPosition, textArea.Caret.Position); + } + } + // Now move the caret and extend the selection + TextViewPosition oldPosition = textArea.Caret.Position; + MoveCaret(textArea, direction); + textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldPosition, textArea.Caret.Position); + textArea.Caret.BringCaretToView(); + } + }; + } + + #region Caret movement + static void MoveCaret(TextArea textArea, CaretMovementType direction) + { + DocumentLine caretLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); + VisualLine visualLine = textArea.TextView.GetOrConstructVisualLine(caretLine); + TextViewPosition caretPosition = textArea.Caret.Position; + TextLine textLine = visualLine.GetTextLine(caretPosition.VisualColumn); + switch (direction) { + case CaretMovementType.CharLeft: + MoveCaretLeft(textArea, caretPosition, visualLine, CaretPositioningMode.Normal); + break; + case CaretMovementType.CharRight: + MoveCaretRight(textArea, caretPosition, visualLine, CaretPositioningMode.Normal); + break; + case CaretMovementType.WordLeft: + MoveCaretLeft(textArea, caretPosition, visualLine, CaretPositioningMode.WordStart); + break; + case CaretMovementType.WordRight: + MoveCaretRight(textArea, caretPosition, visualLine, CaretPositioningMode.WordStart); + break; + case CaretMovementType.LineUp: + case CaretMovementType.LineDown: + case CaretMovementType.PageUp: + case CaretMovementType.PageDown: + MoveCaretUpDown(textArea, direction, visualLine, textLine, caretPosition.VisualColumn); + break; + case CaretMovementType.DocumentStart: + SetCaretPosition(textArea, 0, 0); + break; + case CaretMovementType.DocumentEnd: + SetCaretPosition(textArea, -1, textArea.Document.TextLength); + break; + case CaretMovementType.LineStart: + MoveCaretToStartOfLine(textArea, visualLine); + break; + case CaretMovementType.LineEnd: + MoveCaretToEndOfLine(textArea, visualLine); + break; + default: + throw new NotSupportedException(direction.ToString()); + } + } + #endregion + + #region Home/End + static void MoveCaretToStartOfLine(TextArea textArea, VisualLine visualLine) + { + int newVC = visualLine.GetNextCaretPosition(-1, LogicalDirection.Forward, CaretPositioningMode.WordStart, textArea.Selection.EnableVirtualSpace); + if (newVC < 0) + throw ThrowUtil.NoValidCaretPosition(); + // when the caret is already at the start of the text, jump to start before whitespace + if (newVC == textArea.Caret.VisualColumn) + newVC = 0; + int offset = visualLine.FirstDocumentLine.Offset + visualLine.GetRelativeOffset(newVC); + SetCaretPosition(textArea, newVC, offset); + } + + static void MoveCaretToEndOfLine(TextArea textArea, VisualLine visualLine) + { + int newVC = visualLine.VisualLength; + int offset = visualLine.FirstDocumentLine.Offset + visualLine.GetRelativeOffset(newVC); + SetCaretPosition(textArea, newVC, offset); + } + #endregion + + #region By-character / By-word movement + static void MoveCaretRight(TextArea textArea, TextViewPosition caretPosition, VisualLine visualLine, CaretPositioningMode mode) + { + int pos = visualLine.GetNextCaretPosition(caretPosition.VisualColumn, LogicalDirection.Forward, mode, textArea.Selection.EnableVirtualSpace); + if (pos >= 0) { + SetCaretPosition(textArea, pos, visualLine.GetRelativeOffset(pos) + visualLine.FirstDocumentLine.Offset); + } else { + // move to start of next line + DocumentLine nextDocumentLine = visualLine.LastDocumentLine.NextLine; + if (nextDocumentLine != null) { + VisualLine nextLine = textArea.TextView.GetOrConstructVisualLine(nextDocumentLine); + pos = nextLine.GetNextCaretPosition(-1, LogicalDirection.Forward, mode, textArea.Selection.EnableVirtualSpace); + if (pos < 0) + throw ThrowUtil.NoValidCaretPosition(); + SetCaretPosition(textArea, pos, nextLine.GetRelativeOffset(pos) + nextLine.FirstDocumentLine.Offset); + } else { + // at end of document + Debug.Assert(visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength == textArea.Document.TextLength); + SetCaretPosition(textArea, -1, textArea.Document.TextLength); + } + } + } + + static void MoveCaretLeft(TextArea textArea, TextViewPosition caretPosition, VisualLine visualLine, CaretPositioningMode mode) + { + int pos = visualLine.GetNextCaretPosition(caretPosition.VisualColumn, LogicalDirection.Backward, mode, textArea.Selection.EnableVirtualSpace); + if (pos >= 0) { + SetCaretPosition(textArea, pos, visualLine.GetRelativeOffset(pos) + visualLine.FirstDocumentLine.Offset); + } else { + // move to end of previous line + DocumentLine previousDocumentLine = visualLine.FirstDocumentLine.PreviousLine; + if (previousDocumentLine != null) { + VisualLine previousLine = textArea.TextView.GetOrConstructVisualLine(previousDocumentLine); + pos = previousLine.GetNextCaretPosition(previousLine.VisualLength + 1, LogicalDirection.Backward, mode, textArea.Selection.EnableVirtualSpace); + if (pos < 0) + throw ThrowUtil.NoValidCaretPosition(); + SetCaretPosition(textArea, pos, previousLine.GetRelativeOffset(pos) + previousLine.FirstDocumentLine.Offset); + } else { + // at start of document + Debug.Assert(visualLine.FirstDocumentLine.Offset == 0); + SetCaretPosition(textArea, 0, 0); + } + } + } + #endregion + + #region Line+Page up/down + static void MoveCaretUpDown(TextArea textArea, CaretMovementType direction, VisualLine visualLine, TextLine textLine, int caretVisualColumn) + { + // moving up/down happens using the desired visual X position + double xPos = textArea.Caret.DesiredXPos; + if (double.IsNaN(xPos)) + xPos = visualLine.GetTextLineVisualXPosition(textLine, caretVisualColumn); + // now find the TextLine+VisualLine where the caret will end up in + VisualLine targetVisualLine = visualLine; + TextLine targetLine; + int textLineIndex = visualLine.TextLines.IndexOf(textLine); + switch (direction) { + case CaretMovementType.LineUp: + { + // Move up: move to the previous TextLine in the same visual line + // or move to the last TextLine of the previous visual line + int prevLineNumber = visualLine.FirstDocumentLine.LineNumber - 1; + if (textLineIndex > 0) { + targetLine = visualLine.TextLines[textLineIndex - 1]; + } else if (prevLineNumber >= 1) { + DocumentLine prevLine = textArea.Document.GetLineByNumber(prevLineNumber); + targetVisualLine = textArea.TextView.GetOrConstructVisualLine(prevLine); + targetLine = targetVisualLine.TextLines[targetVisualLine.TextLines.Count - 1]; + } else { + targetLine = null; + } + break; + } + case CaretMovementType.LineDown: + { + // Move down: move to the next TextLine in the same visual line + // or move to the first TextLine of the next visual line + int nextLineNumber = visualLine.LastDocumentLine.LineNumber + 1; + if (textLineIndex < visualLine.TextLines.Count - 1) { + targetLine = visualLine.TextLines[textLineIndex + 1]; + } else if (nextLineNumber <= textArea.Document.LineCount) { + DocumentLine nextLine = textArea.Document.GetLineByNumber(nextLineNumber); + targetVisualLine = textArea.TextView.GetOrConstructVisualLine(nextLine); + targetLine = targetVisualLine.TextLines[0]; + } else { + targetLine = null; + } + break; + } + case CaretMovementType.PageUp: + case CaretMovementType.PageDown: + { + // Page up/down: find the target line using its visual position + double yPos = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineMiddle); + if (direction == CaretMovementType.PageUp) + yPos -= textArea.TextView.RenderSize.Height; + else + yPos += textArea.TextView.RenderSize.Height; + DocumentLine newLine = textArea.TextView.GetDocumentLineByVisualTop(yPos); + targetVisualLine = textArea.TextView.GetOrConstructVisualLine(newLine); + targetLine = targetVisualLine.GetTextLineByVisualYPosition(yPos); + break; + } + default: + throw new NotSupportedException(direction.ToString()); + } + if (targetLine != null) { + double yPos = targetVisualLine.GetTextLineVisualYPosition(targetLine, VisualYPosition.LineMiddle); + int newVisualColumn = targetVisualLine.GetVisualColumn(new Point(xPos, yPos), textArea.Selection.EnableVirtualSpace); + SetCaretPosition(textArea, targetVisualLine, targetLine, newVisualColumn, false); + textArea.Caret.DesiredXPos = xPos; + } + } + #endregion + + #region SetCaretPosition + static void SetCaretPosition(TextArea textArea, VisualLine targetVisualLine, TextLine targetLine, + int newVisualColumn, bool allowWrapToNextLine) + { + int targetLineStartCol = targetVisualLine.GetTextLineVisualStartColumn(targetLine); + if (!allowWrapToNextLine && newVisualColumn >= targetLineStartCol + targetLine.Length) { + if (newVisualColumn <= targetVisualLine.VisualLength) + newVisualColumn = targetLineStartCol + targetLine.Length - 1; + } + int newOffset = targetVisualLine.GetRelativeOffset(newVisualColumn) + targetVisualLine.FirstDocumentLine.Offset; + SetCaretPosition(textArea, newVisualColumn, newOffset); + } + + static void SetCaretPosition(TextArea textArea, int newVisualColumn, int newOffset) + { + textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(newOffset), newVisualColumn); + textArea.Caret.DesiredXPos = double.NaN; + } + #endregion + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretWeakEventHandler.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretWeakEventHandler.cs new file mode 100644 index 000000000..ebf5e4947 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/CaretWeakEventHandler.cs @@ -0,0 +1,33 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using Tango.Scripting.Editors.Utils; +using System; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Contains classes for handling weak events on the Caret class. + /// </summary> + public static class CaretWeakEventManager + { + /// <summary> + /// Handles the Caret.PositionChanged event. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")] + public sealed class PositionChanged : WeakEventManagerBase<PositionChanged, Caret> + { + /// <inheritdoc/> + protected override void StartListening(Caret source) + { + source.PositionChanged += DeliverEvent; + } + + /// <inheritdoc/> + protected override void StopListening(Caret source) + { + source.PositionChanged -= DeliverEvent; + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/DottedLineMargin.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/DottedLineMargin.cs new file mode 100644 index 000000000..3de848dda --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/DottedLineMargin.cs @@ -0,0 +1,63 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Margin for use with the text area. + /// A vertical dotted line to separate the line numbers from the text view. + /// </summary> + public static class DottedLineMargin + { + static readonly object tag = new object(); + + /// <summary> + /// Creates a vertical dotted line to separate the line numbers from the text view. + /// </summary> + public static UIElement Create() + { + Line line = new Line { + //X1 = 0, Y1 = 0, X2 = 0, Y2 = 1, + //StrokeDashArray = { 0, 2 }, + //Stretch = Stretch.Fill, + //StrokeThickness = 1, + //StrokeDashCap = PenLineCap.Round, + //Margin = new Thickness(2, 0, 2, 0), + //Tag = tag + }; + + return line; + } + + /// <summary> + /// Creates a vertical dotted line to separate the line numbers from the text view. + /// </summary> + [Obsolete("This method got published accidentally; and will be removed again in a future version. Use the parameterless overload instead.")] + public static UIElement Create(TextEditor editor) + { + Line line = (Line)Create(); + + line.SetBinding( + Line.StrokeProperty, + new Binding("LineNumbersForeground") { Source = editor } + ); + + return line; + } + + /// <summary> + /// Gets whether the specified UIElement is the result of a DottedLineMargin.Create call. + /// </summary> + public static bool IsDottedLineMargin(UIElement element) + { + Line l = element as Line; + return l != null && l.Tag == tag; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/DragDropException.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/DragDropException.cs new file mode 100644 index 000000000..2ba8e41ec --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/DragDropException.cs @@ -0,0 +1,46 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Runtime.Serialization; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Wraps exceptions that occur during drag'n'drop. + /// Exceptions during drag'n'drop might + /// get swallowed by WPF/COM, so AvalonEdit catches them and re-throws them later + /// wrapped in a DragDropException. + /// </summary> + [Serializable()] + public class DragDropException : Exception + { + /// <summary> + /// Creates a new DragDropException. + /// </summary> + public DragDropException() : base() + { + } + + /// <summary> + /// Creates a new DragDropException. + /// </summary> + public DragDropException(string message) : base(message) + { + } + + /// <summary> + /// Creates a new DragDropException. + /// </summary> + public DragDropException(string message, Exception innerException) : base(message, innerException) + { + } + + /// <summary> + /// Deserializes a DragDropException. + /// </summary> + protected DragDropException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs new file mode 100644 index 000000000..685e7ae31 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EditingCommandHandler.cs @@ -0,0 +1,578 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Highlighting; +using Tango.Scripting.Editors.Search; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// We re-use the CommandBinding and InputBinding instances between multiple text areas, + /// so this class is static. + /// </summary> + static class EditingCommandHandler + { + /// <summary> + /// Creates a new <see cref="TextAreaInputHandler"/> for the text area. + /// </summary> + public static TextAreaInputHandler Create(TextArea textArea) + { + TextAreaInputHandler handler = new TextAreaInputHandler(textArea); + handler.CommandBindings.AddRange(CommandBindings); + handler.InputBindings.AddRange(InputBindings); + return handler; + } + + static readonly List<CommandBinding> CommandBindings = new List<CommandBinding>(); + static readonly List<InputBinding> InputBindings = new List<InputBinding>(); + + static void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler) + { + CommandBindings.Add(new CommandBinding(command, handler)); + InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(command, modifiers, key)); + } + + static EditingCommandHandler() + { + CommandBindings.Add(new CommandBinding(ApplicationCommands.Delete, OnDelete(ApplicationCommands.NotACommand), CanDelete)); + AddBinding(EditingCommands.Delete, ModifierKeys.None, Key.Delete, OnDelete(EditingCommands.SelectRightByCharacter)); + AddBinding(EditingCommands.DeleteNextWord, ModifierKeys.Control, Key.Delete, OnDelete(EditingCommands.SelectRightByWord)); + AddBinding(EditingCommands.Backspace, ModifierKeys.None, Key.Back, OnDelete(EditingCommands.SelectLeftByCharacter)); + InputBindings.Add(TextAreaDefaultInputHandler.CreateFrozenKeyBinding(EditingCommands.Backspace, ModifierKeys.Shift, Key.Back)); // make Shift-Backspace do the same as plain backspace + AddBinding(EditingCommands.DeletePreviousWord, ModifierKeys.Control, Key.Back, OnDelete(EditingCommands.SelectLeftByWord)); + AddBinding(EditingCommands.EnterParagraphBreak, ModifierKeys.None, Key.Enter, OnEnter); + AddBinding(EditingCommands.EnterLineBreak, ModifierKeys.Shift, Key.Enter, OnEnter); + AddBinding(EditingCommands.TabForward, ModifierKeys.None, Key.Tab, OnTab); + AddBinding(EditingCommands.TabBackward, ModifierKeys.Shift, Key.Tab, OnShiftTab); + + CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopy, CanCutOrCopy)); + CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCut, CanCutOrCopy)); + CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPaste, CanPaste)); + + CommandBindings.Add(new CommandBinding(AvalonEditCommands.DeleteLine, OnDeleteLine)); + + CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveLeadingWhitespace, OnRemoveLeadingWhitespace)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.RemoveTrailingWhitespace, OnRemoveTrailingWhitespace)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToUppercase, OnConvertToUpperCase)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToLowercase, OnConvertToLowerCase)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertToTitleCase, OnConvertToTitleCase)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.InvertCase, OnInvertCase)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertTabsToSpaces, OnConvertTabsToSpaces)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertSpacesToTabs, OnConvertSpacesToTabs)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertLeadingTabsToSpaces, OnConvertLeadingTabsToSpaces)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.ConvertLeadingSpacesToTabs, OnConvertLeadingSpacesToTabs)); + CommandBindings.Add(new CommandBinding(AvalonEditCommands.IndentSelection, OnIndentSelection)); + + TextAreaDefaultInputHandler.WorkaroundWPFMemoryLeak(InputBindings); + } + + static TextArea GetTextArea(object target) + { + return target as TextArea; + } + + #region Text Transformation Helpers + enum DefaultSegmentType + { + None, + WholeDocument, + CurrentLine + } + + /// <summary> + /// Calls transformLine on all lines in the selected range. + /// transformLine needs to handle read-only segments! + /// </summary> + static void TransformSelectedLines(Action<TextArea, DocumentLine> transformLine, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + using (textArea.Document.RunUpdate()) { + DocumentLine start, end; + if (textArea.Selection.IsEmpty) { + if (defaultSegmentType == DefaultSegmentType.CurrentLine) { + start = end = textArea.Document.GetLineByNumber(textArea.Caret.Line); + } else if (defaultSegmentType == DefaultSegmentType.WholeDocument) { + start = textArea.Document.Lines.First(); + end = textArea.Document.Lines.Last(); + } else { + start = end = null; + } + } else { + ISegment segment = textArea.Selection.SurroundingSegment; + start = textArea.Document.GetLineByOffset(segment.Offset); + end = textArea.Document.GetLineByOffset(segment.EndOffset); + // don't include the last line if no characters on it are selected + if (start != end && end.Offset == segment.EndOffset) + end = end.PreviousLine; + } + if (start != null) { + transformLine(textArea, start); + while (start != end) { + start = start.NextLine; + transformLine(textArea, start); + } + } + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } + + /// <summary> + /// Calls transformLine on all writable segment in the selected range. + /// </summary> + static void TransformSelectedSegments(Action<TextArea, ISegment> transformSegment, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + using (textArea.Document.RunUpdate()) { + IEnumerable<ISegment> segments; + if (textArea.Selection.IsEmpty) { + if (defaultSegmentType == DefaultSegmentType.CurrentLine) { + segments = new ISegment[] { textArea.Document.GetLineByNumber(textArea.Caret.Line) }; + } else if (defaultSegmentType == DefaultSegmentType.WholeDocument) { + segments = textArea.Document.Lines.Cast<ISegment>(); + } else { + segments = null; + } + } else { + segments = textArea.Selection.Segments.Cast<ISegment>(); + } + if (segments != null) { + foreach (ISegment segment in segments.Reverse()) { + foreach (ISegment writableSegment in textArea.GetDeletableSegments(segment).Reverse()) { + transformSegment(textArea, writableSegment); + } + } + } + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } + #endregion + + #region EnterLineBreak + static void OnEnter(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.IsKeyboardFocused) { + textArea.PerformTextInput("\n"); + args.Handled = true; + } + } + #endregion + + #region Tab + static void OnTab(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + using (textArea.Document.RunUpdate()) { + if (textArea.Selection.IsMultiline) { + var segment = textArea.Selection.SurroundingSegment; + DocumentLine start = textArea.Document.GetLineByOffset(segment.Offset); + DocumentLine end = textArea.Document.GetLineByOffset(segment.EndOffset); + // don't include the last line if no characters on it are selected + if (start != end && end.Offset == segment.EndOffset) + end = end.PreviousLine; + DocumentLine current = start; + while (true) { + int offset = current.Offset; + if (textArea.ReadOnlySectionProvider.CanInsert(offset)) + textArea.Document.Replace(offset, 0, textArea.Options.IndentationString, OffsetChangeMappingType.KeepAnchorBeforeInsertion); + if (current == end) + break; + current = current.NextLine; + } + } else { + string indentationString = textArea.Options.GetIndentationString(textArea.Caret.Column); + textArea.ReplaceSelectionWithText(indentationString); + } + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } + + static void OnShiftTab(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedLines( + delegate (TextArea textArea, DocumentLine line) { + int offset = line.Offset; + ISegment s = TextUtilities.GetSingleIndentationSegment(textArea.Document, offset, textArea.Options.IndentationSize); + if (s.Length > 0) { + s = textArea.GetDeletableSegments(s).FirstOrDefault(); + if (s != null && s.Length > 0) { + textArea.Document.Remove(s.Offset, s.Length); + } + } + }, target, args, DefaultSegmentType.CurrentLine); + } + #endregion + + #region Delete + static ExecutedRoutedEventHandler OnDelete(RoutedUICommand selectingCommand) + { + return (target, args) => { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + // call BeginUpdate before running the 'selectingCommand' + // so that undoing the delete does not select the deleted character + using (textArea.Document.RunUpdate()) { + if (textArea.Selection.IsEmpty) { + TextViewPosition oldCaretPosition = textArea.Caret.Position; + if (textArea.Caret.IsInVirtualSpace && selectingCommand == EditingCommands.SelectRightByCharacter) + EditingCommands.SelectRightByWord.Execute(args.Parameter, textArea); + else + selectingCommand.Execute(args.Parameter, textArea); + bool hasSomethingDeletable = false; + foreach (ISegment s in textArea.Selection.Segments) { + if (textArea.GetDeletableSegments(s).Length > 0) { + hasSomethingDeletable = true; + break; + } + } + if (!hasSomethingDeletable) { + // If nothing in the selection is deletable; then reset caret+selection + // to the previous value. This prevents the caret from moving through read-only sections. + textArea.Caret.Position = oldCaretPosition; + textArea.ClearSelection(); + } + } + textArea.RemoveSelectedText(); + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + }; + } + + static void CanDelete(object target, CanExecuteRoutedEventArgs args) + { + // HasSomethingSelected for delete command + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + args.CanExecute = !textArea.Selection.IsEmpty; + args.Handled = true; + } + } + #endregion + + #region Clipboard commands + static void CanCutOrCopy(object target, CanExecuteRoutedEventArgs args) + { + // HasSomethingSelected for copy and cut commands + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + args.CanExecute = textArea.Options.CutCopyWholeLine || !textArea.Selection.IsEmpty; + args.Handled = true; + } + } + + static void OnCopy(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + if (textArea.Selection.IsEmpty && textArea.Options.CutCopyWholeLine) { + DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); + CopyWholeLine(textArea, currentLine); + } else { + CopySelectedText(textArea); + } + args.Handled = true; + } + } + + static void OnCut(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + if (textArea.Selection.IsEmpty && textArea.Options.CutCopyWholeLine) { + DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); + CopyWholeLine(textArea, currentLine); + ISegment[] segmentsToDelete = textArea.GetDeletableSegments(new SimpleSegment(currentLine.Offset, currentLine.TotalLength)); + for (int i = segmentsToDelete.Length - 1; i >= 0; i--) { + textArea.Document.Remove(segmentsToDelete[i]); + } + } else { + CopySelectedText(textArea); + textArea.RemoveSelectedText(); + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } + + static void CopySelectedText(TextArea textArea) + { + var data = textArea.Selection.CreateDataObject(textArea); + + try { + Clipboard.SetDataObject(data, true); + } catch (ExternalException) { + // Apparently this exception sometimes happens randomly. + // The MS controls just ignore it, so we'll do the same. + return; + } + + string text = textArea.Selection.GetText(); + text = TextUtilities.NormalizeNewLines(text, Environment.NewLine); + textArea.OnTextCopied(new TextEventArgs(text)); + } + + const string LineSelectedType = "MSDEVLineSelect"; // This is the type VS 2003 and 2005 use for flagging a whole line copy + + static void CopyWholeLine(TextArea textArea, DocumentLine line) + { + ISegment wholeLine = new SimpleSegment(line.Offset, line.TotalLength); + string text = textArea.Document.GetText(wholeLine); + // Ensure we use the appropriate newline sequence for the OS + text = TextUtilities.NormalizeNewLines(text, Environment.NewLine); + DataObject data = new DataObject(text); + + // Also copy text in HTML format to clipboard - good for pasting text into Word + // or to the SharpDevelop forums. + IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; + HtmlClipboard.SetHtml(data, HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, wholeLine, new HtmlOptions(textArea.Options))); + + MemoryStream lineSelected = new MemoryStream(1); + lineSelected.WriteByte(1); + data.SetData(LineSelectedType, lineSelected, false); + + try { + Clipboard.SetDataObject(data, true); + } catch (ExternalException) { + // Apparently this exception sometimes happens randomly. + // The MS controls just ignore it, so we'll do the same. + return; + } + textArea.OnTextCopied(new TextEventArgs(text)); + } + + static void CanPaste(object target, CanExecuteRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + args.CanExecute = textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset) + && Clipboard.ContainsText(); + // WPF Clipboard.ContainsText() is safe to call without catching ExternalExceptions + // because it doesn't try to lock the clipboard - it just peeks inside with IsClipboardFormatAvailable(). + args.Handled = true; + } + } + + static void OnPaste(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + IDataObject dataObject; + try { + dataObject = Clipboard.GetDataObject(); + } catch (ExternalException) { + return; + } + if (dataObject == null) + return; + Debug.WriteLine( dataObject.GetData(DataFormats.Html) as string ); + + // convert text back to correct newlines for this document + string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line); + string text; + try { + text = (string)dataObject.GetData(DataFormats.UnicodeText); + text = TextUtilities.NormalizeNewLines(text, newLine); + } catch (OutOfMemoryException) { + return; + } + + if (!string.IsNullOrEmpty(text)) { + bool fullLine = textArea.Options.CutCopyWholeLine && dataObject.GetDataPresent(LineSelectedType); + bool rectangular = dataObject.GetDataPresent(RectangleSelection.RectangularSelectionDataType); + if (fullLine) { + DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); + if (textArea.ReadOnlySectionProvider.CanInsert(currentLine.Offset)) { + textArea.Document.Insert(currentLine.Offset, text); + } + } else if (rectangular && textArea.Selection.IsEmpty && !(textArea.Selection is RectangleSelection)) { + if (!RectangleSelection.PerformRectangularPaste(textArea, textArea.Caret.Position, text, false)) + textArea.ReplaceSelectionWithText(text); + } else { + textArea.ReplaceSelectionWithText(text); + } + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } + #endregion + + #region DeleteLine + static void OnDeleteLine(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line); + textArea.Selection = Selection.Create(textArea, currentLine.Offset, currentLine.Offset + currentLine.TotalLength); + textArea.RemoveSelectedText(); + args.Handled = true; + } + } + #endregion + + #region Remove..Whitespace / Convert Tabs-Spaces + static void OnRemoveLeadingWhitespace(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedLines( + delegate (TextArea textArea, DocumentLine line) { + textArea.Document.Remove(TextUtilities.GetLeadingWhitespace(textArea.Document, line)); + }, target, args, DefaultSegmentType.WholeDocument); + } + + static void OnRemoveTrailingWhitespace(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedLines( + delegate (TextArea textArea, DocumentLine line) { + textArea.Document.Remove(TextUtilities.GetTrailingWhitespace(textArea.Document, line)); + }, target, args, DefaultSegmentType.WholeDocument); + } + + static void OnConvertTabsToSpaces(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedSegments(ConvertTabsToSpaces, target, args, DefaultSegmentType.WholeDocument); + } + + static void OnConvertLeadingTabsToSpaces(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedLines( + delegate (TextArea textArea, DocumentLine line) { + ConvertTabsToSpaces(textArea, TextUtilities.GetLeadingWhitespace(textArea.Document, line)); + }, target, args, DefaultSegmentType.WholeDocument); + } + + static void ConvertTabsToSpaces(TextArea textArea, ISegment segment) + { + TextDocument document = textArea.Document; + int endOffset = segment.EndOffset; + string indentationString = new string(' ', textArea.Options.IndentationSize); + for (int offset = segment.Offset; offset < endOffset; offset++) { + if (document.GetCharAt(offset) == '\t') { + document.Replace(offset, 1, indentationString, OffsetChangeMappingType.CharacterReplace); + endOffset += indentationString.Length - 1; + } + } + } + + static void OnConvertSpacesToTabs(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedSegments(ConvertSpacesToTabs, target, args, DefaultSegmentType.WholeDocument); + } + + static void OnConvertLeadingSpacesToTabs(object target, ExecutedRoutedEventArgs args) + { + TransformSelectedLines( + delegate (TextArea textArea, DocumentLine line) { + ConvertSpacesToTabs(textArea, TextUtilities.GetLeadingWhitespace(textArea.Document, line)); + }, target, args, DefaultSegmentType.WholeDocument); + } + + static void ConvertSpacesToTabs(TextArea textArea, ISegment segment) + { + TextDocument document = textArea.Document; + int endOffset = segment.EndOffset; + int indentationSize = textArea.Options.IndentationSize; + int spacesCount = 0; + for (int offset = segment.Offset; offset < endOffset; offset++) { + if (document.GetCharAt(offset) == ' ') { + spacesCount++; + if (spacesCount == indentationSize) { + document.Replace(offset - (indentationSize - 1), indentationSize, "\t", OffsetChangeMappingType.CharacterReplace); + spacesCount = 0; + offset -= indentationSize - 1; + endOffset -= indentationSize - 1; + } + } else { + spacesCount = 0; + } + } + } + #endregion + + #region Convert...Case + static void ConvertCase(Func<string, string> transformText, object target, ExecutedRoutedEventArgs args) + { + TransformSelectedSegments( + delegate (TextArea textArea, ISegment segment) { + string oldText = textArea.Document.GetText(segment); + string newText = transformText(oldText); + textArea.Document.Replace(segment.Offset, segment.Length, newText, OffsetChangeMappingType.CharacterReplace); + }, target, args, DefaultSegmentType.WholeDocument); + } + + static void OnConvertToUpperCase(object target, ExecutedRoutedEventArgs args) + { + ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToUpper, target, args); + } + + static void OnConvertToLowerCase(object target, ExecutedRoutedEventArgs args) + { + ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToLower, target, args); + } + + static void OnConvertToTitleCase(object target, ExecutedRoutedEventArgs args) + { + ConvertCase(CultureInfo.CurrentCulture.TextInfo.ToTitleCase, target, args); + } + + static void OnInvertCase(object target, ExecutedRoutedEventArgs args) + { + ConvertCase(InvertCase, target, args); + } + + static string InvertCase(string text) + { + CultureInfo culture = CultureInfo.CurrentCulture; + char[] buffer = text.ToCharArray(); + for (int i = 0; i < buffer.Length; ++i) { + char c = buffer[i]; + buffer[i] = char.IsUpper(c) ? char.ToLower(c, culture) : char.ToUpper(c, culture); + } + return new string(buffer); + } + #endregion + + static void OnIndentSelection(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + using (textArea.Document.RunUpdate()) { + int start, end; + if (textArea.Selection.IsEmpty) { + start = 1; + end = textArea.Document.LineCount; + } else { + start = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.Offset).LineNumber; + end = textArea.Document.GetLineByOffset(textArea.Selection.SurroundingSegment.EndOffset).LineNumber; + } + textArea.IndentationStrategy.IndentLines(textArea.Document, start, end); + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EmptySelection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EmptySelection.cs new file mode 100644 index 000000000..6cca63ecb --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/EmptySelection.cs @@ -0,0 +1,85 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + sealed class EmptySelection : Selection + { + public EmptySelection(TextArea textArea) : base(textArea) + { + } + + public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e) + { + return this; + } + + public override TextViewPosition StartPosition { + get { return new TextViewPosition(TextLocation.Empty); } + } + + public override TextViewPosition EndPosition { + get { return new TextViewPosition(TextLocation.Empty); } + } + + public override ISegment SurroundingSegment { + get { return null; } + } + + public override Selection SetEndpoint(TextViewPosition endPosition) + { + throw new NotSupportedException(); + } + + public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition) + { + var document = textArea.Document; + if (document == null) + throw ThrowUtil.NoDocumentAssigned(); + return Create(textArea, startPosition, endPosition); + } + + public override IEnumerable<SelectionSegment> Segments { + get { return Empty<SelectionSegment>.Array; } + } + + public override string GetText() + { + return string.Empty; + } + + public override void ReplaceSelectionWithText(string newText) + { + if (newText == null) + throw new ArgumentNullException("newText"); + newText = AddSpacesIfRequired(newText, textArea.Caret.Position, textArea.Caret.Position); + if (newText.Length > 0) { + if (textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset)) { + textArea.Document.Insert(textArea.Caret.Offset, newText); + } + } + textArea.Caret.VisualColumn = -1; + } + + public override int Length { + get { return 0; } + } + + // Use reference equality because there's only one EmptySelection per text area. + public override int GetHashCode() + { + return RuntimeHelpers.GetHashCode(this); + } + + public override bool Equals(object obj) + { + return this == obj; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/IReadOnlySectionProvider.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/IReadOnlySectionProvider.cs new file mode 100644 index 000000000..fc775baf3 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/IReadOnlySectionProvider.cs @@ -0,0 +1,32 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Determines whether the document can be modified. + /// </summary> + public interface IReadOnlySectionProvider + { + /// <summary> + /// Gets whether insertion is possible at the specified offset. + /// </summary> + bool CanInsert(int offset); + + /// <summary> + /// Gets the deletable segments inside the given segment. + /// </summary> + /// <remarks> + /// All segments in the result must be within the given segment, and they must be returned in order + /// (e.g. if two segments are returned, EndOffset of first segment must be less than StartOffset of second segment). + /// + /// For replacements, the last segment being returned will be replaced with the new text. If an empty list is returned, + /// no replacement will be done. + /// </remarks> + IEnumerable<ISegment> GetDeletableSegments(ISegment segment); + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/ImeNativeWrapper.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/ImeNativeWrapper.cs new file mode 100644 index 000000000..bc2c6f5e4 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/ImeNativeWrapper.cs @@ -0,0 +1,206 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; + +using Tango.Scripting.Editors; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; +using Draw = System.Drawing; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Native API required for IME support. + /// </summary> + static class ImeNativeWrapper + { + [StructLayout(LayoutKind.Sequential)] + struct CompositionForm + { + public int dwStyle; + public POINT ptCurrentPos; + public RECT rcArea; + } + + [StructLayout(LayoutKind.Sequential)] + struct POINT + { + public int x; + public int y; + } + + [StructLayout(LayoutKind.Sequential)] + struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] + struct LOGFONT + { + public int lfHeight; + public int lfWidth; + public int lfEscapement; + public int lfOrientation; + public int lfWeight; + public byte lfItalic; + public byte lfUnderline; + public byte lfStrikeOut; + public byte lfCharSet; + public byte lfOutPrecision; + public byte lfClipPrecision; + public byte lfQuality; + public byte lfPitchAndFamily; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string lfFaceName; + } + + const int CPS_CANCEL = 0x4; + const int NI_COMPOSITIONSTR = 0x15; + const int GCS_COMPSTR = 0x0008; + + public const int WM_IME_COMPOSITION = 0x10F; + public const int WM_IME_SETCONTEXT = 0x281; + public const int WM_INPUTLANGCHANGE = 0x51; + + [DllImport("imm32.dll")] + public static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC); + [DllImport("imm32.dll")] + internal static extern IntPtr ImmGetContext(IntPtr hWnd); + [DllImport("imm32.dll")] + internal static extern IntPtr ImmGetDefaultIMEWnd(IntPtr hWnd); + [DllImport("imm32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC); + [DllImport("imm32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool ImmNotifyIME(IntPtr hIMC, int dwAction, int dwIndex, int dwValue = 0); + [DllImport("imm32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool ImmSetCompositionWindow(IntPtr hIMC, ref CompositionForm form); + [DllImport("imm32.dll", CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool ImmSetCompositionFont(IntPtr hIMC, ref LOGFONT font); + [DllImport("imm32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool ImmGetCompositionFont(IntPtr hIMC, out LOGFONT font); + + [DllImport("msctf.dll")] + static extern int TF_CreateThreadMgr(out ITfThreadMgr threadMgr); + + [ThreadStatic] static bool textFrameworkThreadMgrInitialized; + [ThreadStatic] static ITfThreadMgr textFrameworkThreadMgr; + + public static ITfThreadMgr GetTextFrameworkThreadManager() + { + if (!textFrameworkThreadMgrInitialized) { + textFrameworkThreadMgrInitialized = true; + TF_CreateThreadMgr(out textFrameworkThreadMgr); + } + return textFrameworkThreadMgr; + } + + public static bool NotifyIme(IntPtr hIMC) + { + return ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL); + } + + public static bool SetCompositionWindow(HwndSource source, IntPtr hIMC, TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + Rect textViewBounds = textArea.TextView.GetBounds(source); + Rect characterBounds = textArea.TextView.GetCharacterBounds(textArea.Caret.Position, source); + CompositionForm form = new CompositionForm(); + form.dwStyle = 0x0020; + form.ptCurrentPos.x = (int)Math.Max(characterBounds.Left, textViewBounds.Left); + form.ptCurrentPos.y = (int)Math.Max(characterBounds.Top, textViewBounds.Top); + form.rcArea.left = (int)textViewBounds.Left; + form.rcArea.top = (int)textViewBounds.Top; + form.rcArea.right = (int)textViewBounds.Right; + form.rcArea.bottom = (int)textViewBounds.Bottom; + return ImmSetCompositionWindow(hIMC, ref form); + } + + public static bool SetCompositionFont(HwndSource source, IntPtr hIMC, TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + LOGFONT lf = new LOGFONT(); + Rect characterBounds = textArea.TextView.GetCharacterBounds(textArea.Caret.Position, source); + lf.lfFaceName = textArea.FontFamily.Source; + lf.lfHeight = (int)characterBounds.Height; + return ImmSetCompositionFont(hIMC, ref lf); + } + + static Rect GetBounds(this TextView textView, HwndSource source) + { + // this may happen during layout changes in AvalonDock, so we just return an empty rectangle + // in those cases. It should be refreshed immediately. + if (source.RootVisual == null || !source.RootVisual.IsAncestorOf(textView)) + return EMPTY_RECT; + Rect displayRect = new Rect(0, 0, textView.ActualWidth, textView.ActualHeight); + return textView + .TransformToAncestor(source.RootVisual).TransformBounds(displayRect) // rect on root visual + .TransformToDevice(source.RootVisual); // rect on HWND + } + + static readonly Rect EMPTY_RECT = new Rect(0, 0, 0, 0); + + static Rect GetCharacterBounds(this TextView textView, TextViewPosition pos, HwndSource source) + { + VisualLine vl = textView.GetVisualLine(pos.Line); + if (vl == null) + return EMPTY_RECT; + // this may happen during layout changes in AvalonDock, so we just return an empty rectangle + // in those cases. It should be refreshed immediately. + if (source.RootVisual == null || !source.RootVisual.IsAncestorOf(textView)) + return EMPTY_RECT; + TextLine line = vl.GetTextLine(pos.VisualColumn); + Rect displayRect; + // calculate the display rect for the current character + if (pos.VisualColumn < vl.VisualLengthWithEndOfLineMarker) { + displayRect = line.GetTextBounds(pos.VisualColumn, 1).First().Rectangle; + displayRect.Offset(0, vl.GetTextLineVisualYPosition(line, VisualYPosition.LineTop)); + } else { + // if we are in virtual space, we just use one wide-space as character width + displayRect = new Rect(vl.GetVisualPosition(pos.VisualColumn, VisualYPosition.TextTop), + new Size(textView.WideSpaceWidth, textView.DefaultLineHeight)); + } + // adjust to current scrolling + displayRect.Offset(-textView.ScrollOffset); + return textView + .TransformToAncestor(source.RootVisual).TransformBounds(displayRect) // rect on root visual + .TransformToDevice(source.RootVisual); // rect on HWND + } + } + + [ComImport, Guid("aa80e801-2021-11d2-93e0-0060b067b86e"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface ITfThreadMgr + { + void Activate(out int clientId); + void Deactivate(); + void CreateDocumentMgr(out IntPtr docMgr); + void EnumDocumentMgrs(out IntPtr enumDocMgrs); + void GetFocus(out IntPtr docMgr); + void SetFocus(IntPtr docMgr); + void AssociateFocus(IntPtr hwnd, IntPtr newDocMgr, out IntPtr prevDocMgr); + void IsThreadFocus([MarshalAs(UnmanagedType.Bool)] out bool isFocus); + void GetFunctionProvider(ref Guid classId, out IntPtr funcProvider); + void EnumFunctionProviders(out IntPtr enumProviders); + void GetGlobalCompartment(out IntPtr compartmentMgr); + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/ImeSupport.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/ImeSupport.cs new file mode 100644 index 000000000..ad4c22e10 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/ImeSupport.cs @@ -0,0 +1,150 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; +using Tango.Scripting.Editors; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; + +namespace Tango.Scripting.Editors.Editing +{ + class ImeSupport + { + readonly TextArea textArea; + IntPtr currentContext; + IntPtr previousContext; + IntPtr defaultImeWnd; + HwndSource hwndSource; + EventHandler requerySuggestedHandler; // we need to keep the event handler instance alive because CommandManager.RequerySuggested uses weak references + bool isReadOnly; + + public ImeSupport(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.textArea = textArea; + InputMethod.SetIsInputMethodSuspended(this.textArea, textArea.Options.EnableImeSupport); + // We listen to CommandManager.RequerySuggested for both caret offset changes and changes to the set of read-only sections. + // This is because there's no dedicated event for read-only section changes; but RequerySuggested needs to be raised anyways + // to invalidate the Paste command. + requerySuggestedHandler = OnRequerySuggested; + CommandManager.RequerySuggested += requerySuggestedHandler; + textArea.OptionChanged += TextAreaOptionChanged; + } + + void OnRequerySuggested(object sender, EventArgs e) + { + UpdateImeEnabled(); + } + + void TextAreaOptionChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "EnableImeSupport") { + InputMethod.SetIsInputMethodSuspended(this.textArea, textArea.Options.EnableImeSupport); + UpdateImeEnabled(); + } + } + + public void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + UpdateImeEnabled(); + } + + public void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + if (e.OldFocus == textArea && currentContext != IntPtr.Zero) + ImeNativeWrapper.NotifyIme(currentContext); + ClearContext(); + } + + void UpdateImeEnabled() + { + if (textArea.Options.EnableImeSupport && textArea.IsKeyboardFocused) { + bool newReadOnly = !textArea.ReadOnlySectionProvider.CanInsert(textArea.Caret.Offset); + if (hwndSource == null || isReadOnly != newReadOnly) { + ClearContext(); // clear existing context (on read-only change) + isReadOnly = newReadOnly; + CreateContext(); + } + } else { + ClearContext(); + } + } + + void ClearContext() + { + if (hwndSource != null) { + ImeNativeWrapper.ImmAssociateContext(hwndSource.Handle, previousContext); + ImeNativeWrapper.ImmReleaseContext(defaultImeWnd, currentContext); + currentContext = IntPtr.Zero; + defaultImeWnd = IntPtr.Zero; + hwndSource.RemoveHook(WndProc); + hwndSource = null; + } + } + + void CreateContext() + { + hwndSource = (HwndSource)PresentationSource.FromVisual(this.textArea); + if (hwndSource != null) { + if (isReadOnly) { + defaultImeWnd = IntPtr.Zero; + currentContext = IntPtr.Zero; + } else { + defaultImeWnd = ImeNativeWrapper.ImmGetDefaultIMEWnd(IntPtr.Zero); + currentContext = ImeNativeWrapper.ImmGetContext(defaultImeWnd); + } + previousContext = ImeNativeWrapper.ImmAssociateContext(hwndSource.Handle, currentContext); + hwndSource.AddHook(WndProc); + // UpdateCompositionWindow() will be called by the caret becoming visible + + var threadMgr = ImeNativeWrapper.GetTextFrameworkThreadManager(); + if (threadMgr != null) { + // Even though the docu says passing null is invalid, this seems to help + // activating the IME on the default input context that is shared with WPF + threadMgr.SetFocus(IntPtr.Zero); + } + } + } + + IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + switch (msg) { + case ImeNativeWrapper.WM_INPUTLANGCHANGE: + // Don't mark the message as handled; other windows + // might want to handle it as well. + + // If we have a context, recreate it + if (hwndSource != null) { + ClearContext(); + CreateContext(); + } + break; + case ImeNativeWrapper.WM_IME_COMPOSITION: + UpdateCompositionWindow(); + break; + } + return IntPtr.Zero; + } + + public void UpdateCompositionWindow() + { + if (currentContext != IntPtr.Zero) { + ImeNativeWrapper.SetCompositionFont(hwndSource, currentContext, textArea); + ImeNativeWrapper.SetCompositionWindow(hwndSource, currentContext, textArea); + } + } + } +}
\ No newline at end of file diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/LineNumberMargin.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/LineNumberMargin.cs new file mode 100644 index 000000000..3da96b08c --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/LineNumberMargin.cs @@ -0,0 +1,243 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Margin showing line numbers. + /// </summary> + public class LineNumberMargin : AbstractMargin, IWeakEventListener + { + public Brush Foreground + { + get { return (Brush)GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + public static readonly DependencyProperty ForegroundProperty = + DependencyProperty.Register("Foreground", typeof(Brush), typeof(LineNumberMargin), new PropertyMetadata(Brushes.Gray)); + + + + static LineNumberMargin() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(LineNumberMargin), + new FrameworkPropertyMetadata(typeof(LineNumberMargin))); + } + + TextArea textArea; + + Typeface typeface; + double emSize; + + /// <inheritdoc/> + protected override Size MeasureOverride(Size availableSize) + { + typeface = this.CreateTypeface(); + emSize = (double)GetValue(TextBlock.FontSizeProperty); + + FormattedText text = TextFormatterFactory.CreateFormattedText( + this, + new string('9', maxLineNumberLength), + typeface, + emSize, + Foreground + ); + return new Size(text.Width, 0); + } + + /// <inheritdoc/> + protected override void OnRender(DrawingContext drawingContext) + { + TextView textView = this.TextView; + Size renderSize = this.RenderSize; + if (textView != null && textView.VisualLinesValid) { + var foreground = Foreground; + foreach (VisualLine line in textView.VisualLines) { + int lineNumber = line.FirstDocumentLine.LineNumber; + FormattedText text = TextFormatterFactory.CreateFormattedText( + this, + lineNumber.ToString(CultureInfo.CurrentCulture), + typeface, emSize, foreground + ); + double y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop); + drawingContext.DrawText(text, new Point((renderSize.Width / 2) - text.Width + 5, y - textView.VerticalOffset)); + } + } + } + + /// <inheritdoc/> + protected override void OnTextViewChanged(TextView oldTextView, TextView newTextView) + { + if (oldTextView != null) { + oldTextView.VisualLinesChanged -= TextViewVisualLinesChanged; + } + base.OnTextViewChanged(oldTextView, newTextView); + if (newTextView != null) { + newTextView.VisualLinesChanged += TextViewVisualLinesChanged; + + // find the text area belonging to the new text view + textArea = newTextView.Services.GetService(typeof(TextArea)) as TextArea; + } else { + textArea = null; + } + InvalidateVisual(); + } + + /// <inheritdoc/> + protected override void OnDocumentChanged(TextDocument oldDocument, TextDocument newDocument) + { + if (oldDocument != null) { + PropertyChangedEventManager.RemoveListener(oldDocument, this, "LineCount"); + } + base.OnDocumentChanged(oldDocument, newDocument); + if (newDocument != null) { + PropertyChangedEventManager.AddListener(newDocument, this, "LineCount"); + } + OnDocumentLineCountChanged(); + } + + /// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/> + protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + if (managerType == typeof(PropertyChangedEventManager)) { + OnDocumentLineCountChanged(); + return true; + } + return false; + } + + bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + return ReceiveWeakEvent(managerType, sender, e); + } + + int maxLineNumberLength = 1; + + void OnDocumentLineCountChanged() + { + int documentLineCount = Document != null ? Document.LineCount : 1; + int newLength = documentLineCount.ToString(CultureInfo.CurrentCulture).Length; + + // The margin looks too small when there is only one digit, so always reserve space for + // at least two digits + if (newLength < 2) + newLength = 2; + + if (newLength != maxLineNumberLength) { + maxLineNumberLength = newLength; + InvalidateMeasure(); + } + } + + void TextViewVisualLinesChanged(object sender, EventArgs e) + { + InvalidateVisual(); + } + + AnchorSegment selectionStart; + bool selecting; + + /// <inheritdoc/> + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) + { + base.OnMouseLeftButtonDown(e); + if (!e.Handled && TextView != null && textArea != null) { + e.Handled = true; + textArea.Focus(); + + SimpleSegment currentSeg = GetTextLineSegment(e); + if (currentSeg == SimpleSegment.Invalid) + return; + textArea.Caret.Offset = currentSeg.Offset + currentSeg.Length; + if (CaptureMouse()) { + selecting = true; + selectionStart = new AnchorSegment(Document, currentSeg.Offset, currentSeg.Length); + if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) { + SimpleSelection simpleSelection = textArea.Selection as SimpleSelection; + if (simpleSelection != null) + selectionStart = new AnchorSegment(Document, simpleSelection.SurroundingSegment); + } + textArea.Selection = Selection.Create(textArea, selectionStart); + if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) { + ExtendSelection(currentSeg); + } + } + } + } + + SimpleSegment GetTextLineSegment(MouseEventArgs e) + { + Point pos = e.GetPosition(TextView); + pos.X = 0; + pos.Y += TextView.VerticalOffset; + VisualLine vl = TextView.GetVisualLineFromVisualTop(pos.Y); + if (vl == null) + return SimpleSegment.Invalid; + TextLine tl = vl.GetTextLineByVisualYPosition(pos.Y); + int visualStartColumn = vl.GetTextLineVisualStartColumn(tl); + int visualEndColumn = visualStartColumn + tl.Length; + int relStart = vl.FirstDocumentLine.Offset; + int startOffset = vl.GetRelativeOffset(visualStartColumn) + relStart; + int endOffset = vl.GetRelativeOffset(visualEndColumn) + relStart; + if (endOffset == vl.LastDocumentLine.Offset + vl.LastDocumentLine.Length) + endOffset += vl.LastDocumentLine.DelimiterLength; + return new SimpleSegment(startOffset, endOffset - startOffset); + } + + void ExtendSelection(SimpleSegment currentSeg) + { + if (currentSeg.Offset < selectionStart.Offset) { + textArea.Caret.Offset = currentSeg.Offset; + textArea.Selection = Selection.Create(textArea, currentSeg.Offset, selectionStart.Offset + selectionStart.Length); + } else { + textArea.Caret.Offset = currentSeg.Offset + currentSeg.Length; + textArea.Selection = Selection.Create(textArea, selectionStart.Offset, currentSeg.Offset + currentSeg.Length); + } + } + + /// <inheritdoc/> + protected override void OnMouseMove(MouseEventArgs e) + { + if (selecting && textArea != null && TextView != null) { + e.Handled = true; + SimpleSegment currentSeg = GetTextLineSegment(e); + if (currentSeg == SimpleSegment.Invalid) + return; + ExtendSelection(currentSeg); + } + base.OnMouseMove(e); + } + + /// <inheritdoc/> + protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) + { + if (selecting) { + selecting = false; + selectionStart = null; + ReleaseMouseCapture(); + e.Handled = true; + } + base.OnMouseLeftButtonUp(e); + } + + /// <inheritdoc/> + protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) + { + // accept clicks even when clicking on the background + return new PointHitTestResult(this, hitTestParameters.HitPoint); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/NoReadOnlySections.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/NoReadOnlySections.cs new file mode 100644 index 000000000..35cd1dd1b --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/NoReadOnlySections.cs @@ -0,0 +1,50 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Linq; +using System.Collections.Generic; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// <see cref="IReadOnlySectionProvider"/> that has no read-only sections; all text is editable. + /// </summary> + sealed class NoReadOnlySections : IReadOnlySectionProvider + { + public static readonly NoReadOnlySections Instance = new NoReadOnlySections(); + + public bool CanInsert(int offset) + { + return true; + } + + public IEnumerable<ISegment> GetDeletableSegments(ISegment segment) + { + if (segment == null) + throw new ArgumentNullException("segment"); + // the segment is always deletable + return ExtensionMethods.Sequence(segment); + } + } + + /// <summary> + /// <see cref="IReadOnlySectionProvider"/> that completely disables editing. + /// </summary> + sealed class ReadOnlyDocument : IReadOnlySectionProvider + { + public static readonly ReadOnlyDocument Instance = new ReadOnlyDocument(); + + public bool CanInsert(int offset) + { + return false; + } + + public IEnumerable<ISegment> GetDeletableSegments(ISegment segment) + { + return Enumerable.Empty<ISegment>(); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/RectangleSelection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/RectangleSelection.cs new file mode 100644 index 000000000..2d87d7934 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/RectangleSelection.cs @@ -0,0 +1,396 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media.TextFormatting; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Rectangular selection ("box selection"). + /// </summary> + public sealed class RectangleSelection : Selection + { + #region Commands + /// <summary> + /// Expands the selection left by one character, creating a rectangular selection. + /// Key gesture: Alt+Shift+Left + /// </summary> + public static readonly RoutedUICommand BoxSelectLeftByCharacter = Command("BoxSelectLeftByCharacter"); + + /// <summary> + /// Expands the selection right by one character, creating a rectangular selection. + /// Key gesture: Alt+Shift+Right + /// </summary> + public static readonly RoutedUICommand BoxSelectRightByCharacter = Command("BoxSelectRightByCharacter"); + + /// <summary> + /// Expands the selection left by one word, creating a rectangular selection. + /// Key gesture: Ctrl+Alt+Shift+Left + /// </summary> + public static readonly RoutedUICommand BoxSelectLeftByWord = Command("BoxSelectLeftByWord"); + + /// <summary> + /// Expands the selection left by one word, creating a rectangular selection. + /// Key gesture: Ctrl+Alt+Shift+Right + /// </summary> + public static readonly RoutedUICommand BoxSelectRightByWord = Command("BoxSelectRightByWord"); + + /// <summary> + /// Expands the selection up by one line, creating a rectangular selection. + /// Key gesture: Alt+Shift+Up + /// </summary> + public static readonly RoutedUICommand BoxSelectUpByLine = Command("BoxSelectUpByLine"); + + /// <summary> + /// Expands the selection up by one line, creating a rectangular selection. + /// Key gesture: Alt+Shift+Down + /// </summary> + public static readonly RoutedUICommand BoxSelectDownByLine = Command("BoxSelectDownByLine"); + + /// <summary> + /// Expands the selection to the start of the line, creating a rectangular selection. + /// Key gesture: Alt+Shift+Home + /// </summary> + public static readonly RoutedUICommand BoxSelectToLineStart = Command("BoxSelectToLineStart"); + + /// <summary> + /// Expands the selection to the end of the line, creating a rectangular selection. + /// Key gesture: Alt+Shift+End + /// </summary> + public static readonly RoutedUICommand BoxSelectToLineEnd = Command("BoxSelectToLineEnd"); + + static RoutedUICommand Command(string name) + { + return new RoutedUICommand(name, name, typeof(RectangleSelection)); + } + #endregion + + TextDocument document; + readonly int startLine, endLine; + readonly double startXPos, endXPos; + readonly int topLeftOffset, bottomRightOffset; + readonly TextViewPosition start, end; + + readonly List<SelectionSegment> segments = new List<SelectionSegment>(); + + #region Constructors + /// <summary> + /// Creates a new rectangular selection. + /// </summary> + public RectangleSelection(TextArea textArea, TextViewPosition start, TextViewPosition end) + : base(textArea) + { + InitDocument(); + this.startLine = start.Line; + this.endLine = end.Line; + this.startXPos = GetXPos(textArea, start); + this.endXPos = GetXPos(textArea, end); + CalculateSegments(); + this.topLeftOffset = this.segments.First().StartOffset; + this.bottomRightOffset = this.segments.Last().EndOffset; + + this.start = start; + this.end = end; + } + + private RectangleSelection(TextArea textArea, int startLine, double startXPos, TextViewPosition end) + : base(textArea) + { + InitDocument(); + this.startLine = startLine; + this.endLine = end.Line; + this.startXPos = startXPos; + this.endXPos = GetXPos(textArea, end); + CalculateSegments(); + this.topLeftOffset = this.segments.First().StartOffset; + this.bottomRightOffset = this.segments.Last().EndOffset; + + this.start = GetStart(); + this.end = end; + } + + private RectangleSelection(TextArea textArea, TextViewPosition start, int endLine, double endXPos) + : base(textArea) + { + InitDocument(); + this.startLine = start.Line; + this.endLine = endLine; + this.startXPos = GetXPos(textArea, start); + this.endXPos = endXPos; + CalculateSegments(); + this.topLeftOffset = this.segments.First().StartOffset; + this.bottomRightOffset = this.segments.Last().EndOffset; + + this.start = start; + this.end = GetEnd(); + } + + void InitDocument() + { + document = textArea.Document; + if (document == null) + throw ThrowUtil.NoDocumentAssigned(); + } + + static double GetXPos(TextArea textArea, TextViewPosition pos) + { + DocumentLine documentLine = textArea.Document.GetLineByNumber(pos.Line); + VisualLine visualLine = textArea.TextView.GetOrConstructVisualLine(documentLine); + int vc = visualLine.ValidateVisualColumn(pos, true); + TextLine textLine = visualLine.GetTextLine(vc); + return visualLine.GetTextLineVisualXPosition(textLine, vc); + } + + void CalculateSegments() + { + DocumentLine nextLine = document.GetLineByNumber(Math.Min(startLine, endLine)); + do { + VisualLine vl = textArea.TextView.GetOrConstructVisualLine(nextLine); + int startVC = vl.GetVisualColumn(new Point(startXPos, 0), true); + int endVC = vl.GetVisualColumn(new Point(endXPos, 0), true); + + int baseOffset = vl.FirstDocumentLine.Offset; + int startOffset = baseOffset + vl.GetRelativeOffset(startVC); + int endOffset = baseOffset + vl.GetRelativeOffset(endVC); + segments.Add(new SelectionSegment(startOffset, startVC, endOffset, endVC)); + + nextLine = vl.LastDocumentLine.NextLine; + } while (nextLine != null && nextLine.LineNumber <= Math.Max(startLine, endLine)); + } + + TextViewPosition GetStart() + { + SelectionSegment segment = (startLine < endLine ? segments.First() : segments.Last()); + if (startXPos < endXPos) { + return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn); + } else { + return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn); + } + } + + TextViewPosition GetEnd() + { + SelectionSegment segment = (startLine < endLine ? segments.Last() : segments.First()); + if (startXPos < endXPos) { + return new TextViewPosition(document.GetLocation(segment.EndOffset), segment.EndVisualColumn); + } else { + return new TextViewPosition(document.GetLocation(segment.StartOffset), segment.StartVisualColumn); + } + } + #endregion + + /// <inheritdoc/> + public override string GetText() + { + StringBuilder b = new StringBuilder(); + foreach (ISegment s in this.Segments) { + if (b.Length > 0) + b.AppendLine(); + b.Append(document.GetText(s)); + } + return b.ToString(); + } + + /// <inheritdoc/> + public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition) + { + return SetEndpoint(endPosition); + } + + /// <inheritdoc/> + public override int Length { + get { + return this.Segments.Sum(s => s.Length); + } + } + + /// <inheritdoc/> + public override bool EnableVirtualSpace { + get { return true; } + } + + /// <inheritdoc/> + public override ISegment SurroundingSegment { + get { + return new SimpleSegment(topLeftOffset, bottomRightOffset - topLeftOffset); + } + } + + /// <inheritdoc/> + public override IEnumerable<SelectionSegment> Segments { + get { return segments; } + } + + /// <inheritdoc/> + public override TextViewPosition StartPosition { + get { return start; } + } + + /// <inheritdoc/> + public override TextViewPosition EndPosition { + get { return end; } + } + + /// <inheritdoc/> + public override bool Equals(object obj) + { + RectangleSelection r = obj as RectangleSelection; + return r != null && r.textArea == this.textArea + && r.topLeftOffset == this.topLeftOffset && r.bottomRightOffset == this.bottomRightOffset + && r.startLine == this.startLine && r.endLine == this.endLine + && r.startXPos == this.startXPos && r.endXPos == this.endXPos; + } + + /// <inheritdoc/> + public override int GetHashCode() + { + return topLeftOffset ^ bottomRightOffset; + } + + /// <inheritdoc/> + public override Selection SetEndpoint(TextViewPosition endPosition) + { + return new RectangleSelection(textArea, startLine, startXPos, endPosition); + } + + int GetVisualColumnFromXPos(int line, double xPos) + { + var vl = textArea.TextView.GetOrConstructVisualLine(textArea.Document.GetLineByNumber(line)); + return vl.GetVisualColumn(new Point(xPos, 0), true); + } + + /// <inheritdoc/> + public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e) + { + TextLocation newStartLocation = textArea.Document.GetLocation(e.GetNewOffset(topLeftOffset, AnchorMovementType.AfterInsertion)); + TextLocation newEndLocation = textArea.Document.GetLocation(e.GetNewOffset(bottomRightOffset, AnchorMovementType.BeforeInsertion)); + + return new RectangleSelection(textArea, + new TextViewPosition(newStartLocation, GetVisualColumnFromXPos(newStartLocation.Line, startXPos)), + new TextViewPosition(newEndLocation, GetVisualColumnFromXPos(newEndLocation.Line, endXPos))); + } + + /// <inheritdoc/> + public override void ReplaceSelectionWithText(string newText) + { + if (newText == null) + throw new ArgumentNullException("newText"); + using (textArea.Document.RunUpdate()) { + TextViewPosition start = new TextViewPosition(document.GetLocation(topLeftOffset), GetVisualColumnFromXPos(startLine, startXPos)); + TextViewPosition end = new TextViewPosition(document.GetLocation(bottomRightOffset), GetVisualColumnFromXPos(endLine, endXPos)); + int insertionLength; + int totalInsertionLength = 0; + int firstInsertionLength = 0; + int editOffset = Math.Min(topLeftOffset, bottomRightOffset); + TextViewPosition pos; + if (NewLineFinder.NextNewLine(newText, 0) == SimpleSegment.Invalid) { + // insert same text into every line + foreach (SelectionSegment lineSegment in this.Segments.Reverse()) { + ReplaceSingleLineText(textArea, lineSegment, newText, out insertionLength); + totalInsertionLength += insertionLength; + firstInsertionLength = insertionLength; + } + + int newEndOffset = editOffset + totalInsertionLength; + pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength)); + + textArea.Selection = new RectangleSelection(textArea, pos, Math.Max(startLine, endLine), GetXPos(textArea, pos)); + } else { + string[] lines = newText.Split(NewLineFinder.NewlineStrings, segments.Count, StringSplitOptions.None); + int line = Math.Min(startLine, endLine); + for (int i = lines.Length - 1; i >= 0; i--) { + ReplaceSingleLineText(textArea, segments[i], lines[i], out insertionLength); + firstInsertionLength = insertionLength; + } + pos = new TextViewPosition(document.GetLocation(editOffset + firstInsertionLength)); + textArea.ClearSelection(); + } + textArea.Caret.Position = textArea.TextView.GetPosition(new Point(GetXPos(textArea, pos), textArea.TextView.GetVisualTopByDocumentLine(Math.Max(startLine, endLine)))).GetValueOrDefault(); + } + } + + void ReplaceSingleLineText(TextArea textArea, SelectionSegment lineSegment, string newText, out int insertionLength) + { + if (lineSegment.Length == 0) { + if (newText.Length > 0 && textArea.ReadOnlySectionProvider.CanInsert(lineSegment.StartOffset)) { + newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn)); + textArea.Document.Insert(lineSegment.StartOffset, newText); + } + } else { + ISegment[] segmentsToDelete = textArea.GetDeletableSegments(lineSegment); + for (int i = segmentsToDelete.Length - 1; i >= 0; i--) { + if (i == segmentsToDelete.Length - 1) { + if (segmentsToDelete[i].Offset == SurroundingSegment.Offset && segmentsToDelete[i].Length == SurroundingSegment.Length) { + newText = AddSpacesIfRequired(newText, new TextViewPosition(document.GetLocation(lineSegment.StartOffset), lineSegment.StartVisualColumn), new TextViewPosition(document.GetLocation(lineSegment.EndOffset), lineSegment.EndVisualColumn)); + } + textArea.Document.Replace(segmentsToDelete[i], newText); + } else { + textArea.Document.Remove(segmentsToDelete[i]); + } + } + } + insertionLength = newText.Length; + } + + /// <summary> + /// Performs a rectangular paste operation. + /// </summary> + public static bool PerformRectangularPaste(TextArea textArea, TextViewPosition startPosition, string text, bool selectInsertedText) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + if (text == null) + throw new ArgumentNullException("text"); + int newLineCount = text.Count(c => c == '\n'); // TODO might not work in all cases, but single \r line endings are really rare today. + TextLocation endLocation = new TextLocation(startPosition.Line + newLineCount, startPosition.Column); + if (endLocation.Line <= textArea.Document.LineCount) { + int endOffset = textArea.Document.GetOffset(endLocation); + if (textArea.Selection.EnableVirtualSpace || textArea.Document.GetLocation(endOffset) == endLocation) { + RectangleSelection rsel = new RectangleSelection(textArea, startPosition, endLocation.Line, GetXPos(textArea, startPosition)); + rsel.ReplaceSelectionWithText(text); + if (selectInsertedText && textArea.Selection is RectangleSelection) { + RectangleSelection sel = (RectangleSelection)textArea.Selection; + textArea.Selection = new RectangleSelection(textArea, startPosition, sel.endLine, sel.endXPos); + } + return true; + } + } + return false; + } + + /// <summary> + /// Gets the name of the entry in the DataObject that signals rectangle selections. + /// </summary> + public const string RectangularSelectionDataType = "AvalonEditRectangularSelection"; + + /// <inheritdoc/> + public override System.Windows.DataObject CreateDataObject(TextArea textArea) + { + var data = base.CreateDataObject(textArea); + + MemoryStream isRectangle = new MemoryStream(1); + isRectangle.WriteByte(1); + data.SetData(RectangularSelectionDataType, isRectangle, false); + return data; + } + + /// <inheritdoc/> + public override string ToString() + { + // It's possible that ToString() gets called on old (invalid) selections, e.g. for "change from... to..." debug message + // make sure we don't crash even when the desired locations don't exist anymore. + return string.Format("[RectangleSelection {0} {1} {2} to {3} {4} {5}]", startLine, topLeftOffset, startXPos, endLine, bottomRightOffset, endXPos); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/Selection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/Selection.cs new file mode 100644 index 000000000..014fa8680 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/Selection.cs @@ -0,0 +1,268 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Highlighting; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Base class for selections. + /// </summary> + public abstract class Selection + { + /// <summary> + /// Creates a new simple selection that selects the text from startOffset to endOffset. + /// </summary> + public static Selection Create(TextArea textArea, int startOffset, int endOffset) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + if (startOffset == endOffset) + return textArea.emptySelection; + else + return new SimpleSelection(textArea, + new TextViewPosition(textArea.Document.GetLocation(startOffset)), + new TextViewPosition(textArea.Document.GetLocation(endOffset))); + } + + internal static Selection Create(TextArea textArea, TextViewPosition start, TextViewPosition end) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + if (textArea.Document.GetOffset(start.Location) == textArea.Document.GetOffset(end.Location) && start.VisualColumn == end.VisualColumn) + return textArea.emptySelection; + else + return new SimpleSelection(textArea, start, end); + } + + /// <summary> + /// Creates a new simple selection that selects the text in the specified segment. + /// </summary> + public static Selection Create(TextArea textArea, ISegment segment) + { + if (segment == null) + throw new ArgumentNullException("segment"); + return Create(textArea, segment.Offset, segment.EndOffset); + } + + internal readonly TextArea textArea; + + /// <summary> + /// Constructor for Selection. + /// </summary> + protected Selection(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.textArea = textArea; + } + + /// <summary> + /// Gets the start position of the selection. + /// </summary> + public abstract TextViewPosition StartPosition { get; } + + /// <summary> + /// Gets the end position of the selection. + /// </summary> + public abstract TextViewPosition EndPosition { get; } + + /// <summary> + /// Gets the selected text segments. + /// </summary> + public abstract IEnumerable<SelectionSegment> Segments { get; } + + /// <summary> + /// Gets the smallest segment that contains all segments in this selection. + /// May return null if the selection is empty. + /// </summary> + public abstract ISegment SurroundingSegment { get; } + + /// <summary> + /// Replaces the selection with the specified text. + /// </summary> + public abstract void ReplaceSelectionWithText(string newText); + + internal string AddSpacesIfRequired(string newText, TextViewPosition start, TextViewPosition end) + { + if (EnableVirtualSpace && InsertVirtualSpaces(newText, start, end)) { + var line = textArea.Document.GetLineByNumber(start.Line); + string lineText = textArea.Document.GetText(line); + var vLine = textArea.TextView.GetOrConstructVisualLine(line); + int colDiff = start.VisualColumn - vLine.VisualLengthWithEndOfLineMarker; + if (colDiff > 0) { + string additionalSpaces = ""; + if (!textArea.Options.ConvertTabsToSpaces && lineText.Trim('\t').Length == 0) { + int tabCount = (int)(colDiff / textArea.Options.IndentationSize); + additionalSpaces = new string('\t', tabCount); + colDiff -= tabCount * textArea.Options.IndentationSize; + } + additionalSpaces += new string(' ', colDiff); + return additionalSpaces + newText; + } + } + return newText; + } + + bool InsertVirtualSpaces(string newText, TextViewPosition start, TextViewPosition end) + { + return (!string.IsNullOrEmpty(newText) || !(IsInVirtualSpace(start) && IsInVirtualSpace(end))) + && newText != "\r\n" + && newText != "\n" + && newText != "\r"; + } + + bool IsInVirtualSpace(TextViewPosition pos) + { + return pos.VisualColumn > textArea.TextView.GetOrConstructVisualLine(textArea.Document.GetLineByNumber(pos.Line)).VisualLength; + } + + /// <summary> + /// Updates the selection when the document changes. + /// </summary> + public abstract Selection UpdateOnDocumentChange(DocumentChangeEventArgs e); + + /// <summary> + /// Gets whether the selection is empty. + /// </summary> + public virtual bool IsEmpty { + get { return Length == 0; } + } + + /// <summary> + /// Gets whether virtual space is enabled for this selection. + /// </summary> + public virtual bool EnableVirtualSpace { + get { return textArea.Options.EnableVirtualSpace; } + } + + /// <summary> + /// Gets the selection length. + /// </summary> + public abstract int Length { get; } + + /// <summary> + /// Returns a new selection with the changed end point. + /// </summary> + /// <exception cref="NotSupportedException">Cannot set endpoint for empty selection</exception> + public abstract Selection SetEndpoint(TextViewPosition endPosition); + + /// <summary> + /// If this selection is empty, starts a new selection from <paramref name="startPosition"/> to + /// <paramref name="endPosition"/>, otherwise, changes the endpoint of this selection. + /// </summary> + public abstract Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition); + + /// <summary> + /// Gets whether the selection is multi-line. + /// </summary> + public virtual bool IsMultiline { + get { + ISegment surroundingSegment = this.SurroundingSegment; + if (surroundingSegment == null) + return false; + int start = surroundingSegment.Offset; + int end = start + surroundingSegment.Length; + var document = textArea.Document; + if (document == null) + throw ThrowUtil.NoDocumentAssigned(); + return document.GetLineByOffset(start) != document.GetLineByOffset(end); + } + } + + /// <summary> + /// Gets the selected text. + /// </summary> + public virtual string GetText() + { + var document = textArea.Document; + if (document == null) + throw ThrowUtil.NoDocumentAssigned(); + StringBuilder b = null; + string text = null; + foreach (ISegment s in Segments) { + if (text != null) { + if (b == null) + b = new StringBuilder(text); + else + b.Append(text); + } + text = document.GetText(s); + } + if (b != null) { + if (text != null) b.Append(text); + return b.ToString(); + } else { + return text ?? string.Empty; + } + } + + /// <summary> + /// Creates a HTML fragment for the selected text. + /// </summary> + public string CreateHtmlFragment(HtmlOptions options) + { + if (options == null) + throw new ArgumentNullException("options"); + IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; + StringBuilder html = new StringBuilder(); + bool first = true; + foreach (ISegment selectedSegment in this.Segments) { + if (first) + first = false; + else + html.AppendLine("<br>"); + html.Append(HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, selectedSegment, options)); + } + return html.ToString(); + } + + /// <inheritdoc/> + public abstract override bool Equals(object obj); + + /// <inheritdoc/> + public abstract override int GetHashCode(); + + /// <summary> + /// Gets whether the specified offset is included in the selection. + /// </summary> + /// <returns>True, if the selection contains the offset (selection borders inclusive); + /// otherwise, false.</returns> + public virtual bool Contains(int offset) + { + if (this.IsEmpty) + return false; + if (this.SurroundingSegment.Contains(offset)) { + foreach (ISegment s in this.Segments) { + if (s.Contains(offset)) { + return true; + } + } + } + return false; + } + + /// <summary> + /// Creates a data object containing the selection's text. + /// </summary> + public virtual DataObject CreateDataObject(TextArea textArea) + { + string text = GetText(); + // Ensure we use the appropriate newline sequence for the OS + DataObject data = new DataObject(TextUtilities.NormalizeNewLines(text, Environment.NewLine)); + // we cannot use DataObject.SetText - then we cannot drag to SciTe + // (but dragging to Word works in both cases) + + // Also copy text in HTML format to clipboard - good for pasting text into Word + // or to the SharpDevelop forums. + HtmlClipboard.SetHtml(data, CreateHtmlFragment(new HtmlOptions(textArea.Options))); + return data; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionColorizer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionColorizer.cs new file mode 100644 index 000000000..857c4c060 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionColorizer.cs @@ -0,0 +1,58 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; + +namespace Tango.Scripting.Editors.Editing +{ + sealed class SelectionColorizer : ColorizingTransformer + { + TextArea textArea; + + public SelectionColorizer(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.textArea = textArea; + } + + protected override void Colorize(ITextRunConstructionContext context) + { + // if SelectionForeground is null, keep the existing foreground color + if (textArea.SelectionForeground == null) + return; + + int lineStartOffset = context.VisualLine.FirstDocumentLine.Offset; + int lineEndOffset = context.VisualLine.LastDocumentLine.Offset + context.VisualLine.LastDocumentLine.TotalLength; + + foreach (SelectionSegment segment in textArea.Selection.Segments) { + int segmentStart = segment.StartOffset; + int segmentEnd = segment.EndOffset; + if (segmentEnd <= lineStartOffset) + continue; + if (segmentStart >= lineEndOffset) + continue; + int startColumn; + if (segmentStart < lineStartOffset) + startColumn = 0; + else + startColumn = context.VisualLine.ValidateVisualColumn(segment.StartOffset, segment.StartVisualColumn, textArea.Selection.EnableVirtualSpace); + + int endColumn; + if (segmentEnd > lineEndOffset) + endColumn = textArea.Selection.EnableVirtualSpace ? int.MaxValue : context.VisualLine.VisualLengthWithEndOfLineMarker; + else + endColumn = context.VisualLine.ValidateVisualColumn(segment.EndOffset, segment.EndVisualColumn, textArea.Selection.EnableVirtualSpace); + + ChangeVisualElements( + startColumn, endColumn, + element => { + element.TextRunProperties.SetForegroundBrush(textArea.SelectionForeground); + }); + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionLayer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionLayer.cs new file mode 100644 index 000000000..28631affe --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionLayer.cs @@ -0,0 +1,53 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Windows; +using System.Windows.Media; + +using Tango.Scripting.Editors.Rendering; + +namespace Tango.Scripting.Editors.Editing +{ + sealed class SelectionLayer : Layer, IWeakEventListener + { + readonly TextArea textArea; + + public SelectionLayer(TextArea textArea) : base(textArea.TextView, KnownLayer.Selection) + { + this.IsHitTestVisible = false; + + this.textArea = textArea; + TextViewWeakEventManager.VisualLinesChanged.AddListener(textView, this); + TextViewWeakEventManager.ScrollOffsetChanged.AddListener(textView, this); + } + + bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + if (managerType == typeof(TextViewWeakEventManager.VisualLinesChanged) + || managerType == typeof(TextViewWeakEventManager.ScrollOffsetChanged)) + { + InvalidateVisual(); + return true; + } + return false; + } + + protected override void OnRender(DrawingContext drawingContext) + { + base.OnRender(drawingContext); + + BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder(); + geoBuilder.AlignToMiddleOfPixels = true; + geoBuilder.ExtendToFullWidthAtLineEnd = textArea.Selection.EnableVirtualSpace; + geoBuilder.CornerRadius = textArea.SelectionCornerRadius; + foreach (var segment in textArea.Selection.Segments) { + geoBuilder.AddSegment(textView, segment); + } + Geometry geometry = geoBuilder.CreateGeometry(); + if (geometry != null) { + drawingContext.DrawGeometry(textArea.SelectionBrush, textArea.SelectionBorder, geometry); + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionMouseHandler.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionMouseHandler.cs new file mode 100644 index 000000000..3c5ec51dc --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionMouseHandler.cs @@ -0,0 +1,631 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media.TextFormatting; +using System.Windows.Threading; +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Handles selection of text using the mouse. + /// </summary> + sealed class SelectionMouseHandler : ITextAreaInputHandler + { + #region enum SelectionMode + enum SelectionMode + { + /// <summary> + /// no selection (no mouse button down) + /// </summary> + None, + /// <summary> + /// left mouse button down on selection, might be normal click + /// or might be drag'n'drop + /// </summary> + PossibleDragStart, + /// <summary> + /// dragging text + /// </summary> + Drag, + /// <summary> + /// normal selection (click+drag) + /// </summary> + Normal, + /// <summary> + /// whole-word selection (double click+drag or ctrl+click+drag) + /// </summary> + WholeWord, + /// <summary> + /// whole-line selection (triple click+drag) + /// </summary> + WholeLine, + /// <summary> + /// rectangular selection (alt+click+drag) + /// </summary> + Rectangular + } + #endregion + + readonly TextArea textArea; + + SelectionMode mode; + AnchorSegment startWord; + Point possibleDragStartMousePos; + + #region Constructor + Attach + Detach + public SelectionMouseHandler(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.textArea = textArea; + } + + public TextArea TextArea { + get { return textArea; } + } + + public void Attach() + { + textArea.MouseLeftButtonDown += textArea_MouseLeftButtonDown; + textArea.MouseMove += textArea_MouseMove; + textArea.MouseLeftButtonUp += textArea_MouseLeftButtonUp; + textArea.QueryCursor += textArea_QueryCursor; + textArea.OptionChanged += textArea_OptionChanged; + + enableTextDragDrop = textArea.Options.EnableTextDragDrop; + if (enableTextDragDrop) { + AttachDragDrop(); + } + } + + public void Detach() + { + mode = SelectionMode.None; + textArea.MouseLeftButtonDown -= textArea_MouseLeftButtonDown; + textArea.MouseMove -= textArea_MouseMove; + textArea.MouseLeftButtonUp -= textArea_MouseLeftButtonUp; + textArea.QueryCursor -= textArea_QueryCursor; + textArea.OptionChanged -= textArea_OptionChanged; + if (enableTextDragDrop) { + DetachDragDrop(); + } + } + + void AttachDragDrop() + { + textArea.AllowDrop = true; + textArea.GiveFeedback += textArea_GiveFeedback; + textArea.QueryContinueDrag += textArea_QueryContinueDrag; + textArea.DragEnter += textArea_DragEnter; + textArea.DragOver += textArea_DragOver; + textArea.DragLeave += textArea_DragLeave; + textArea.Drop += textArea_Drop; + } + + void DetachDragDrop() + { + textArea.AllowDrop = false; + textArea.GiveFeedback -= textArea_GiveFeedback; + textArea.QueryContinueDrag -= textArea_QueryContinueDrag; + textArea.DragEnter -= textArea_DragEnter; + textArea.DragOver -= textArea_DragOver; + textArea.DragLeave -= textArea_DragLeave; + textArea.Drop -= textArea_Drop; + } + + bool enableTextDragDrop; + + void textArea_OptionChanged(object sender, PropertyChangedEventArgs e) + { + bool newEnableTextDragDrop = textArea.Options.EnableTextDragDrop; + if (newEnableTextDragDrop != enableTextDragDrop) { + enableTextDragDrop = newEnableTextDragDrop; + if (newEnableTextDragDrop) + AttachDragDrop(); + else + DetachDragDrop(); + } + } + #endregion + + #region Dropping text + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + void textArea_DragEnter(object sender, DragEventArgs e) + { + try { + e.Effects = GetEffect(e); + textArea.Caret.Show(); + } catch (Exception ex) { + OnDragException(ex); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + void textArea_DragOver(object sender, DragEventArgs e) + { + try { + e.Effects = GetEffect(e); + } catch (Exception ex) { + OnDragException(ex); + } + } + + DragDropEffects GetEffect(DragEventArgs e) + { + if (e.Data.GetDataPresent(DataFormats.UnicodeText, true)) { + e.Handled = true; + int visualColumn; + int offset = GetOffsetFromMousePosition(e.GetPosition(textArea.TextView), out visualColumn); + if (offset >= 0) { + textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(offset), visualColumn); + textArea.Caret.DesiredXPos = double.NaN; + if (textArea.ReadOnlySectionProvider.CanInsert(offset)) { + if ((e.AllowedEffects & DragDropEffects.Move) == DragDropEffects.Move + && (e.KeyStates & DragDropKeyStates.ControlKey) != DragDropKeyStates.ControlKey) + { + return DragDropEffects.Move; + } else { + return e.AllowedEffects & DragDropEffects.Copy; + } + } + } + } + return DragDropEffects.None; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + void textArea_DragLeave(object sender, DragEventArgs e) + { + try { + e.Handled = true; + if (!textArea.IsKeyboardFocusWithin) + textArea.Caret.Hide(); + } catch (Exception ex) { + OnDragException(ex); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + void textArea_Drop(object sender, DragEventArgs e) + { + try { + DragDropEffects effect = GetEffect(e); + e.Effects = effect; + if (effect != DragDropEffects.None) { + string text = e.Data.GetData(DataFormats.UnicodeText, true) as string; + if (text != null) { + int start = textArea.Caret.Offset; + if (mode == SelectionMode.Drag && textArea.Selection.Contains(start)) { + Debug.WriteLine("Drop: did not drop: drop target is inside selection"); + e.Effects = DragDropEffects.None; + } else { + Debug.WriteLine("Drop: insert at " + start); + + bool rectangular = e.Data.GetDataPresent(RectangleSelection.RectangularSelectionDataType); + + string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line); + text = TextUtilities.NormalizeNewLines(text, newLine); + + // Mark the undo group with the currentDragDescriptor, if the drag + // is originating from the same control. This allows combining + // the undo groups when text is moved. + textArea.Document.UndoStack.StartUndoGroup(this.currentDragDescriptor); + try { + if (rectangular && RectangleSelection.PerformRectangularPaste(textArea, textArea.Caret.Position, text, true)) { + + } else { + textArea.Document.Insert(start, text); + textArea.Selection = Selection.Create(textArea, start, start + text.Length); + } + } finally { + textArea.Document.UndoStack.EndUndoGroup(); + } + } + e.Handled = true; + } + } + } catch (Exception ex) { + OnDragException(ex); + } + } + + void OnDragException(Exception ex) + { + // WPF swallows exceptions during drag'n'drop or reports them incorrectly, so + // we re-throw them later to allow the application's unhandled exception handler + // to catch them + textArea.Dispatcher.BeginInvoke( + DispatcherPriority.Normal, + new Action(delegate { + throw new DragDropException("Exception during drag'n'drop", ex); + })); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + void textArea_GiveFeedback(object sender, GiveFeedbackEventArgs e) + { + try { + e.UseDefaultCursors = true; + e.Handled = true; + } catch (Exception ex) { + OnDragException(ex); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + void textArea_QueryContinueDrag(object sender, QueryContinueDragEventArgs e) + { + try { + if (e.EscapePressed) { + e.Action = DragAction.Cancel; + } else if ((e.KeyStates & DragDropKeyStates.LeftMouseButton) != DragDropKeyStates.LeftMouseButton) { + e.Action = DragAction.Drop; + } else { + e.Action = DragAction.Continue; + } + e.Handled = true; + } catch (Exception ex) { + OnDragException(ex); + } + } + #endregion + + #region Start Drag + object currentDragDescriptor; + + void StartDrag() + { + // prevent nested StartDrag calls + mode = SelectionMode.Drag; + + // mouse capture and Drag'n'Drop doesn't mix + textArea.ReleaseMouseCapture(); + + DataObject dataObject = textArea.Selection.CreateDataObject(textArea); + + DragDropEffects allowedEffects = DragDropEffects.All; + var deleteOnMove = textArea.Selection.Segments.Select(s => new AnchorSegment(textArea.Document, s)).ToList(); + foreach (ISegment s in deleteOnMove) { + ISegment[] result = textArea.GetDeletableSegments(s); + if (result.Length != 1 || result[0].Offset != s.Offset || result[0].EndOffset != s.EndOffset) { + allowedEffects &= ~DragDropEffects.Move; + } + } + + object dragDescriptor = new object(); + this.currentDragDescriptor = dragDescriptor; + + DragDropEffects resultEffect; + using (textArea.AllowCaretOutsideSelection()) { + var oldCaretPosition = textArea.Caret.Position; + try { + Debug.WriteLine("DoDragDrop with allowedEffects=" + allowedEffects); + resultEffect = DragDrop.DoDragDrop(textArea, dataObject, allowedEffects); + Debug.WriteLine("DoDragDrop done, resultEffect=" + resultEffect); + } catch (COMException ex) { + // ignore COM errors - don't crash on badly implemented drop targets + Debug.WriteLine("DoDragDrop failed: " + ex.ToString()); + return; + } + if (resultEffect == DragDropEffects.None) { + // reset caret if drag was aborted + textArea.Caret.Position = oldCaretPosition; + } + } + + this.currentDragDescriptor = null; + + if (deleteOnMove != null && resultEffect == DragDropEffects.Move && (allowedEffects & DragDropEffects.Move) == DragDropEffects.Move) { + bool draggedInsideSingleDocument = (dragDescriptor == textArea.Document.UndoStack.LastGroupDescriptor); + if (draggedInsideSingleDocument) + textArea.Document.UndoStack.StartContinuedUndoGroup(null); + textArea.Document.BeginUpdate(); + try { + foreach (ISegment s in deleteOnMove) { + textArea.Document.Remove(s.Offset, s.Length); + } + } finally { + textArea.Document.EndUpdate(); + if (draggedInsideSingleDocument) + textArea.Document.UndoStack.EndUndoGroup(); + } + } + } + #endregion + + #region QueryCursor + // provide the IBeam Cursor for the text area + void textArea_QueryCursor(object sender, QueryCursorEventArgs e) + { + if (!e.Handled) { + if (mode != SelectionMode.None || !enableTextDragDrop) { + e.Cursor = Cursors.IBeam; + e.Handled = true; + } else if (textArea.TextView.VisualLinesValid) { + // Only query the cursor if the visual lines are valid. + // If they are invalid, the cursor will get re-queried when the visual lines + // get refreshed. + Point p = e.GetPosition(textArea.TextView); + if (p.X >= 0 && p.Y >= 0 && p.X <= textArea.TextView.ActualWidth && p.Y <= textArea.TextView.ActualHeight) { + int visualColumn; + int offset = GetOffsetFromMousePosition(e, out visualColumn); + if (textArea.Selection.Contains(offset)) + e.Cursor = Cursors.Arrow; + else + e.Cursor = Cursors.IBeam; + e.Handled = true; + } + } + } + } + #endregion + + #region LeftButtonDown + void textArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + mode = SelectionMode.None; + if (!e.Handled && e.ChangedButton == MouseButton.Left) { + ModifierKeys modifiers = Keyboard.Modifiers; + bool shift = (modifiers & ModifierKeys.Shift) == ModifierKeys.Shift; + if (enableTextDragDrop && e.ClickCount == 1 && !shift) { + int visualColumn; + int offset = GetOffsetFromMousePosition(e, out visualColumn); + if (textArea.Selection.Contains(offset)) { + if (textArea.CaptureMouse()) { + mode = SelectionMode.PossibleDragStart; + possibleDragStartMousePos = e.GetPosition(textArea); + } + e.Handled = true; + return; + } + } + + var oldPosition = textArea.Caret.Position; + SetCaretOffsetToMousePosition(e); + + + if (!shift) { + textArea.ClearSelection(); + } + if (textArea.CaptureMouse()) { + if ((modifiers & ModifierKeys.Alt) == ModifierKeys.Alt && textArea.Options.EnableRectangularSelection) { + mode = SelectionMode.Rectangular; + if (shift && textArea.Selection is RectangleSelection) { + textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldPosition, textArea.Caret.Position); + } + } else if (e.ClickCount == 1 && ((modifiers & ModifierKeys.Control) == 0)) { + mode = SelectionMode.Normal; + if (shift && !(textArea.Selection is RectangleSelection)) { + textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldPosition, textArea.Caret.Position); + } + } else { + SimpleSegment startWord; + if (e.ClickCount == 3) { + mode = SelectionMode.WholeLine; + startWord = GetLineAtMousePosition(e); + } else { + mode = SelectionMode.WholeWord; + startWord = GetWordAtMousePosition(e); + } + if (startWord == SimpleSegment.Invalid) { + mode = SelectionMode.None; + textArea.ReleaseMouseCapture(); + return; + } + if (shift && !textArea.Selection.IsEmpty) { + if (startWord.Offset < textArea.Selection.SurroundingSegment.Offset) { + textArea.Selection = textArea.Selection.SetEndpoint(new TextViewPosition(textArea.Document.GetLocation(startWord.Offset))); + } else if (startWord.EndOffset > textArea.Selection.SurroundingSegment.EndOffset) { + textArea.Selection = textArea.Selection.SetEndpoint(new TextViewPosition(textArea.Document.GetLocation(startWord.EndOffset))); + } + this.startWord = new AnchorSegment(textArea.Document, textArea.Selection.SurroundingSegment); + } else { + textArea.Selection = Selection.Create(textArea, startWord.Offset, startWord.EndOffset); + this.startWord = new AnchorSegment(textArea.Document, startWord.Offset, startWord.Length); + } + } + } + } + e.Handled = true; + } + #endregion + + #region Mouse Position <-> Text coordinates + SimpleSegment GetWordAtMousePosition(MouseEventArgs e) + { + TextView textView = textArea.TextView; + if (textView == null) return SimpleSegment.Invalid; + Point pos = e.GetPosition(textView); + if (pos.Y < 0) + pos.Y = 0; + if (pos.Y > textView.ActualHeight) + pos.Y = textView.ActualHeight; + pos += textView.ScrollOffset; + VisualLine line = textView.GetVisualLineFromVisualTop(pos.Y); + if (line != null) { + int visualColumn = line.GetVisualColumn(pos, textArea.Selection.EnableVirtualSpace); + int wordStartVC = line.GetNextCaretPosition(visualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.WordStartOrSymbol, textArea.Selection.EnableVirtualSpace); + if (wordStartVC == -1) + wordStartVC = 0; + int wordEndVC = line.GetNextCaretPosition(wordStartVC, LogicalDirection.Forward, CaretPositioningMode.WordBorderOrSymbol, textArea.Selection.EnableVirtualSpace); + if (wordEndVC == -1) + wordEndVC = line.VisualLength; + int relOffset = line.FirstDocumentLine.Offset; + int wordStartOffset = line.GetRelativeOffset(wordStartVC) + relOffset; + int wordEndOffset = line.GetRelativeOffset(wordEndVC) + relOffset; + return new SimpleSegment(wordStartOffset, wordEndOffset - wordStartOffset); + } else { + return SimpleSegment.Invalid; + } + } + + SimpleSegment GetLineAtMousePosition(MouseEventArgs e) + { + TextView textView = textArea.TextView; + if (textView == null) return SimpleSegment.Invalid; + Point pos = e.GetPosition(textView); + if (pos.Y < 0) + pos.Y = 0; + if (pos.Y > textView.ActualHeight) + pos.Y = textView.ActualHeight; + pos += textView.ScrollOffset; + VisualLine line = textView.GetVisualLineFromVisualTop(pos.Y); + if (line != null) { + return new SimpleSegment(line.StartOffset, line.LastDocumentLine.EndOffset - line.StartOffset); + } else { + return SimpleSegment.Invalid; + } + } + + int GetOffsetFromMousePosition(MouseEventArgs e, out int visualColumn) + { + return GetOffsetFromMousePosition(e.GetPosition(textArea.TextView), out visualColumn); + } + + int GetOffsetFromMousePosition(Point positionRelativeToTextView, out int visualColumn) + { + visualColumn = 0; + TextView textView = textArea.TextView; + Point pos = positionRelativeToTextView; + if (pos.Y < 0) + pos.Y = 0; + if (pos.Y > textView.ActualHeight) + pos.Y = textView.ActualHeight; + pos += textView.ScrollOffset; + if (pos.Y > textView.DocumentHeight) + pos.Y = textView.DocumentHeight - ExtensionMethods.Epsilon; + VisualLine line = textView.GetVisualLineFromVisualTop(pos.Y); + if (line != null) { + visualColumn = line.GetVisualColumn(pos, textArea.Selection.EnableVirtualSpace); + return line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset; + } + return -1; + } + + int GetOffsetFromMousePositionFirstTextLineOnly(Point positionRelativeToTextView, out int visualColumn) + { + visualColumn = 0; + TextView textView = textArea.TextView; + Point pos = positionRelativeToTextView; + if (pos.Y < 0) + pos.Y = 0; + if (pos.Y > textView.ActualHeight) + pos.Y = textView.ActualHeight; + pos += textView.ScrollOffset; + if (pos.Y > textView.DocumentHeight) + pos.Y = textView.DocumentHeight - ExtensionMethods.Epsilon; + VisualLine line = textView.GetVisualLineFromVisualTop(pos.Y); + if (line != null) { + visualColumn = line.GetVisualColumn(line.TextLines.First(), pos.X, textArea.Selection.EnableVirtualSpace); + return line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset; + } + return -1; + } + #endregion + + #region MouseMove + void textArea_MouseMove(object sender, MouseEventArgs e) + { + if (e.Handled) + return; + if (mode == SelectionMode.Normal || mode == SelectionMode.WholeWord || mode == SelectionMode.WholeLine || mode == SelectionMode.Rectangular) { + e.Handled = true; + if (textArea.TextView.VisualLinesValid) { + // If the visual lines are not valid, don't extend the selection. + // Extending the selection forces a VisualLine refresh, and it is sufficient + // to do that on MouseUp, we don't have to do it every MouseMove. + ExtendSelectionToMouse(e); + } + } else if (mode == SelectionMode.PossibleDragStart) { + e.Handled = true; + Vector mouseMovement = e.GetPosition(textArea) - possibleDragStartMousePos; + if (Math.Abs(mouseMovement.X) > SystemParameters.MinimumHorizontalDragDistance + || Math.Abs(mouseMovement.Y) > SystemParameters.MinimumVerticalDragDistance) + { + StartDrag(); + } + } + } + #endregion + + #region ExtendSelection + void SetCaretOffsetToMousePosition(MouseEventArgs e) + { + SetCaretOffsetToMousePosition(e, null); + } + + void SetCaretOffsetToMousePosition(MouseEventArgs e, ISegment allowedSegment) + { + int visualColumn; + int offset; + if (mode == SelectionMode.Rectangular) + offset = GetOffsetFromMousePositionFirstTextLineOnly(e.GetPosition(textArea.TextView), out visualColumn); + else + offset = GetOffsetFromMousePosition(e, out visualColumn); + if (allowedSegment != null) { + offset = offset.CoerceValue(allowedSegment.Offset, allowedSegment.EndOffset); + } + if (offset >= 0) { + textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(offset), visualColumn); + textArea.Caret.DesiredXPos = double.NaN; + } + } + + void ExtendSelectionToMouse(MouseEventArgs e) + { + TextViewPosition oldPosition = textArea.Caret.Position; + if (mode == SelectionMode.Normal || mode == SelectionMode.Rectangular) { + SetCaretOffsetToMousePosition(e); + if (mode == SelectionMode.Normal && textArea.Selection is RectangleSelection) + textArea.Selection = new SimpleSelection(textArea, oldPosition, textArea.Caret.Position); + else if (mode == SelectionMode.Rectangular && !(textArea.Selection is RectangleSelection)) + textArea.Selection = new RectangleSelection(textArea, oldPosition, textArea.Caret.Position); + else + textArea.Selection = textArea.Selection.StartSelectionOrSetEndpoint(oldPosition, textArea.Caret.Position); + } else if (mode == SelectionMode.WholeWord || mode == SelectionMode.WholeLine) { + var newWord = (mode == SelectionMode.WholeLine) ? GetLineAtMousePosition(e) : GetWordAtMousePosition(e); + if (newWord != SimpleSegment.Invalid) { + textArea.Selection = Selection.Create(textArea, + Math.Min(newWord.Offset, startWord.Offset), + Math.Max(newWord.EndOffset, startWord.EndOffset)); + // Set caret offset, but limit the caret to stay inside the selection. + // in whole-word selection, it's otherwise possible that we get the caret outside the + // selection - but the TextArea doesn't like that and will reset the selection, causing + // flickering. + SetCaretOffsetToMousePosition(e, textArea.Selection.SurroundingSegment); + } + } + textArea.Caret.BringCaretToView(5.0); + } + #endregion + + #region MouseLeftButtonUp + void textArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (mode == SelectionMode.None || e.Handled) + return; + e.Handled = true; + if (mode == SelectionMode.PossibleDragStart) { + // -> this was not a drag start (mouse didn't move after mousedown) + SetCaretOffsetToMousePosition(e); + textArea.ClearSelection(); + } else if (mode == SelectionMode.Normal || mode == SelectionMode.WholeWord || mode == SelectionMode.WholeLine || mode == SelectionMode.Rectangular) { + ExtendSelectionToMouse(e); + } + mode = SelectionMode.None; + textArea.ReleaseMouseCapture(); + } + #endregion + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionSegment.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionSegment.cs new file mode 100644 index 000000000..46c560b34 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SelectionSegment.cs @@ -0,0 +1,89 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Represents a selected segment. + /// </summary> + public class SelectionSegment : ISegment + { + readonly int startOffset, endOffset; + readonly int startVC, endVC; + + /// <summary> + /// Creates a SelectionSegment from two offsets. + /// </summary> + public SelectionSegment(int startOffset, int endOffset) + { + this.startOffset = Math.Min(startOffset, endOffset); + this.endOffset = Math.Max(startOffset, endOffset); + this.startVC = this.endVC = -1; + } + + /// <summary> + /// Creates a SelectionSegment from two offsets and visual columns. + /// </summary> + public SelectionSegment(int startOffset, int startVC, int endOffset, int endVC) + { + if (startOffset < endOffset || (startOffset == endOffset && startVC <= endVC)) { + this.startOffset = startOffset; + this.startVC = startVC; + this.endOffset = endOffset; + this.endVC = endVC; + } else { + this.startOffset = endOffset; + this.startVC = endVC; + this.endOffset = startOffset; + this.endVC = startVC; + } + } + + /// <summary> + /// Gets the start offset. + /// </summary> + public int StartOffset { + get { return startOffset; } + } + + /// <summary> + /// Gets the end offset. + /// </summary> + public int EndOffset { + get { return endOffset; } + } + + /// <summary> + /// Gets the start visual column. + /// </summary> + public int StartVisualColumn { + get { return startVC; } + } + + /// <summary> + /// Gets the end visual column. + /// </summary> + public int EndVisualColumn { + get { return endVC; } + } + + /// <inheritdoc/> + int ISegment.Offset { + get { return startOffset; } + } + + /// <inheritdoc/> + public int Length { + get { return endOffset - startOffset; } + } + + /// <inheritdoc/> + public override string ToString() + { + return string.Format("[SelectionSegment StartOffset={0}, EndOffset={1}, StartVC={2}, EndVC={3}]", startOffset, endOffset, startVC, endVC); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SimpleSelection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SimpleSelection.cs new file mode 100644 index 000000000..132b09e20 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/SimpleSelection.cs @@ -0,0 +1,144 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Linq; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// A simple selection. + /// </summary> + sealed class SimpleSelection : Selection + { + readonly TextViewPosition start, end; + readonly int startOffset, endOffset; + + /// <summary> + /// Creates a new SimpleSelection instance. + /// </summary> + internal SimpleSelection(TextArea textArea, TextViewPosition start, TextViewPosition end) + : base(textArea) + { + this.start = start; + this.end = end; + this.startOffset = textArea.Document.GetOffset(start.Location); + this.endOffset = textArea.Document.GetOffset(end.Location); + } + + /// <inheritdoc/> + public override IEnumerable<SelectionSegment> Segments { + get { + return ExtensionMethods.Sequence<SelectionSegment>(new SelectionSegment(startOffset, start.VisualColumn, endOffset, end.VisualColumn)); + } + } + + /// <inheritdoc/> + public override ISegment SurroundingSegment { + get { + return new SelectionSegment(startOffset, endOffset); + } + } + + /// <inheritdoc/> + public override void ReplaceSelectionWithText(string newText) + { + if (newText == null) + throw new ArgumentNullException("newText"); + using (textArea.Document.RunUpdate()) { + ISegment[] segmentsToDelete = textArea.GetDeletableSegments(this.SurroundingSegment); + for (int i = segmentsToDelete.Length - 1; i >= 0; i--) { + if (i == segmentsToDelete.Length - 1) { + if (segmentsToDelete[i].Offset == SurroundingSegment.Offset && segmentsToDelete[i].Length == SurroundingSegment.Length) { + newText = AddSpacesIfRequired(newText, start, end); + } + int vc = textArea.Caret.VisualColumn; + textArea.Caret.Offset = segmentsToDelete[i].EndOffset; + if (string.IsNullOrEmpty(newText)) + textArea.Caret.VisualColumn = vc; + textArea.Document.Replace(segmentsToDelete[i], newText); + } else { + textArea.Document.Remove(segmentsToDelete[i]); + } + } + if (segmentsToDelete.Length != 0) { + textArea.ClearSelection(); + } + } + } + + public override TextViewPosition StartPosition { + get { return start; } + } + + public override TextViewPosition EndPosition { + get { return end; } + } + + /// <inheritdoc/> + public override Selection UpdateOnDocumentChange(DocumentChangeEventArgs e) + { + if (e == null) + throw new ArgumentNullException("e"); + return Selection.Create( + textArea, + new TextViewPosition(textArea.Document.GetLocation(e.GetNewOffset(startOffset, AnchorMovementType.Default)), start.VisualColumn), + new TextViewPosition(textArea.Document.GetLocation(e.GetNewOffset(endOffset, AnchorMovementType.Default)), end.VisualColumn) + ); + } + + /// <inheritdoc/> + public override bool IsEmpty { + get { return startOffset == endOffset; } + } + + /// <inheritdoc/> + public override int Length { + get { + return Math.Abs(endOffset - startOffset); + } + } + + /// <inheritdoc/> + public override Selection SetEndpoint(TextViewPosition endPosition) + { + return Create(textArea, start, endPosition); + } + + public override Selection StartSelectionOrSetEndpoint(TextViewPosition startPosition, TextViewPosition endPosition) + { + var document = textArea.Document; + if (document == null) + throw ThrowUtil.NoDocumentAssigned(); + return Create(textArea, start, endPosition); + } + + /// <inheritdoc/> + public override int GetHashCode() + { + unchecked { + return startOffset * 27811 + endOffset + textArea.GetHashCode(); + } + } + + /// <inheritdoc/> + public override bool Equals(object obj) + { + SimpleSelection other = obj as SimpleSelection; + if (other == null) return false; + return this.start.Equals(other.start) && this.end.Equals(other.end) + && this.startOffset == other.startOffset && this.endOffset == other.endOffset + && this.textArea == other.textArea; + } + + /// <inheritdoc/> + public override string ToString() + { + return "[SimpleSelection Start=" + start + " End=" + end + "]"; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextArea.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextArea.cs new file mode 100644 index 000000000..393a3e29c --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextArea.cs @@ -0,0 +1,1048 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +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.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using System.Windows.Threading; + +using Tango.Scripting.Editors.Document; +using Tango.Scripting.Editors.Indentation; +using Tango.Scripting.Editors.Rendering; +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Control that wraps a TextView and adds support for user input and the caret. + /// </summary> + 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)); + } + + /// <summary> + /// Creates a new TextArea instance. + /// </summary> + public TextArea() : this(new TextView()) + { + } + + /// <summary> + /// Creates a new TextArea instance. + /// </summary> + 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(); + ime = new ImeSupport(this); + + leftMargins.CollectionChanged += leftMargins_CollectionChanged; + + this.DefaultInputHandler = new TextAreaDefaultInputHandler(this); + this.ActiveInputHandler = this.DefaultInputHandler; + } + #endregion + + #region InputHandler management + /// <summary> + /// Gets the default input handler. + /// </summary> + /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks> + public TextAreaDefaultInputHandler DefaultInputHandler { get; private set; } + + ITextAreaInputHandler activeInputHandler; + bool isChangingInputHandler; + + /// <summary> + /// Gets/Sets the active input handler. + /// This property does not return currently active stacked input handlers. Setting this property detached all stacked input handlers. + /// </summary> + /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks> + 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); + } + } + } + + /// <summary> + /// Occurs when the ActiveInputHandler property changes. + /// </summary> + public event EventHandler ActiveInputHandlerChanged; + + ImmutableStack<TextAreaStackedInputHandler> stackedInputHandlers = ImmutableStack<TextAreaStackedInputHandler>.Empty; + + /// <summary> + /// Gets the list of currently active stacked input handlers. + /// </summary> + /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks> + public ImmutableStack<TextAreaStackedInputHandler> StackedInputHandlers { + get { return stackedInputHandlers; } + } + + /// <summary> + /// Pushes an input handler onto the list of stacked input handlers. + /// </summary> + /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks> + public void PushStackedInputHandler(TextAreaStackedInputHandler inputHandler) + { + if (inputHandler == null) + throw new ArgumentNullException("inputHandler"); + stackedInputHandlers = stackedInputHandlers.Push(inputHandler); + inputHandler.Attach(); + } + + /// <summary> + /// Pops the stacked input handler (and all input handlers above it). + /// If <paramref name="inputHandler"/> is not found in the currently stacked input handlers, or is null, this method + /// does nothing. + /// </summary> + /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks> + 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 + /// <summary> + /// Document property. + /// </summary> + public static readonly DependencyProperty DocumentProperty + = TextView.DocumentProperty.AddOwner(typeof(TextArea), new FrameworkPropertyMetadata(OnDocumentChanged)); + + /// <summary> + /// Gets/Sets the document displayed by the text editor. + /// </summary> + public TextDocument Document { + get { return (TextDocument)GetValue(DocumentProperty); } + set { SetValue(DocumentProperty, value); } + } + + /// <inheritdoc/> + 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 + /// <summary> + /// Options property. + /// </summary> + public static readonly DependencyProperty OptionsProperty + = TextView.OptionsProperty.AddOwner(typeof(TextArea), new FrameworkPropertyMetadata(OnOptionsChanged)); + + /// <summary> + /// Gets/Sets the document displayed by the text editor. + /// </summary> + public TextEditorOptions Options { + get { return (TextEditorOptions)GetValue(OptionsProperty); } + set { SetValue(OptionsProperty, value); } + } + + /// <summary> + /// Occurs when a text editor option has changed. + /// </summary> + public event PropertyChangedEventHandler OptionChanged; + + /// <summary> + /// Raises the <see cref="OptionChanged"/> event. + /// </summary> + 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 + /// <inheritdoc cref="IWeakEventListener.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; + + /// <summary> + /// Gets the text view used to display text in this text area. + /// </summary> + public TextView TextView { + get { + return textView; + } + } + /// <inheritdoc/> + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + scrollInfo = textView; + ApplyScrollInfo(); + } + #endregion + + #region Selection property + internal readonly Selection emptySelection; + Selection selection; + + /// <summary> + /// Occurs when the selection has changed. + /// </summary> + public event EventHandler SelectionChanged; + + /// <summary> + /// Gets/Sets the selection in this text area. + /// </summary> + + 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(); + } + } + } + + /// <summary> + /// Clears the current selection. + /// </summary> + public void ClearSelection() + { + this.Selection = emptySelection; + } + + /// <summary> + /// The <see cref="SelectionBrush"/> property. + /// </summary> + public static readonly DependencyProperty SelectionBrushProperty = + DependencyProperty.Register("SelectionBrush", typeof(Brush), typeof(TextArea)); + + /// <summary> + /// Gets/Sets the background brush used for the selection. + /// </summary> + public Brush SelectionBrush { + get { return (Brush)GetValue(SelectionBrushProperty); } + set { SetValue(SelectionBrushProperty, value); } + } + + /// <summary> + /// The <see cref="SelectionForeground"/> property. + /// </summary> + public static readonly DependencyProperty SelectionForegroundProperty = + DependencyProperty.Register("SelectionForeground", typeof(Brush), typeof(TextArea)); + + /// <summary> + /// Gets/Sets the foreground brush used selected text. + /// </summary> + public Brush SelectionForeground { + get { return (Brush)GetValue(SelectionForegroundProperty); } + set { SetValue(SelectionForegroundProperty, value); } + } + + /// <summary> + /// The <see cref="SelectionBorder"/> property. + /// </summary> + public static readonly DependencyProperty SelectionBorderProperty = + DependencyProperty.Register("SelectionBorder", typeof(Pen), typeof(TextArea)); + + /// <summary> + /// Gets/Sets the background brush used for the selection. + /// </summary> + public Pen SelectionBorder { + get { return (Pen)GetValue(SelectionBorderProperty); } + set { SetValue(SelectionBorderProperty, value); } + } + + /// <summary> + /// The <see cref="SelectionCornerRadius"/> property. + /// </summary> + public static readonly DependencyProperty SelectionCornerRadiusProperty = + DependencyProperty.Register("SelectionCornerRadius", typeof(double), typeof(TextArea), + new FrameworkPropertyMetadata(3.0)); + + /// <summary> + /// Gets/Sets the corner radius of the selection. + /// </summary> + 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)); + } + } + + /// <summary> + /// 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. + /// </summary> + void EnsureSelectionValid() + { + ensureSelectionValidRequested = false; + if (allowCaretOutsideSelection == 0) { + if (!selection.IsEmpty && !selection.Contains(caret.Offset)) { + Debug.WriteLine("Resetting selection because caret is outside"); + this.ClearSelection(); + } + } + } + + /// <summary> + /// Temporarily allows positioning the caret outside the selection. + /// Dispose the returned IDisposable to revert the allowance. + /// </summary> + /// <remarks> + /// 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. + /// </remarks> + public IDisposable AllowCaretOutsideSelection() + { + VerifyAccess(); + allowCaretOutsideSelection++; + return new CallbackOnDispose( + delegate { + VerifyAccess(); + allowCaretOutsideSelection--; + RequestSelectionValidation(); + }); + } + #endregion + + #region Properties + readonly Caret caret; + + /// <summary> + /// Gets the Caret used for this text area. + /// </summary> + public Caret Caret { + get { return caret; } + } + + ObservableCollection<UIElement> leftMargins = new ObservableCollection<UIElement>(); + + /// <summary> + /// Gets the collection of margins displayed to the left of the text view. + /// </summary> + public ObservableCollection<UIElement> LeftMargins { + get { + return leftMargins; + } + } + + void leftMargins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) { + foreach (ITextViewConnect c in e.OldItems.OfType<ITextViewConnect>()) { + c.RemoveFromTextView(textView); + } + } + if (e.NewItems != null) { + foreach (ITextViewConnect c in e.NewItems.OfType<ITextViewConnect>()) { + c.AddToTextView(textView); + } + } + } + + IReadOnlySectionProvider readOnlySectionProvider = NoReadOnlySections.Instance; + + /// <summary> + /// Gets/Sets an object that provides read-only sections for the text area. + /// </summary> + 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) + /// <inheritdoc/> + protected override void OnMouseDown(MouseButtonEventArgs e) + { + base.OnMouseDown(e); + Focus(); + } + + /// <inheritdoc/> + protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + base.OnGotKeyboardFocus(e); + // First activate IME, then show caret + ime.OnGotKeyboardFocus(e); + caret.Show(); + } + + /// <inheritdoc/> + protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + base.OnLostKeyboardFocus(e); + caret.Hide(); + ime.OnLostKeyboardFocus(e); + } + #endregion + + #region OnTextInput / RemoveSelectedText / ReplaceSelectionWithText + /// <summary> + /// Occurs when the TextArea receives text input. + /// This is like the <see cref="UIElement.TextInput"/> event, + /// but occurs immediately before the TextArea handles the TextInput event. + /// </summary> + public event TextCompositionEventHandler TextEntering; + + /// <summary> + /// Occurs when the TextArea receives text input. + /// This is like the <see cref="UIElement.TextInput"/> event, + /// but occurs immediately after the TextArea handles the TextInput event. + /// </summary> + public event TextCompositionEventHandler TextEntered; + + /// <summary> + /// Raises the TextEntering event. + /// </summary> + protected virtual void OnTextEntering(TextCompositionEventArgs e) + { + if (TextEntering != null) { + TextEntering(this, e); + } + } + + /// <summary> + /// Raises the TextEntered event. + /// </summary> + protected virtual void OnTextEntered(TextCompositionEventArgs e) + { + if (TextEntered != null) { + TextEntered(this, e); + } + } + + /// <inheritdoc/> + 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; + } + PerformTextInput(e); + e.Handled = true; + } + } + + /// <summary> + /// Performs text input. + /// This raises the <see cref="TextEntering"/> event, replaces the selection with the text, + /// and then raises the <see cref="TextEntered"/> event. + /// </summary> + 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); + } + + /// <summary> + /// Performs text input. + /// This raises the <see cref="TextEntering"/> event, replaces the selection with the text, + /// and then raises the <see cref="TextEntered"/> event. + /// </summary> + 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 + 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 + /// <summary> + /// IndentationStrategy property. + /// </summary> + public static readonly DependencyProperty IndentationStrategyProperty = + DependencyProperty.Register("IndentationStrategy", typeof(IIndentationStrategy), typeof(TextArea), + new FrameworkPropertyMetadata(new DefaultIndentationStrategy())); + + /// <summary> + /// Gets/Sets the indentation strategy used when inserting new lines. + /// </summary> + public IIndentationStrategy IndentationStrategy { + get { return (IIndentationStrategy)GetValue(IndentationStrategyProperty); } + set { SetValue(IndentationStrategyProperty, value); } + } + #endregion + + #region OnKeyDown/OnKeyUp + /// <inheritdoc/> + protected override void OnPreviewKeyDown(KeyEventArgs e) + { + base.OnPreviewKeyDown(e); + foreach (TextAreaStackedInputHandler h in stackedInputHandlers) { + if (e.Handled) + break; + h.OnPreviewKeyDown(e); + } + } + + /// <inheritdoc/> + 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. + /// <inheritdoc/> + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + TextView.InvalidateCursor(); + } + + /// <inheritdoc/> + protected override void OnKeyUp(KeyEventArgs e) + { + base.OnKeyUp(e); + TextView.InvalidateCursor(); + } + #endregion + + /// <inheritdoc/> + protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) + { + // accept clicks even where the text area draws no background + return new PointHitTestResult(this, hitTestParameters.HitPoint); + } + + /// <inheritdoc/> + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + if (e.Property == SelectionBrushProperty + || e.Property == SelectionBorderProperty + || e.Property == SelectionForegroundProperty + || e.Property == SelectionCornerRadiusProperty) + { + textView.Redraw(); + } + } + + /// <summary> + /// Gets the requested service. + /// </summary> + /// <returns>Returns the requested service instance, or null if the service cannot be found.</returns> + public virtual object GetService(Type serviceType) + { + return textView.Services.GetService(serviceType); + } + + /// <summary> + /// Occurs when text inside the TextArea was copied. + /// </summary> + public event EventHandler<TextEventArgs> TextCopied; + + internal void OnTextCopied(TextEventArgs e) + { + if (TextCopied != null) + TextCopied(this, e); + } + } + + /// <summary> + /// EventArgs with text. + /// </summary> + [Serializable] + public class TextEventArgs : EventArgs + { + string text; + + /// <summary> + /// Gets the text. + /// </summary> + public string Text { + get { + return text; + } + } + + /// <summary> + /// Creates a new TextEventArgs instance. + /// </summary> + public TextEventArgs(string text) + { + if (text == null) + throw new ArgumentNullException("text"); + this.text = text; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextAreaDefaultInputHandlers.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextAreaDefaultInputHandlers.cs new file mode 100644 index 000000000..7101d16c8 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextAreaDefaultInputHandlers.cs @@ -0,0 +1,120 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Input; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Contains the predefined input handlers. + /// </summary> + public class TextAreaDefaultInputHandler : TextAreaInputHandler + { + /// <summary> + /// Gets the caret navigation input handler. + /// </summary> + public TextAreaInputHandler CaretNavigation { get; private set; } + + /// <summary> + /// Gets the editing input handler. + /// </summary> + public TextAreaInputHandler Editing { get; private set; } + + /// <summary> + /// Gets the mouse selection input handler. + /// </summary> + public ITextAreaInputHandler MouseSelection { get; private set; } + + /// <summary> + /// Creates a new TextAreaDefaultInputHandler instance. + /// </summary> + public TextAreaDefaultInputHandler(TextArea textArea) : base(textArea) + { + this.NestedInputHandlers.Add(CaretNavigation = CaretNavigationCommandHandler.Create(textArea)); + this.NestedInputHandlers.Add(Editing = EditingCommandHandler.Create(textArea)); + this.NestedInputHandlers.Add(MouseSelection = new SelectionMouseHandler(textArea)); + + this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, ExecuteUndo, CanExecuteUndo)); + this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Redo, ExecuteRedo, CanExecuteRedo)); + } + + internal static KeyBinding CreateFrozenKeyBinding(ICommand command, ModifierKeys modifiers, Key key) + { + KeyBinding kb = new KeyBinding(command, key, modifiers); + // Mark KeyBindings as frozen because they're shared between multiple editor instances. + // KeyBinding derives from Freezable only in .NET 4, so we have to use this little trick: + Freezable f = ((object)kb) as Freezable; + if (f != null) + f.Freeze(); + return kb; + } + + internal static void WorkaroundWPFMemoryLeak(List<InputBinding> inputBindings) + { + // Work around WPF memory leak: + // KeyBinding retains a reference to whichever UIElement it is used in first. + // Using a dummy element for this purpose ensures that we don't leak + // a real text editor (with a potentially large document). + UIElement dummyElement = new UIElement(); + dummyElement.InputBindings.AddRange(inputBindings); + } + + #region Undo / Redo + UndoStack GetUndoStack() + { + TextDocument document = this.TextArea.Document; + if (document != null) + return document.UndoStack; + else + return null; + } + + void ExecuteUndo(object sender, ExecutedRoutedEventArgs e) + { + var undoStack = GetUndoStack(); + if (undoStack != null) { + if (undoStack.CanUndo) { + undoStack.Undo(); + this.TextArea.Caret.BringCaretToView(); + } + e.Handled = true; + } + } + + void CanExecuteUndo(object sender, CanExecuteRoutedEventArgs e) + { + var undoStack = GetUndoStack(); + if (undoStack != null) { + e.Handled = true; + e.CanExecute = undoStack.CanUndo; + } + } + + void ExecuteRedo(object sender, ExecutedRoutedEventArgs e) + { + var undoStack = GetUndoStack(); + if (undoStack != null) { + if (undoStack.CanRedo) { + undoStack.Redo(); + this.TextArea.Caret.BringCaretToView(); + } + e.Handled = true; + } + } + + void CanExecuteRedo(object sender, CanExecuteRoutedEventArgs e) + { + var undoStack = GetUndoStack(); + if (undoStack != null) { + e.Handled = true; + e.CanExecute = undoStack.CanRedo; + } + } + #endregion + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextAreaInputHandler.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextAreaInputHandler.cs new file mode 100644 index 000000000..3256875be --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextAreaInputHandler.cs @@ -0,0 +1,242 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using Tango.Scripting.Editors.Utils; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Windows.Input; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// A set of input bindings and event handlers for the text area. + /// </summary> + /// <remarks> + /// <para> + /// There is one active input handler per text area (<see cref="Editing.TextArea.ActiveInputHandler"/>), plus + /// a number of active stacked input handlers. + /// </para> + /// <para> + /// The text area also stores a reference to a default input handler, but that is not necessarily active. + /// </para> + /// <para> + /// Stacked input handlers work in addition to the set of currently active handlers (without detaching them). + /// They are detached in the reverse order of being attached. + /// </para> + /// </remarks> + public interface ITextAreaInputHandler + { + /// <summary> + /// Gets the text area that the input handler belongs to. + /// </summary> + TextArea TextArea { + get; + } + + /// <summary> + /// Attaches an input handler to the text area. + /// </summary> + void Attach(); + + /// <summary> + /// Detaches the input handler from the text area. + /// </summary> + void Detach(); + } + + /// <summary> + /// Stacked input handler. + /// Uses OnEvent-methods instead of registering event handlers to ensure that the events are handled in the correct order. + /// </summary> + public abstract class TextAreaStackedInputHandler : ITextAreaInputHandler + { + readonly TextArea textArea; + + /// <inheritdoc/> + public TextArea TextArea { + get { return textArea; } + } + + /// <summary> + /// Creates a new TextAreaInputHandler. + /// </summary> + protected TextAreaStackedInputHandler(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.textArea = textArea; + } + + /// <inheritdoc/> + public virtual void Attach() + { + } + + /// <inheritdoc/> + public virtual void Detach() + { + } + + /// <summary> + /// Called for the PreviewKeyDown event. + /// </summary> + public virtual void OnPreviewKeyDown(KeyEventArgs e) + { + } + + /// <summary> + /// Called for the PreviewKeyUp event. + /// </summary> + public virtual void OnPreviewKeyUp(KeyEventArgs e) + { + } + } + + /// <summary> + /// Default-implementation of <see cref="ITextAreaInputHandler"/>. + /// </summary> + /// <remarks><inheritdoc cref="ITextAreaInputHandler"/></remarks> + public class TextAreaInputHandler : ITextAreaInputHandler + { + readonly ObserveAddRemoveCollection<CommandBinding> commandBindings; + readonly ObserveAddRemoveCollection<InputBinding> inputBindings; + readonly ObserveAddRemoveCollection<ITextAreaInputHandler> nestedInputHandlers; + readonly TextArea textArea; + bool isAttached; + + /// <summary> + /// Creates a new TextAreaInputHandler. + /// </summary> + public TextAreaInputHandler(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.textArea = textArea; + commandBindings = new ObserveAddRemoveCollection<CommandBinding>(CommandBinding_Added, CommandBinding_Removed); + inputBindings = new ObserveAddRemoveCollection<InputBinding>(InputBinding_Added, InputBinding_Removed); + nestedInputHandlers = new ObserveAddRemoveCollection<ITextAreaInputHandler>(NestedInputHandler_Added, NestedInputHandler_Removed); + } + + /// <inheritdoc/> + public TextArea TextArea { + get { return textArea; } + } + + /// <summary> + /// Gets whether the input handler is currently attached to the text area. + /// </summary> + public bool IsAttached { + get { return isAttached; } + } + + #region CommandBindings / InputBindings + /// <summary> + /// Gets the command bindings of this input handler. + /// </summary> + public ICollection<CommandBinding> CommandBindings { + get { return commandBindings; } + } + + void CommandBinding_Added(CommandBinding commandBinding) + { + if (isAttached) + textArea.CommandBindings.Add(commandBinding); + } + + void CommandBinding_Removed(CommandBinding commandBinding) + { + if (isAttached) + textArea.CommandBindings.Remove(commandBinding); + } + + /// <summary> + /// Gets the input bindings of this input handler. + /// </summary> + public ICollection<InputBinding> InputBindings { + get { return inputBindings; } + } + + void InputBinding_Added(InputBinding inputBinding) + { + if (isAttached) + textArea.InputBindings.Add(inputBinding); + } + + void InputBinding_Removed(InputBinding inputBinding) + { + if (isAttached) + textArea.InputBindings.Remove(inputBinding); + } + + /// <summary> + /// Adds a command and input binding. + /// </summary> + /// <param name="command">The command ID.</param> + /// <param name="modifiers">The modifiers of the keyboard shortcut.</param> + /// <param name="key">The key of the keyboard shortcut.</param> + /// <param name="handler">The event handler to run when the command is executed.</param> + public void AddBinding(ICommand command, ModifierKeys modifiers, Key key, ExecutedRoutedEventHandler handler) + { + this.CommandBindings.Add(new CommandBinding(command, handler)); + this.InputBindings.Add(new KeyBinding(command, key, modifiers)); + } + #endregion + + #region NestedInputHandlers + /// <summary> + /// Gets the collection of nested input handlers. NestedInputHandlers are activated and deactivated + /// together with this input handler. + /// </summary> + public ICollection<ITextAreaInputHandler> NestedInputHandlers { + get { return nestedInputHandlers; } + } + + void NestedInputHandler_Added(ITextAreaInputHandler handler) + { + if (handler == null) + throw new ArgumentNullException("handler"); + if (handler.TextArea != textArea) + throw new ArgumentException("The nested handler must be working for the same text area!"); + if (isAttached) + handler.Attach(); + } + + void NestedInputHandler_Removed(ITextAreaInputHandler handler) + { + if (isAttached) + handler.Detach(); + } + #endregion + + #region Attach/Detach + /// <inheritdoc/> + public virtual void Attach() + { + if (isAttached) + throw new InvalidOperationException("Input handler is already attached"); + isAttached = true; + + textArea.CommandBindings.AddRange(commandBindings); + textArea.InputBindings.AddRange(inputBindings); + foreach (ITextAreaInputHandler handler in nestedInputHandlers) + handler.Attach(); + } + + /// <inheritdoc/> + public virtual void Detach() + { + if (!isAttached) + throw new InvalidOperationException("Input handler is not attached"); + isAttached = false; + + foreach (CommandBinding b in commandBindings) + textArea.CommandBindings.Remove(b); + foreach (InputBinding b in inputBindings) + textArea.InputBindings.Remove(b); + foreach (ITextAreaInputHandler handler in nestedInputHandlers) + handler.Detach(); + } + #endregion + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextSegmentReadOnlySectionProvider.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextSegmentReadOnlySectionProvider.cs new file mode 100644 index 000000000..c582301d4 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Editing/TextSegmentReadOnlySectionProvider.cs @@ -0,0 +1,80 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Editing +{ + /// <summary> + /// Implementation for <see cref="IReadOnlySectionProvider"/> that stores the segments + /// in a <see cref="TextSegmentCollection{T}"/>. + /// </summary> + public class TextSegmentReadOnlySectionProvider<T> : IReadOnlySectionProvider where T : TextSegment + { + readonly TextSegmentCollection<T> segments; + + /// <summary> + /// Gets the collection storing the read-only segments. + /// </summary> + public TextSegmentCollection<T> Segments { + get { return segments; } + } + + /// <summary> + /// Creates a new TextSegmentReadOnlySectionProvider instance for the specified document. + /// </summary> + public TextSegmentReadOnlySectionProvider(TextDocument textDocument) + { + segments = new TextSegmentCollection<T>(textDocument); + } + + /// <summary> + /// Creates a new TextSegmentReadOnlySectionProvider instance using the specified TextSegmentCollection. + /// </summary> + public TextSegmentReadOnlySectionProvider(TextSegmentCollection<T> segments) + { + if (segments == null) + throw new ArgumentNullException("segments"); + this.segments = segments; + } + + /// <summary> + /// Gets whether insertion is possible at the specified offset. + /// </summary> + public virtual bool CanInsert(int offset) + { + foreach (TextSegment segment in segments.FindSegmentsContaining(offset)) { + if (segment.StartOffset < offset && offset < segment.EndOffset) + return false; + } + return true; + } + + /// <summary> + /// Gets the deletable segments inside the given segment. + /// </summary> + public virtual IEnumerable<ISegment> GetDeletableSegments(ISegment segment) + { + if (segment == null) + throw new ArgumentNullException("segment"); + + int readonlyUntil = segment.Offset; + foreach (TextSegment ts in segments.FindOverlappingSegments(segment)) { + int start = ts.StartOffset; + int end = start + ts.Length; + if (start > readonlyUntil) { + yield return new SimpleSegment(readonlyUntil, start - readonlyUntil); + } + if (end > readonlyUntil) { + readonlyUntil = end; + } + } + int endOffset = segment.EndOffset; + if (readonlyUntil < endOffset) { + yield return new SimpleSegment(readonlyUntil, endOffset - readonlyUntil); + } + } + } +} |
