// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.ComponentModel; using System.Linq; using Unity.CodeEditor.Document; using Unity.CodeEditor.Utils; using UnityEngine; using UnityEngine.UIElements; namespace Unity.CodeEditor.Editing { /// /// Handles selection of text using the mouse. /// internal sealed class SelectionMouseHandler //: ITextAreaInputHandler { #region enum SelectionMode private enum SelectionMode { /// /// no selection (no mouse button down) /// None, /// /// left mouse button down on selection, might be normal click /// or might be drag'n'drop /// PossibleDragStart, /// /// dragging text /// Drag, /// /// normal selection (click+drag) /// Normal, /// /// whole-word selection (double click+drag or ctrl+click+drag) /// WholeWord, /// /// whole-line selection (triple click+drag) /// WholeLine, /// /// rectangular selection (alt+click+drag) /// Rectangular } #endregion private SelectionMode _mode; private AnchorSegment _startWord; private Vector2 _possibleDragStartMousePos; private int _consecutiveClicks; private float _lastClickTime; private Vector2 _lastClickPosition; #region Constructor + Attach + Detach internal SelectionMouseHandler(TextArea textArea) { TextArea = textArea ?? throw new ArgumentNullException(nameof(textArea)); } internal TextArea TextArea { get; } internal void Attach() { //TextArea.PointerPressed += TextArea_MouseLeftButtonDown; //TextArea.PointerMoved += TextArea_MouseMove; //TextArea.PointerReleased += TextArea_MouseLeftButtonUp; //textArea.QueryCursor += textArea_QueryCursor; TextArea.OptionChanged += TextArea_OptionChanged; _enableTextDragDrop = TextArea.Options.EnableTextDragDrop; if (_enableTextDragDrop) { AttachDragDrop(); } } internal void Detach() { _mode = SelectionMode.None; //TextArea.PointerPressed -= TextArea_MouseLeftButtonDown; //TextArea.PointerMoved -= TextArea_MouseMove; //TextArea.PointerReleased -= TextArea_MouseLeftButtonUp; //textArea.QueryCursor -= textArea_QueryCursor; TextArea.OptionChanged -= TextArea_OptionChanged; if (_enableTextDragDrop) { DetachDragDrop(); } } private 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; } private 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; } private bool _enableTextDragDrop; private void TextArea_OptionChanged(object sender, PropertyChangedEventArgs e) { var 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; // bool isAtEndOfLine; // int offset = GetOffsetFromMousePosition(e.GetPosition(textArea.TextView), out visualColumn, out isAtEndOfLine); // if (offset >= 0) { // textArea.Caret.Position = new TextViewPosition(textArea.Document.GetLocation(offset), visualColumn) { IsAtEndOfLine = isAtEndOfLine }; // 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) { // 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); // var pastingEventArgs = new DataObjectPastingEventArgs(e.Data, true, DataFormats.UnicodeText); // textArea.RaiseEvent(pastingEventArgs); // if (pastingEventArgs.CommandCancelled) // return; // string text = EditingCommandHandler.GetTextToPaste(pastingEventArgs, textArea); // if (text == null) // return; // bool rectangular = pastingEventArgs.DataObject.GetDataPresent(RectangleSelection.RectangularSelectionDataType); // // 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) //{ // // 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.Send, // 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; // } // } // var copyingEventArgs = new DataObjectCopyingEventArgs(dataObject, true); // textArea.RaiseEvent(copyingEventArgs); // if (copyingEventArgs.CommandCancelled) // return; // 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) { // // during selection, use IBeam cursor even outside the text area // 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; // bool isAtEndOfLine; // int offset = GetOffsetFromMousePosition(e, out visualColumn, out isAtEndOfLine); // if (enableTextDragDrop && textArea.Selection.Contains(offset)) // e.Cursor = Cursors.Arrow; // else // e.Cursor = Cursors.IBeam; // e.Handled = true; // } // } // } //} #endregion #region LeftButtonDown internal void OnPointerPressed(PointerDownEvent e) { Vector2 mousePoint = GetTextViewRelativePosition(e); if (e.button != 0) { if (TextArea.RightClickMovesCaret == true) { SetCaretOffsetToMousePosition(mousePoint); } } else { int clickCount = ComputeClickCount(e); _mode = SelectionMode.None; bool isShift = (e.modifiers & EventModifiers.Shift) != 0; bool isAlt = (e.modifiers & EventModifiers.Alt) != 0; bool isCtrl = (e.modifiers & EventModifiers.Control) != 0 || (e.modifiers & EventModifiers.Command) != 0; if (_enableTextDragDrop && clickCount == 1 && !isShift) { var offset = GetOffsetFromMousePosition(mousePoint, out _, out _); if (TextArea.Selection.Contains(offset)) { _mode = SelectionMode.PossibleDragStart; _possibleDragStartMousePos = mousePoint; return; } } var oldPosition = TextArea.Caret.Position; SetCaretOffsetToMousePosition(mousePoint); if (!isShift) { TextArea.ClearSelection(); } if (isAlt && TextArea.Options.EnableRectangularSelection) { _mode = SelectionMode.Rectangular; if (isShift && TextArea.Selection is RectangleSelection) { TextArea.Selection = TextArea.Selection.StartSelectionOrSetEndpoint(oldPosition, TextArea.Caret.Position); } } else if (isCtrl && clickCount == 1) { _mode = SelectionMode.WholeWord; if (isShift && !(TextArea.Selection is RectangleSelection)) { TextArea.Selection = TextArea.Selection.StartSelectionOrSetEndpoint(oldPosition, TextArea.Caret.Position); } } else if (e.button == 0 && clickCount == 1) { _mode = SelectionMode.Normal; if (isShift && !(TextArea.Selection is RectangleSelection)) { TextArea.Selection = TextArea.Selection.StartSelectionOrSetEndpoint(oldPosition, TextArea.Caret.Position); } } else { SimpleSegment startWord; if (clickCount == 3) { _mode = SelectionMode.WholeLine; startWord = GetLineAtMousePosition(mousePoint); } else { _mode = SelectionMode.WholeWord; startWord = GetWordAtMousePosition(mousePoint); } if (startWord == SimpleSegment.Invalid) { _mode = SelectionMode.None; return; } if (isShift && !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))); } _startWord = new AnchorSegment(TextArea.Document, TextArea.Selection.SurroundingSegment); } else { TextArea.Selection = Selection.Create(TextArea, startWord.Offset, startWord.EndOffset); _startWord = new AnchorSegment(TextArea.Document, startWord.Offset, startWord.Length); } } } } private int ComputeClickCount(PointerDownEvent e) { float currentTime = Time.realtimeSinceStartup; Vector2 currentPosition = e.position; if (currentTime - _lastClickTime < TripleClickTimeSeconds && Vector2.Distance(currentPosition, _lastClickPosition) < TripleClickMaxDistance) { _consecutiveClicks++; } else { _consecutiveClicks = 1; } _lastClickTime = currentTime; _lastClickPosition = currentPosition; return _consecutiveClicks; } #endregion #region LeftButtonClick #endregion #region LeftButtonDoubleTap #endregion #region Mouse Position <-> Text coordinates private Vector2 GetTextViewRelativePosition(IPointerEvent e) { return TextArea.TextView.WorldToLocal(e.position); } private SimpleSegment GetWordAtMousePosition(Vector2 posRelativeToTextView) { var textView = TextArea.TextView; if (textView == null) return SimpleSegment.Invalid; var pos = posRelativeToTextView; if (pos.y < 0) pos.y = 0; if (pos.y > textView.Bounds.height) pos.y = textView.Bounds.height; pos += textView.ScrollOffset; var line = textView.GetVisualLineFromVisualTop(pos.y); if (line != null && line.TextLines != null) { var visualColumn = line.GetVisualColumn(pos, TextArea.Selection.EnableVirtualSpace); var wordStartVc = line.GetNextCaretPosition(visualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.WordStartOrSymbol, TextArea.Selection.EnableVirtualSpace); if (wordStartVc == -1) wordStartVc = 0; var wordEndVc = line.GetNextCaretPosition(wordStartVc, LogicalDirection.Forward, CaretPositioningMode.WordBorderOrSymbol, TextArea.Selection.EnableVirtualSpace); if (wordEndVc == -1) wordEndVc = line.VisualLength; var relOffset = line.FirstDocumentLine.Offset; var wordStartOffset = line.GetRelativeOffset(wordStartVc) + relOffset; var wordEndOffset = line.GetRelativeOffset(wordEndVc) + relOffset; return new SimpleSegment(wordStartOffset, wordEndOffset - wordStartOffset); } else { return SimpleSegment.Invalid; } } private SimpleSegment GetLineAtMousePosition(Vector2 posRelativeToTextView) { var textView = TextArea.TextView; if (textView == null) return SimpleSegment.Invalid; var pos = posRelativeToTextView; if (pos.y < 0) pos.y = 0; if (pos.y > textView.Bounds.height) pos.y = textView.Bounds.height; pos += textView.ScrollOffset; var line = textView.GetVisualLineFromVisualTop(pos.y); return line != null && line.TextLines != null ? new SimpleSegment(line.StartOffset, line.LastDocumentLine.EndOffset - line.StartOffset) : SimpleSegment.Invalid; } private int GetOffsetFromMousePosition(Vector2 mousePosition, out int visualColumn, out bool isAtEndOfLine) { visualColumn = 0; var textView = TextArea.TextView; var pos = mousePosition; if (pos.y < 0) pos.y = 0; if (pos.y > textView.Bounds.height) pos.y = textView.Bounds.height; pos += textView.ScrollOffset; if (pos.y >= textView.DocumentHeight) pos.y = textView.DocumentHeight - ExtensionMethods.Epsilon; var line = textView.GetVisualLineFromVisualTop(pos.y); if (line != null && line.TextLines != null) { visualColumn = line.GetVisualColumn(pos, TextArea.Selection.EnableVirtualSpace, out isAtEndOfLine); return line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset; } isAtEndOfLine = false; return -1; } private int GetOffsetFromMousePositionFirstTextLineOnly(Vector2 positionRelativeToTextView, out int visualColumn) { visualColumn = 0; var textView = TextArea.TextView; var pos = positionRelativeToTextView; if (pos.y < 0) pos.y = 0; if (pos.y > textView.Bounds.height) pos.y = textView.Bounds.height; pos += textView.ScrollOffset; if (pos.y >= textView.DocumentHeight) pos.y = textView.DocumentHeight - ExtensionMethods.Epsilon; var line = textView.GetVisualLineFromVisualTop(pos.y); if (line != null && line.TextLines != null) { visualColumn = line.GetVisualColumn(line.TextLines.First(), pos.x, TextArea.Selection.EnableVirtualSpace); return line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset; } return -1; } #endregion private const int MinimumHorizontalDragDistance = 2; private const int MinimumVerticalDragDistance = 2; private const float TripleClickTimeSeconds = 0.5f; private const float TripleClickMaxDistance = 5f; #region MouseMove internal void OnPointerMoved(PointerMoveEvent e) { Vector2 mousePoint = GetTextViewRelativePosition(e); if (_mode == SelectionMode.Normal || _mode == SelectionMode.WholeWord || _mode == SelectionMode.WholeLine || _mode == SelectionMode.Rectangular) { if (TextArea.TextView.VisualLinesValid) { ExtendSelectionToMouse(mousePoint); } } else if (_mode == SelectionMode.PossibleDragStart) { Vector2 mouseMovement = mousePoint - _possibleDragStartMousePos; if (Math.Abs(mouseMovement.x) > MinimumHorizontalDragDistance || Math.Abs(mouseMovement.y) > MinimumVerticalDragDistance) { // TODO: drag //StartDrag(); } } } #endregion #region ExtendSelection private void SetCaretOffsetToMousePosition(Vector2 posRelativeToTextView) { int visualColumn; bool isAtEndOfLine; int offset; if (_mode == SelectionMode.Rectangular) { offset = GetOffsetFromMousePositionFirstTextLineOnly(posRelativeToTextView, out visualColumn); isAtEndOfLine = true; } else { offset = GetOffsetFromMousePosition(posRelativeToTextView, out visualColumn, out isAtEndOfLine); } if (offset >= 0) { TextArea.Caret.Position = new TextViewPosition(TextArea.Document.GetLocation(offset), visualColumn) { IsAtEndOfLine = isAtEndOfLine }; TextArea.Caret.DesiredXPos = float.NaN; } } private void ExtendSelectionToMouse(Vector2 posRelativeToTextView) { var oldPosition = TextArea.Caret.Position; if (_mode == SelectionMode.Normal || _mode == SelectionMode.Rectangular) { SetCaretOffsetToMousePosition(posRelativeToTextView); 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(posRelativeToTextView) : GetWordAtMousePosition(posRelativeToTextView); if (newWord != SimpleSegment.Invalid && _startWord != null) { TextArea.Selection = Selection.Create(TextArea, Math.Min(newWord.Offset, _startWord.Offset), Math.Max(newWord.EndOffset, _startWord.EndOffset)); TextArea.Caret.Offset = newWord.Offset < _startWord.Offset ? newWord.Offset : Math.Max(newWord.EndOffset, _startWord.EndOffset); } } TextArea.Caret.BringCaretToView(5.0f); } #endregion #region MouseLeftButtonUp internal void OnPointerReleased(PointerUpEvent e) { if (_mode == SelectionMode.None) return; Vector2 mousePoint = GetTextViewRelativePosition(e); switch (_mode) { case SelectionMode.PossibleDragStart: SetCaretOffsetToMousePosition(mousePoint); TextArea.ClearSelection(); break; case SelectionMode.Normal: case SelectionMode.WholeWord: case SelectionMode.WholeLine: case SelectionMode.Rectangular: if (TextArea.Options.ExtendSelectionOnMouseUp) ExtendSelectionToMouse(mousePoint); break; } _mode = SelectionMode.None; } #endregion } }