diff --git a/ICSharpCode.AvalonEdit/Editing/Caret.cs b/ICSharpCode.AvalonEdit/Editing/Caret.cs index 894031d0..1039f896 100644 --- a/ICSharpCode.AvalonEdit/Editing/Caret.cs +++ b/ICSharpCode.AvalonEdit/Editing/Caret.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team -// +// 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 @@ -20,9 +20,6 @@ using System.Diagnostics; using System.Windows; using System.Windows.Documents; -using System.Windows.Media; -using System.Windows.Media.TextFormatting; -using System.Windows.Threading; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Rendering; @@ -33,11 +30,10 @@ namespace ICSharpCode.AvalonEdit.Editing /// /// Helper class with caret-related methods. /// - public sealed class Caret + public sealed partial class Caret { readonly TextArea textArea; readonly TextView textView; - readonly CaretLayer caretAdorner; bool visible; internal Caret(TextArea textArea) @@ -46,12 +42,14 @@ internal Caret(TextArea textArea) this.textView = textArea.TextView; position = new TextViewPosition(1, 1, 0); - caretAdorner = new CaretLayer(textArea); - textView.InsertLayer(caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); textView.VisualLinesChanged += TextView_VisualLinesChanged; textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged; + + Initialize(); } + partial void Initialize(); + internal void UpdateIfVisible() { if (visible) { @@ -72,11 +70,11 @@ void TextView_VisualLinesChanged(object sender, EventArgs e) void TextView_ScrollOffsetChanged(object sender, EventArgs e) { - if (caretAdorner != null) { - caretAdorner.InvalidateVisual(); - } + InvalidateCaretVisual(); } + partial void InvalidateCaretVisual(); + double desiredXPos = double.NaN; TextViewPosition position; @@ -371,56 +369,20 @@ void RevalidateVisualColumn(VisualLine visualLine) isInVirtualSpace = (position.VisualColumn > visualLine.VisualLength); } - Rect CalcCaretRectangle(VisualLine visualLine) - { - if (!visualColumnValid) { - RevalidateVisualColumn(visualLine); - } - - TextLine textLine = visualLine.GetTextLine(position.VisualColumn, position.IsAtEndOfLine); - 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); - } + partial void GetCaretWidthCore(ref double width); - Rect CalcCaretOverstrikeRectangle(VisualLine visualLine) + double GetCaretWidth() { - if (!visualColumnValid) { - RevalidateVisualColumn(visualLine); - } - - int currentPos = position.VisualColumn; - // The text being overwritten in overstrike mode is everything up to the next normal caret stop - int nextPos = visualLine.GetNextCaretPosition(currentPos, LogicalDirection.Forward, CaretPositioningMode.Normal, true); - TextLine textLine = visualLine.GetTextLine(currentPos); - - Rect r; - if (currentPos < visualLine.VisualLength) { - // If the caret is within the text, use GetTextBounds() for the text being overwritten. - // This is necessary to ensure the rectangle is calculated correctly in bidirectional text. - var textBounds = textLine.GetTextBounds(currentPos, nextPos - currentPos)[0]; - r = textBounds.Rectangle; - r.Y += visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop); - } else { - // If the caret is at the end of the line (or in virtual space), - // use the visual X position of currentPos and nextPos (one or more of which will be in virtual space) - double xPos = visualLine.GetTextLineVisualXPosition(textLine, currentPos); - double xPos2 = visualLine.GetTextLineVisualXPosition(textLine, nextPos); - double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); - double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); - r = new Rect(xPos, lineTop, xPos2 - xPos, lineBottom - lineTop); - } - // If the caret is too small (e.g. in front of zero-width character), ensure it's still visible - if (r.Width < SystemParameters.CaretWidth) - r.Width = SystemParameters.CaretWidth; - return r; + double width = 1.0; + GetCaretWidthCore(ref width); + return width; } + // These methods use System.Windows.Media.TextFormatting.TextLine (WPF TextFormatting API). + // Implementations live in Caret.wpf.cs (WPF) and Caret.uno.cs (Uno). + private partial Rect CalcCaretRectangle(VisualLine visualLine); + private partial Rect CalcCaretOverstrikeRectangle(VisualLine visualLine); + /// /// Returns the caret rectangle. The coordinate system is in device-independent pixels from the top of the document. /// @@ -465,10 +427,18 @@ public void Show() visible = true; if (!showScheduled) { showScheduled = true; - textArea.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(ShowInternal)); + // ShowCaretAsync (wpf) clears showScheduled and posts a dispatcher call to ShowInternal. + // If ShowCaretAsync has no implementation (Uno), showScheduled stays true and we fall + // through to call ShowInternal synchronously. + ShowCaretAsync(); + if (showScheduled) { + ShowInternal(); + } } } + partial void ShowCaretAsync(); + bool showScheduled; bool hasWin32Caret; @@ -480,26 +450,20 @@ void ShowInternal() if (!visible) return; - if (caretAdorner != null && textView != null) { + if (textView != null) { VisualLine visualLine = textView.GetVisualLine(position.Line); if (visualLine != null) { Rect caretRect = this.textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine) : 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(); + ShowCaretInternal(caretRect); } else { - caretAdorner.Hide(); + HideCaretInternal(); } } } + partial void ShowCaretInternal(Rect caretRect); + partial void HideCaretInternal(); + /// /// Makes the caret invisible. /// @@ -507,28 +471,17 @@ public void Hide() { Log("Caret.Hide()"); visible = false; - if (hasWin32Caret) { - Win32.DestroyCaret(); - hasWin32Caret = false; - } - if (caretAdorner != null) { - caretAdorner.Hide(); - } + DestroyWin32Caret(); + HideCaretInternal(); } + partial void DestroyWin32Caret(); + [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); } - - /// - /// Gets/Sets the color of the caret. - /// - public Brush CaretBrush { - get { return caretAdorner.CaretBrush; } - set { caretAdorner.CaretBrush = value; } - } } } diff --git a/ICSharpCode.AvalonEdit/Editing/Caret.wpf.cs b/ICSharpCode.AvalonEdit/Editing/Caret.wpf.cs new file mode 100644 index 00000000..015f2432 --- /dev/null +++ b/ICSharpCode.AvalonEdit/Editing/Caret.wpf.cs @@ -0,0 +1,137 @@ +// 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.Windows; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; +using System.Windows.Threading; + +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit.Editing +{ + public sealed partial class Caret + { + CaretLayer caretAdorner; + + partial void Initialize() + { + caretAdorner = new CaretLayer(textArea); + textView.InsertLayer(caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); + } + + partial void InvalidateCaretVisual() + { + if (caretAdorner != null) { + caretAdorner.InvalidateVisual(); + } + } + + partial void ShowCaretAsync() + { + // Clear showScheduled now so Show()'s fallback guard does not fire synchronously. + // The dispatcher callback will call ShowInternal() when it runs. + showScheduled = false; + textArea.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(ShowInternal)); + } + + partial void ShowCaretInternal(Rect caretRect) + { + if (caretAdorner != null) { + if (!hasWin32Caret) { + hasWin32Caret = Win32.CreateCaret(textView, caretRect.Size); + } + if (hasWin32Caret) { + Win32.SetCaretPosition(textView, caretRect.Location - textView.ScrollOffset); + } + caretAdorner.Show(caretRect); + textArea.ime.UpdateCompositionWindow(); + } + } + + partial void HideCaretInternal() + { + if (caretAdorner != null) { + caretAdorner.Hide(); + } + } + + partial void DestroyWin32Caret() + { + if (hasWin32Caret) { + Win32.DestroyCaret(); + hasWin32Caret = false; + } + } + + private partial Rect CalcCaretRectangle(VisualLine visualLine) + { + if (!visualColumnValid) { + RevalidateVisualColumn(visualLine); + } + TextLine textLine = visualLine.GetTextLine(position.VisualColumn, position.IsAtEndOfLine); + 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, GetCaretWidth(), lineBottom - lineTop); + } + + partial void GetCaretWidthCore(ref double width) + { + width = SystemParameters.CaretWidth; + } + + private partial Rect CalcCaretOverstrikeRectangle(VisualLine visualLine) + { + if (!visualColumnValid) { + RevalidateVisualColumn(visualLine); + } + int currentPos = position.VisualColumn; + int nextPos = visualLine.GetNextCaretPosition(currentPos, LogicalDirection.Forward, CaretPositioningMode.Normal, true); + TextLine textLine = visualLine.GetTextLine(currentPos); + Rect r; + if (currentPos < visualLine.VisualLength) { + var textBounds = textLine.GetTextBounds(currentPos, nextPos - currentPos)[0]; + r = textBounds.Rectangle; + r.Y += visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop); + } else { + double xPos = visualLine.GetTextLineVisualXPosition(textLine, currentPos); + double xPos2 = visualLine.GetTextLineVisualXPosition(textLine, nextPos); + double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); + double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); + r = new Rect(xPos, lineTop, xPos2 - xPos, lineBottom - lineTop); + } + double caretWidth = GetCaretWidth(); + if (r.Width < caretWidth) + r.Width = caretWidth; + return r; + } + + /// + /// Gets/Sets the color of the caret. + /// + public Brush CaretBrush { + get { return caretAdorner.CaretBrush; } + set { caretAdorner.CaretBrush = value; } + } + } +} diff --git a/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs b/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs index 90cc8fbe..cdd811d1 100644 --- a/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs +++ b/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team -// +// 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 @@ -17,12 +17,9 @@ // DEALINGS IN THE SOFTWARE. using System; -using System.Collections.Generic; using System.Diagnostics; using System.Windows; using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media.TextFormatting; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Rendering; @@ -48,146 +45,10 @@ enum CaretMovementType DocumentEnd } - static class CaretNavigationCommandHandler + static partial class CaretNavigationCommandHandler { - /// - /// Creates a new for the text area. - /// - public static TextAreaInputHandler Create(TextArea textArea) - { - TextAreaInputHandler handler = new TextAreaInputHandler(textArea); - handler.CommandBindings.AddRange(CommandBindings); - handler.InputBindings.AddRange(InputBindings); - return handler; - } - - static readonly List CommandBindings = new List(); - static readonly List InputBindings = new List(); - - 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; - } - - 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); - 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 + /// Moves the caret in the given direction, accounting for FlowDirection. internal static void MoveCaret(TextArea textArea, CaretMovementType direction) { double desiredXPos = textArea.Caret.DesiredXPos; @@ -207,82 +68,9 @@ internal static void MoveCaret(TextArea textArea, CaretMovementType direction) textArea.Caret.Position = GetNewCaretPosition(textArea.TextView, textArea.Caret.Position, direction, textArea.Selection.EnableVirtualSpace, ref desiredXPos); textArea.Caret.DesiredXPos = desiredXPos; } - - internal static TextViewPosition GetNewCaretPosition(TextView textView, TextViewPosition caretPosition, CaretMovementType direction, bool enableVirtualSpace, ref double desiredXPos) - { - switch (direction) { - case CaretMovementType.None: - return caretPosition; - case CaretMovementType.DocumentStart: - desiredXPos = double.NaN; - return new TextViewPosition(0, 0); - case CaretMovementType.DocumentEnd: - desiredXPos = double.NaN; - return new TextViewPosition(textView.Document.GetLocation(textView.Document.TextLength)); - } - DocumentLine caretLine = textView.Document.GetLineByNumber(caretPosition.Line); - VisualLine visualLine = textView.GetOrConstructVisualLine(caretLine); - TextLine textLine = visualLine.GetTextLine(caretPosition.VisualColumn, caretPosition.IsAtEndOfLine); - switch (direction) { - case CaretMovementType.CharLeft: - desiredXPos = double.NaN; - // do not move caret to previous line in virtual space - if (caretPosition.VisualColumn == 0 && enableVirtualSpace) - return caretPosition; - return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.Normal, enableVirtualSpace); - case CaretMovementType.Backspace: - desiredXPos = double.NaN; - return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.EveryCodepoint, enableVirtualSpace); - case CaretMovementType.CharRight: - desiredXPos = double.NaN; - return GetNextCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.Normal, enableVirtualSpace); - case CaretMovementType.WordLeft: - desiredXPos = double.NaN; - return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.WordStart, enableVirtualSpace); - case CaretMovementType.WordRight: - desiredXPos = double.NaN; - return GetNextCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.WordStart, enableVirtualSpace); - case CaretMovementType.LineUp: - case CaretMovementType.LineDown: - case CaretMovementType.PageUp: - case CaretMovementType.PageDown: - return GetUpDownCaretPosition(textView, caretPosition, direction, visualLine, textLine, enableVirtualSpace, ref desiredXPos); - case CaretMovementType.LineStart: - desiredXPos = double.NaN; - return GetStartOfLineCaretPosition(caretPosition.VisualColumn, visualLine, textLine, enableVirtualSpace); - case CaretMovementType.LineEnd: - desiredXPos = double.NaN; - return GetEndOfLineCaretPosition(visualLine, textLine); - default: - throw new NotSupportedException(direction.ToString()); - } - } #endregion - #region Home/End - static TextViewPosition GetStartOfLineCaretPosition(int oldVC, VisualLine visualLine, TextLine textLine, bool enableVirtualSpace) - { - int newVC = visualLine.GetTextLineVisualStartColumn(textLine); - if (newVC == 0) - newVC = visualLine.GetNextCaretPosition(newVC - 1, LogicalDirection.Forward, CaretPositioningMode.WordStart, 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 == oldVC) - newVC = 0; - return visualLine.GetTextViewPosition(newVC); - } - - static TextViewPosition GetEndOfLineCaretPosition(VisualLine visualLine, TextLine textLine) - { - int newVC = visualLine.GetTextLineVisualStartColumn(textLine) + textLine.Length - textLine.NewlineLength; - TextViewPosition pos = visualLine.GetTextViewPosition(newVC); - pos.IsAtEndOfLine = true; - return pos; - } - #endregion - - #region By-character / By-word movement + #region By-character / By-word movement (platform-independent) static TextViewPosition GetNextCaretPosition(TextView textView, TextViewPosition caretPosition, VisualLine visualLine, CaretPositioningMode mode, bool enableVirtualSpace) { int pos = visualLine.GetNextCaretPosition(caretPosition.VisualColumn, LogicalDirection.Forward, mode, enableVirtualSpace); @@ -327,79 +115,5 @@ static TextViewPosition GetPrevCaretPosition(TextView textView, TextViewPosition } } #endregion - - #region Line+Page up/down - static TextViewPosition GetUpDownCaretPosition(TextView textView, TextViewPosition caretPosition, CaretMovementType direction, VisualLine visualLine, TextLine textLine, bool enableVirtualSpace, ref double xPos) - { - // moving up/down happens using the desired visual X position - if (double.IsNaN(xPos)) - xPos = visualLine.GetTextLineVisualXPosition(textLine, caretPosition.VisualColumn); - // 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 = textView.Document.GetLineByNumber(prevLineNumber); - targetVisualLine = 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 <= textView.Document.LineCount) { - DocumentLine nextLine = textView.Document.GetLineByNumber(nextLineNumber); - targetVisualLine = 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 -= textView.RenderSize.Height; - else - yPos += textView.RenderSize.Height; - DocumentLine newLine = textView.GetDocumentLineByVisualTop(yPos); - targetVisualLine = 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), enableVirtualSpace); - - // prevent wrapping to the next line; TODO: could 'IsAtEnd' help here? - int targetLineStartCol = targetVisualLine.GetTextLineVisualStartColumn(targetLine); - if (newVisualColumn >= targetLineStartCol + targetLine.Length) { - if (newVisualColumn <= targetVisualLine.VisualLength) - newVisualColumn = targetLineStartCol + targetLine.Length - 1; - } - return targetVisualLine.GetTextViewPosition(newVisualColumn); - } else { - return caretPosition; - } - } - #endregion } } diff --git a/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.wpf.cs b/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.wpf.cs new file mode 100644 index 00000000..7ea9948f --- /dev/null +++ b/ICSharpCode.AvalonEdit/Editing/CaretNavigationCommandHandler.wpf.cs @@ -0,0 +1,321 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media.TextFormatting; + +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit.Editing +{ + static partial class CaretNavigationCommandHandler + { + /// + /// Creates a new for the text area. + /// + public static TextAreaInputHandler Create(TextArea textArea) + { + TextAreaInputHandler handler = new TextAreaInputHandler(textArea); + handler.CommandBindings.AddRange(CommandBindings); + handler.InputBindings.AddRange(InputBindings); + return handler; + } + + static readonly List CommandBindings = new List(); + static readonly List InputBindings = new List(); + + 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; + } + + 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); + 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(); + } + }; + } + + // GetNewCaretPosition — uses System.Windows.Media.TextFormatting.TextLine. + // Implementations live here (WPF). For Uno, this will be in CaretNavigationCommandHandler.uno.cs. + internal static TextViewPosition GetNewCaretPosition(TextView textView, TextViewPosition caretPosition, CaretMovementType direction, bool enableVirtualSpace, ref double desiredXPos) + { + switch (direction) { + case CaretMovementType.None: + return caretPosition; + case CaretMovementType.DocumentStart: + desiredXPos = double.NaN; + return new TextViewPosition(0, 0); + case CaretMovementType.DocumentEnd: + desiredXPos = double.NaN; + return new TextViewPosition(textView.Document.GetLocation(textView.Document.TextLength)); + } + DocumentLine caretLine = textView.Document.GetLineByNumber(caretPosition.Line); + VisualLine visualLine = textView.GetOrConstructVisualLine(caretLine); + TextLine textLine = visualLine.GetTextLine(caretPosition.VisualColumn, caretPosition.IsAtEndOfLine); + switch (direction) { + case CaretMovementType.CharLeft: + desiredXPos = double.NaN; + // do not move caret to previous line in virtual space + if (caretPosition.VisualColumn == 0 && enableVirtualSpace) + return caretPosition; + return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.Normal, enableVirtualSpace); + case CaretMovementType.Backspace: + desiredXPos = double.NaN; + return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.EveryCodepoint, enableVirtualSpace); + case CaretMovementType.CharRight: + desiredXPos = double.NaN; + return GetNextCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.Normal, enableVirtualSpace); + case CaretMovementType.WordLeft: + desiredXPos = double.NaN; + return GetPrevCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.WordStart, enableVirtualSpace); + case CaretMovementType.WordRight: + desiredXPos = double.NaN; + return GetNextCaretPosition(textView, caretPosition, visualLine, CaretPositioningMode.WordStart, enableVirtualSpace); + case CaretMovementType.LineUp: + case CaretMovementType.LineDown: + case CaretMovementType.PageUp: + case CaretMovementType.PageDown: + return GetUpDownCaretPosition(textView, caretPosition, direction, visualLine, textLine, enableVirtualSpace, ref desiredXPos); + case CaretMovementType.LineStart: + desiredXPos = double.NaN; + return GetStartOfLineCaretPosition(caretPosition.VisualColumn, visualLine, textLine, enableVirtualSpace); + case CaretMovementType.LineEnd: + desiredXPos = double.NaN; + return GetEndOfLineCaretPosition(visualLine, textLine); + default: + throw new NotSupportedException(direction.ToString()); + } + } + + #region Home/End + static TextViewPosition GetStartOfLineCaretPosition(int oldVC, VisualLine visualLine, TextLine textLine, bool enableVirtualSpace) + { + int newVC = visualLine.GetTextLineVisualStartColumn(textLine); + if (newVC == 0) + newVC = visualLine.GetNextCaretPosition(newVC - 1, LogicalDirection.Forward, CaretPositioningMode.WordStart, 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 == oldVC) + newVC = 0; + return visualLine.GetTextViewPosition(newVC); + } + + static TextViewPosition GetEndOfLineCaretPosition(VisualLine visualLine, TextLine textLine) + { + int newVC = visualLine.GetTextLineVisualStartColumn(textLine) + textLine.Length - textLine.NewlineLength; + TextViewPosition pos = visualLine.GetTextViewPosition(newVC); + pos.IsAtEndOfLine = true; + return pos; + } + #endregion + + #region Line+Page up/down + static TextViewPosition GetUpDownCaretPosition(TextView textView, TextViewPosition caretPosition, CaretMovementType direction, VisualLine visualLine, TextLine textLine, bool enableVirtualSpace, ref double xPos) + { + // moving up/down happens using the desired visual X position + if (double.IsNaN(xPos)) + xPos = visualLine.GetTextLineVisualXPosition(textLine, caretPosition.VisualColumn); + // 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 = textView.Document.GetLineByNumber(prevLineNumber); + targetVisualLine = 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 <= textView.Document.LineCount) { + DocumentLine nextLine = textView.Document.GetLineByNumber(nextLineNumber); + targetVisualLine = 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 -= textView.RenderSize.Height; + else + yPos += textView.RenderSize.Height; + DocumentLine newLine = textView.GetDocumentLineByVisualTop(yPos); + targetVisualLine = 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), enableVirtualSpace); + + // prevent wrapping to the next line; TODO: could 'IsAtEnd' help here? + int targetLineStartCol = targetVisualLine.GetTextLineVisualStartColumn(targetLine); + if (newVisualColumn >= targetLineStartCol + targetLine.Length) { + if (newVisualColumn <= targetVisualLine.VisualLength) + newVisualColumn = targetLineStartCol + targetLine.Length - 1; + } + return targetVisualLine.GetTextViewPosition(newVisualColumn); + } else { + return caretPosition; + } + } + #endregion + } +} diff --git a/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs b/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs index fcdda52c..8e0a63d9 100644 --- a/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs +++ b/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team -// +// 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 @@ -17,18 +17,9 @@ // DEALINGS IN THE SOFTWARE. using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Windows; -using System.Windows.Documents; -using System.Windows.Input; using ICSharpCode.AvalonEdit.Document; -using ICSharpCode.AvalonEdit.Highlighting; -using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Editing { @@ -36,68 +27,8 @@ namespace ICSharpCode.AvalonEdit.Editing /// We re-use the CommandBinding and InputBinding instances between multiple text areas, /// so this class is static. /// - static class EditingCommandHandler + static partial class EditingCommandHandler { - /// - /// Creates a new for the text area. - /// - public static TextAreaInputHandler Create(TextArea textArea) - { - TextAreaInputHandler handler = new TextAreaInputHandler(textArea); - handler.CommandBindings.AddRange(CommandBindings); - handler.InputBindings.AddRange(InputBindings); - return handler; - } - - static readonly List CommandBindings = new List(); - static readonly List InputBindings = new List(); - - 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(CaretMovementType.None), CanDelete)); - AddBinding(EditingCommands.Delete, ModifierKeys.None, Key.Delete, OnDelete(CaretMovementType.CharRight)); - AddBinding(EditingCommands.DeleteNextWord, ModifierKeys.Control, Key.Delete, OnDelete(CaretMovementType.WordRight)); - AddBinding(EditingCommands.Backspace, ModifierKeys.None, Key.Back, OnDelete(CaretMovementType.Backspace)); - 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(CaretMovementType.WordLeft)); - 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.ToggleOverstrike, OnToggleOverstrike)); - 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 { @@ -106,451 +37,6 @@ enum DefaultSegmentType CurrentLine } - /// - /// Calls transformLine on all lines in the selected range. - /// transformLine needs to handle read-only segments! - /// - static void TransformSelectedLines(Action 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; - } - } - - /// - /// Calls transformLine on all writable segment in the selected range. - /// - static void TransformSelectedSegments(Action transformSegment, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) - { - TextArea textArea = GetTextArea(target); - if (textArea != null && textArea.Document != null) { - using (textArea.Document.RunUpdate()) { - IEnumerable 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(); - } else { - segments = null; - } - } else { - segments = textArea.Selection.Segments.Cast(); - } - 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(CaretMovementType caretMovement) - { - return (target, args) => { - TextArea textArea = GetTextArea(target); - if (textArea != null && textArea.Document != null) { - if (textArea.Selection.IsEmpty) { - TextViewPosition startPos = textArea.Caret.Position; - bool enableVirtualSpace = textArea.Options.EnableVirtualSpace; - // When pressing delete; don't move the caret further into virtual space - instead delete the newline - if (caretMovement == CaretMovementType.CharRight) - enableVirtualSpace = false; - double desiredXPos = textArea.Caret.DesiredXPos; - TextViewPosition endPos = CaretNavigationCommandHandler.GetNewCaretPosition( - textArea.TextView, startPos, caretMovement, enableVirtualSpace, ref desiredXPos); - // GetNewCaretPosition may return (0,0) as new position, - // thus we need to validate endPos before using it in the selection. - if (endPos.Line < 1 || endPos.Column < 1) - endPos = new TextViewPosition(Math.Max(endPos.Line, 1), Math.Max(endPos.Column, 1)); - // Don't do anything if the number of lines of a rectangular selection would be changed by the deletion. - if (textArea.Selection is RectangleSelection && startPos.Line != endPos.Line) - return; - // Don't select the text to be deleted; just reuse the ReplaceSelectionWithText logic - // Reuse the existing selection, so that we continue using the same logic - textArea.Selection.StartSelectionOrSetEndpoint(startPos, endPos) - .ReplaceSelectionWithText(string.Empty); - } else { - 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); - if (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 { - if (CopySelectedText(textArea)) - textArea.RemoveSelectedText(); - } - textArea.Caret.BringCaretToView(); - args.Handled = true; - } - } - - static bool CopySelectedText(TextArea textArea) - { - var data = textArea.Selection.CreateDataObject(textArea); - var copyingEventArgs = new DataObjectCopyingEventArgs(data, false); - textArea.RaiseEvent(copyingEventArgs); - if (copyingEventArgs.CommandCancelled) - return 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. - } - - string text = textArea.Selection.GetText(); - text = TextUtilities.NormalizeNewLines(text, Environment.NewLine); - textArea.OnTextCopied(new TextEventArgs(text)); - return true; - } - - const string LineSelectedType = "MSDEVLineSelect"; // This is the type VS 2003 and 2005 use for flagging a whole line copy - - public static bool ConfirmDataFormat(TextArea textArea, DataObject dataObject, string format) - { - var e = new DataObjectSettingDataEventArgs(dataObject, format); - textArea.RaiseEvent(e); - return !e.CommandCancelled; - } - - static bool 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(); - if (ConfirmDataFormat(textArea, data, DataFormats.UnicodeText)) - data.SetText(text); - - // Also copy text in HTML format to clipboard - good for pasting text into Word - // or to the SharpDevelop forums. - if (ConfirmDataFormat(textArea, data, DataFormats.Html)) { - IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; - HtmlClipboard.SetHtml(data, HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, wholeLine, new HtmlOptions(textArea.Options))); - } - - if (ConfirmDataFormat(textArea, data, LineSelectedType)) { - MemoryStream lineSelected = new MemoryStream(1); - lineSelected.WriteByte(1); - data.SetData(LineSelectedType, lineSelected, false); - } - - var copyingEventArgs = new DataObjectCopyingEventArgs(data, false); - textArea.RaiseEvent(copyingEventArgs); - if (copyingEventArgs.CommandCancelled) - return 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 false; - } - textArea.OnTextCopied(new TextEventArgs(text)); - return true; - } - - 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; - - var pastingEventArgs = new DataObjectPastingEventArgs(dataObject, false, DataFormats.UnicodeText); - textArea.RaiseEvent(pastingEventArgs); - if (pastingEventArgs.CommandCancelled) - return; - - string text = GetTextToPaste(pastingEventArgs, textArea); - - if (!string.IsNullOrEmpty(text)) { - dataObject = pastingEventArgs.DataObject; - 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; - } - } - - internal static string GetTextToPaste(DataObjectPastingEventArgs pastingEventArgs, TextArea textArea) - { - var dataObject = pastingEventArgs.DataObject; - if (dataObject == null) - return null; - try { - string text; - // Try retrieving the text as one of: - // - the FormatToApply - // - UnicodeText - // - Text - // (but don't try the same format twice) - if (pastingEventArgs.FormatToApply != null && dataObject.GetDataPresent(pastingEventArgs.FormatToApply)) - text = (string)dataObject.GetData(pastingEventArgs.FormatToApply); - else if (pastingEventArgs.FormatToApply != DataFormats.UnicodeText && dataObject.GetDataPresent(DataFormats.UnicodeText)) - text = (string)dataObject.GetData(DataFormats.UnicodeText); - else if (pastingEventArgs.FormatToApply != DataFormats.Text && dataObject.GetDataPresent(DataFormats.Text)) - text = (string)dataObject.GetData(DataFormats.Text); - else - return null; // no text data format - // convert text back to correct newlines for this document - string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line); - text = TextUtilities.NormalizeNewLines(text, newLine); - text = textArea.Options.ConvertTabsToSpaces ? text.Replace("\t", new String(' ', textArea.Options.IndentationSize)) : text; - return text; - } catch (OutOfMemoryException) { - // may happen when trying to paste a huge string - return null; - } catch (COMException) { - // may happen with incorrect data => Data on clipboard is invalid (Exception from HRESULT: 0x800401D3 (CLIPBRD_E_BAD_DATA)) - return null; - } - } - #endregion - - #region Toggle Overstrike - static void OnToggleOverstrike(object target, ExecutedRoutedEventArgs args) - { - TextArea textArea = GetTextArea(target); - if (textArea != null && textArea.Options.AllowToggleOverstrikeMode) - textArea.OverstrikeMode = !textArea.OverstrikeMode; - } - #endregion - - #region DeleteLine - static void OnDeleteLine(object target, ExecutedRoutedEventArgs args) - { - TextArea textArea = GetTextArea(target); - if (textArea != null && textArea.Document != null) { - int firstLineIndex, lastLineIndex; - if (textArea.Selection.Length == 0) { - // There is no selection, simply delete current line - firstLineIndex = lastLineIndex = textArea.Caret.Line; - } else { - // There is a selection, remove all lines affected by it (use Min/Max to be independent from selection direction) - firstLineIndex = Math.Min(textArea.Selection.StartPosition.Line, textArea.Selection.EndPosition.Line); - lastLineIndex = Math.Max(textArea.Selection.StartPosition.Line, textArea.Selection.EndPosition.Line); - } - DocumentLine startLine = textArea.Document.GetLineByNumber(firstLineIndex); - DocumentLine endLine = textArea.Document.GetLineByNumber(lastLineIndex); - textArea.Selection = Selection.Create(textArea, startLine.Offset, endLine.Offset + endLine.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; @@ -564,19 +50,6 @@ static void ConvertTabsToSpaces(TextArea textArea, ISegment segment) } } - 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; @@ -597,38 +70,6 @@ static void ConvertSpacesToTabs(TextArea textArea, ISegment segment) } } } - #endregion - - #region Convert...Case - static void ConvertCase(Func 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) { @@ -641,25 +82,5 @@ static string InvertCase(string text) return new string(buffer); } #endregion - - static void OnIndentSelection(object target, ExecutedRoutedEventArgs args) - { - TextArea textArea = GetTextArea(target); - if (textArea != null && textArea.Document != null && textArea.IndentationStrategy != 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/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.wpf.cs b/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.wpf.cs new file mode 100644 index 00000000..dc909c8d --- /dev/null +++ b/ICSharpCode.AvalonEdit/Editing/EditingCommandHandler.wpf.cs @@ -0,0 +1,609 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; + +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Highlighting; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit.Editing +{ + static partial class EditingCommandHandler + { + /// + /// Creates a new for the text area. + /// + public static TextAreaInputHandler Create(TextArea textArea) + { + TextAreaInputHandler handler = new TextAreaInputHandler(textArea); + handler.CommandBindings.AddRange(CommandBindings); + handler.InputBindings.AddRange(InputBindings); + return handler; + } + + static readonly List CommandBindings = new List(); + static readonly List InputBindings = new List(); + + 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(CaretMovementType.None), CanDelete)); + AddBinding(EditingCommands.Delete, ModifierKeys.None, Key.Delete, OnDelete(CaretMovementType.CharRight)); + AddBinding(EditingCommands.DeleteNextWord, ModifierKeys.Control, Key.Delete, OnDelete(CaretMovementType.WordRight)); + AddBinding(EditingCommands.Backspace, ModifierKeys.None, Key.Back, OnDelete(CaretMovementType.Backspace)); + 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(CaretMovementType.WordLeft)); + 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.ToggleOverstrike, OnToggleOverstrike)); + 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 + /// + /// Calls transformLine on all lines in the selected range. + /// transformLine needs to handle read-only segments! + /// + static void TransformSelectedLines(Action 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; + } + } + + /// + /// Calls transformLine on all writable segment in the selected range. + /// + static void TransformSelectedSegments(Action transformSegment, object target, ExecutedRoutedEventArgs args, DefaultSegmentType defaultSegmentType) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + using (textArea.Document.RunUpdate()) { + IEnumerable 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(); + } else { + segments = null; + } + } else { + segments = textArea.Selection.Segments.Cast(); + } + 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(CaretMovementType caretMovement) + { + return (target, args) => { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + if (textArea.Selection.IsEmpty) { + TextViewPosition startPos = textArea.Caret.Position; + bool enableVirtualSpace = textArea.Options.EnableVirtualSpace; + // When pressing delete; don't move the caret further into virtual space - instead delete the newline + if (caretMovement == CaretMovementType.CharRight) + enableVirtualSpace = false; + double desiredXPos = textArea.Caret.DesiredXPos; + TextViewPosition endPos = CaretNavigationCommandHandler.GetNewCaretPosition( + textArea.TextView, startPos, caretMovement, enableVirtualSpace, ref desiredXPos); + // GetNewCaretPosition may return (0,0) as new position, + // thus we need to validate endPos before using it in the selection. + if (endPos.Line < 1 || endPos.Column < 1) + endPos = new TextViewPosition(Math.Max(endPos.Line, 1), Math.Max(endPos.Column, 1)); + // Don't do anything if the number of lines of a rectangular selection would be changed by the deletion. + if (textArea.Selection is RectangleSelection && startPos.Line != endPos.Line) + return; + // Don't select the text to be deleted; just reuse the ReplaceSelectionWithText logic + // Reuse the existing selection, so that we continue using the same logic + textArea.Selection.StartSelectionOrSetEndpoint(startPos, endPos) + .ReplaceSelectionWithText(string.Empty); + } else { + 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); + if (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 { + if (CopySelectedText(textArea)) + textArea.RemoveSelectedText(); + } + textArea.Caret.BringCaretToView(); + args.Handled = true; + } + } + + static bool CopySelectedText(TextArea textArea) + { + var data = textArea.Selection.CreateDataObject(textArea); + var copyingEventArgs = new DataObjectCopyingEventArgs(data, false); + textArea.RaiseEvent(copyingEventArgs); + if (copyingEventArgs.CommandCancelled) + return 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. + } + + string text = textArea.Selection.GetText(); + text = TextUtilities.NormalizeNewLines(text, Environment.NewLine); + textArea.OnTextCopied(new TextEventArgs(text)); + return true; + } + + const string LineSelectedType = "MSDEVLineSelect"; // This is the type VS 2003 and 2005 use for flagging a whole line copy + + public static bool ConfirmDataFormat(TextArea textArea, DataObject dataObject, string format) + { + var e = new DataObjectSettingDataEventArgs(dataObject, format); + textArea.RaiseEvent(e); + return !e.CommandCancelled; + } + + static bool 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(); + if (ConfirmDataFormat(textArea, data, DataFormats.UnicodeText)) + data.SetText(text); + + // Also copy text in HTML format to clipboard - good for pasting text into Word + // or to the SharpDevelop forums. + if (ConfirmDataFormat(textArea, data, DataFormats.Html)) { + IHighlighter highlighter = textArea.GetService(typeof(IHighlighter)) as IHighlighter; + HtmlClipboard.SetHtml(data, HtmlClipboard.CreateHtmlFragment(textArea.Document, highlighter, wholeLine, new HtmlOptions(textArea.Options))); + } + + if (ConfirmDataFormat(textArea, data, LineSelectedType)) { + MemoryStream lineSelected = new MemoryStream(1); + lineSelected.WriteByte(1); + data.SetData(LineSelectedType, lineSelected, false); + } + + var copyingEventArgs = new DataObjectCopyingEventArgs(data, false); + textArea.RaiseEvent(copyingEventArgs); + if (copyingEventArgs.CommandCancelled) + return 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 false; + } + textArea.OnTextCopied(new TextEventArgs(text)); + return true; + } + + 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; + + var pastingEventArgs = new DataObjectPastingEventArgs(dataObject, false, DataFormats.UnicodeText); + textArea.RaiseEvent(pastingEventArgs); + if (pastingEventArgs.CommandCancelled) + return; + + string text = GetTextToPaste(pastingEventArgs, textArea); + + if (!string.IsNullOrEmpty(text)) { + dataObject = pastingEventArgs.DataObject; + 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; + } + } + + internal static string GetTextToPaste(DataObjectPastingEventArgs pastingEventArgs, TextArea textArea) + { + var dataObject = pastingEventArgs.DataObject; + if (dataObject == null) + return null; + try { + string text; + // Try retrieving the text as one of: + // - the FormatToApply + // - UnicodeText + // - Text + // (but don't try the same format twice) + if (pastingEventArgs.FormatToApply != null && dataObject.GetDataPresent(pastingEventArgs.FormatToApply)) + text = (string)dataObject.GetData(pastingEventArgs.FormatToApply); + else if (pastingEventArgs.FormatToApply != DataFormats.UnicodeText && dataObject.GetDataPresent(DataFormats.UnicodeText)) + text = (string)dataObject.GetData(DataFormats.UnicodeText); + else if (pastingEventArgs.FormatToApply != DataFormats.Text && dataObject.GetDataPresent(DataFormats.Text)) + text = (string)dataObject.GetData(DataFormats.Text); + else + return null; // no text data format + // convert text back to correct newlines for this document + string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line); + text = TextUtilities.NormalizeNewLines(text, newLine); + text = textArea.Options.ConvertTabsToSpaces ? text.Replace("\t", new String(' ', textArea.Options.IndentationSize)) : text; + return text; + } catch (OutOfMemoryException) { + // may happen when trying to paste a huge string + return null; + } catch (COMException) { + // may happen with incorrect data => Data on clipboard is invalid (Exception from HRESULT: 0x800401D3 (CLIPBRD_E_BAD_DATA)) + return null; + } + } + #endregion + + #region Toggle Overstrike + static void OnToggleOverstrike(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Options.AllowToggleOverstrikeMode) + textArea.OverstrikeMode = !textArea.OverstrikeMode; + } + #endregion + + #region DeleteLine + static void OnDeleteLine(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null) { + int firstLineIndex, lastLineIndex; + if (textArea.Selection.Length == 0) { + // There is no selection, simply delete current line + firstLineIndex = lastLineIndex = textArea.Caret.Line; + } else { + // There is a selection, remove all lines affected by it (use Min/Max to be independent from selection direction) + firstLineIndex = Math.Min(textArea.Selection.StartPosition.Line, textArea.Selection.EndPosition.Line); + lastLineIndex = Math.Max(textArea.Selection.StartPosition.Line, textArea.Selection.EndPosition.Line); + } + DocumentLine startLine = textArea.Document.GetLineByNumber(firstLineIndex); + DocumentLine endLine = textArea.Document.GetLineByNumber(lastLineIndex); + textArea.Selection = Selection.Create(textArea, startLine.Offset, endLine.Offset + endLine.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 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); + } + #endregion + + #region Convert...Case + static void ConvertCase(Func 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); + } + #endregion + + static void OnIndentSelection(object target, ExecutedRoutedEventArgs args) + { + TextArea textArea = GetTextArea(target); + if (textArea != null && textArea.Document != null && textArea.IndentationStrategy != 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/ICSharpCode.AvalonEdit/Editing/TextArea.cs b/ICSharpCode.AvalonEdit/Editing/TextArea.cs index 5ede9a16..d93f0463 100644 --- a/ICSharpCode.AvalonEdit/Editing/TextArea.cs +++ b/ICSharpCode.AvalonEdit/Editing/TextArea.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team -// +// 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 @@ -24,10 +24,8 @@ using System.Linq; using System.Windows; using System.Windows.Controls; -using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; -using System.Windows.Media; using System.Windows.Threading; using ICSharpCode.AvalonEdit.Document; @@ -40,21 +38,24 @@ namespace ICSharpCode.AvalonEdit.Editing /// /// Control that wraps a TextView and adds support for user input and the caret. /// - public class TextArea : Control, IScrollInfo, IWeakEventListener, ITextEditorComponent, IServiceProvider + public partial class TextArea : Control, ITextEditorComponent, IServiceProvider { - internal readonly ImeSupport ime; + #region Partial void declarations + static partial void InitializeWpfDefaults(); + partial void InitializeIme(); + partial void AttachTypingEvents(); + partial void RequestSelectionValidationAsync(); + partial void InvalidateRequery(); + partial void SubscribeToDocumentEvents(TextDocument document); + partial void UnsubscribeFromDocumentEvents(TextDocument document); + partial void SubscribeToOptionsEvents(TextEditorOptions options); + partial void UnsubscribeFromOptionsEvents(TextEditorOptions options); + #endregion #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)); + InitializeWpfDefaults(); } /// @@ -85,7 +86,7 @@ protected TextArea(TextView textView) caret.PositionChanged += (sender, e) => RequestSelectionValidation(); caret.PositionChanged += CaretPositionChanged; AttachTypingEvents(); - ime = new ImeSupport(this); + InitializeIme(); leftMargins.CollectionChanged += leftMargins_CollectionChanged; @@ -208,26 +209,16 @@ static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEven 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); - } + UnsubscribeFromDocumentEvents(oldValue); 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); - } + SubscribeToDocumentEvents(newValue); // 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(); + InvalidateRequery(); } #endregion @@ -268,46 +259,13 @@ static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEvent void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue) { - if (oldValue != null) { - PropertyChangedWeakEventManager.RemoveListener(oldValue, this); - } + UnsubscribeFromOptionsEvents(oldValue); textView.Options = newValue; - if (newValue != null) { - PropertyChangedWeakEventManager.AddListener(newValue, this); - } + SubscribeToOptionsEvents(newValue); OnOptionChanged(new PropertyChangedEventArgs(null)); } #endregion - #region ReceiveWeakEvent - /// - protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) - { - if (managerType == typeof(TextDocumentWeakEventManager.Changing)) { - OnDocumentChanging(); - return true; - } else if (managerType == typeof(TextDocumentWeakEventManager.Changed)) { - OnDocumentChanged((DocumentChangeEventArgs)e); - return true; - } else if (managerType == typeof(TextDocumentWeakEventManager.UpdateStarted)) { - OnUpdateStarted(); - return true; - } else if (managerType == typeof(TextDocumentWeakEventManager.UpdateFinished)) { - OnUpdateFinished(); - return true; - } else if (managerType == typeof(PropertyChangedWeakEventManager)) { - OnOptionChanged((PropertyChangedEventArgs)e); - return true; - } - return false; - } - - bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) - { - return ReceiveWeakEvent(managerType, sender, e); - } - #endregion - #region Caret handling on document changes void OnDocumentChanging() { @@ -365,7 +323,6 @@ public void Redo() #region TextView property readonly TextView textView; - IScrollInfo scrollInfo; /// /// Gets the text view used to display text in this text area. @@ -375,13 +332,6 @@ public TextView TextView { return textView; } } - /// - public override void OnApplyTemplate() - { - base.OnApplyTemplate(); - scrollInfo = textView; - ApplyScrollInfo(); - } #endregion #region Selection property @@ -434,7 +384,7 @@ public Selection Selection { if (SelectionChanged != null) SelectionChanged(this, EventArgs.Empty); // a selection change causes commands like copy/paste/etc. to change status - CommandManager.InvalidateRequerySuggested(); + InvalidateRequery(); } } } @@ -451,13 +401,13 @@ public void ClearSelection() /// The property. /// public static readonly DependencyProperty SelectionBrushProperty = - DependencyProperty.Register("SelectionBrush", typeof(Brush), typeof(TextArea)); + DependencyProperty.Register("SelectionBrush", typeof(System.Windows.Media.Brush), typeof(TextArea)); /// /// Gets/Sets the background brush used for the selection. /// - public Brush SelectionBrush { - get { return (Brush)GetValue(SelectionBrushProperty); } + public System.Windows.Media.Brush SelectionBrush { + get { return (System.Windows.Media.Brush)GetValue(SelectionBrushProperty); } set { SetValue(SelectionBrushProperty, value); } } @@ -465,13 +415,13 @@ public Brush SelectionBrush { /// The property. /// public static readonly DependencyProperty SelectionForegroundProperty = - DependencyProperty.Register("SelectionForeground", typeof(Brush), typeof(TextArea)); + DependencyProperty.Register("SelectionForeground", typeof(System.Windows.Media.Brush), typeof(TextArea)); /// /// Gets/Sets the foreground brush used for selected text. /// - public Brush SelectionForeground { - get { return (Brush)GetValue(SelectionForegroundProperty); } + public System.Windows.Media.Brush SelectionForeground { + get { return (System.Windows.Media.Brush)GetValue(SelectionForegroundProperty); } set { SetValue(SelectionForegroundProperty, value); } } @@ -479,13 +429,13 @@ public Brush SelectionForeground { /// The property. /// public static readonly DependencyProperty SelectionBorderProperty = - DependencyProperty.Register("SelectionBorder", typeof(Pen), typeof(TextArea)); + DependencyProperty.Register("SelectionBorder", typeof(System.Windows.Media.Pen), typeof(TextArea)); /// /// Gets/Sets the pen used for the border of the selection. /// - public Pen SelectionBorder { - get { return (Pen)GetValue(SelectionBorderProperty); } + public System.Windows.Media.Pen SelectionBorder { + get { return (System.Windows.Media.Pen)GetValue(SelectionBorderProperty); } set { SetValue(SelectionBorderProperty, value); } } @@ -506,14 +456,14 @@ public double SelectionCornerRadius { /// /// Gets/Sets the active mouse selection mode. - /// + /// /// Setting this property to MouseSelectionMode.None will cancel mouse selection /// and release mouse capture. - /// + /// /// Setting this property to another value will acquire mouse capture and /// activate the mouse selection mode. /// If mouse capture cannot be acquired, MouseSelectionMode will stay unchanged. - /// + /// /// Currently, the setter only supports the values None, Normal /// and Rectangular. /// @@ -543,7 +493,7 @@ void RequestSelectionValidation() { if (!ensureSelectionValidRequested && allowCaretOutsideSelection == 0) { ensureSelectionValidRequested = true; - Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(EnsureSelectionValid)); + RequestSelectionValidationAsync(); } } @@ -551,10 +501,10 @@ void RequestSelectionValidation() /// 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. @@ -648,278 +598,12 @@ public IReadOnlySectionProvider ReadOnlySectionProvider { if (value == null) throw new ArgumentNullException("value"); readOnlySectionProvider = value; - CommandManager.InvalidateRequerySuggested(); // the read-only status effects Paste.CanExecute and the IME + InvalidateRequery(); // the read-only status effects Paste.CanExecute and the IME } } #endregion - #region IScrollInfo implementation - ScrollViewer scrollOwner; - bool canVerticallyScroll, canHorizontallyScroll; - - void ApplyScrollInfo() - { - if (scrollInfo != null) { - scrollInfo.ScrollOwner = scrollOwner; - scrollInfo.CanVerticallyScroll = canVerticallyScroll; - scrollInfo.CanHorizontallyScroll = canHorizontallyScroll; - scrollOwner = null; - } - } - - bool IScrollInfo.CanVerticallyScroll { - get { return scrollInfo != null ? scrollInfo.CanVerticallyScroll : false; } - set { - canVerticallyScroll = value; - if (scrollInfo != null) - scrollInfo.CanVerticallyScroll = value; - } - } - - bool IScrollInfo.CanHorizontallyScroll { - get { return scrollInfo != null ? scrollInfo.CanHorizontallyScroll : false; } - set { - canHorizontallyScroll = value; - if (scrollInfo != null) - scrollInfo.CanHorizontallyScroll = value; - } - } - - double IScrollInfo.ExtentWidth { - get { return scrollInfo != null ? scrollInfo.ExtentWidth : 0; } - } - - double IScrollInfo.ExtentHeight { - get { return scrollInfo != null ? scrollInfo.ExtentHeight : 0; } - } - - double IScrollInfo.ViewportWidth { - get { return scrollInfo != null ? scrollInfo.ViewportWidth : 0; } - } - - double IScrollInfo.ViewportHeight { - get { return scrollInfo != null ? scrollInfo.ViewportHeight : 0; } - } - - double IScrollInfo.HorizontalOffset { - get { return scrollInfo != null ? scrollInfo.HorizontalOffset : 0; } - } - - double IScrollInfo.VerticalOffset { - get { return scrollInfo != null ? scrollInfo.VerticalOffset : 0; } - } - - ScrollViewer IScrollInfo.ScrollOwner { - get { return scrollInfo != null ? scrollInfo.ScrollOwner : null; } - set { - if (scrollInfo != null) - scrollInfo.ScrollOwner = value; - else - scrollOwner = value; - } - } - - void IScrollInfo.LineUp() - { - if (scrollInfo != null) scrollInfo.LineUp(); - } - - void IScrollInfo.LineDown() - { - if (scrollInfo != null) scrollInfo.LineDown(); - } - - void IScrollInfo.LineLeft() - { - if (scrollInfo != null) scrollInfo.LineLeft(); - } - - void IScrollInfo.LineRight() - { - if (scrollInfo != null) scrollInfo.LineRight(); - } - - void IScrollInfo.PageUp() - { - if (scrollInfo != null) scrollInfo.PageUp(); - } - - void IScrollInfo.PageDown() - { - if (scrollInfo != null) scrollInfo.PageDown(); - } - - void IScrollInfo.PageLeft() - { - if (scrollInfo != null) scrollInfo.PageLeft(); - } - - void IScrollInfo.PageRight() - { - if (scrollInfo != null) scrollInfo.PageRight(); - } - - void IScrollInfo.MouseWheelUp() - { - if (scrollInfo != null) scrollInfo.MouseWheelUp(); - } - - void IScrollInfo.MouseWheelDown() - { - if (scrollInfo != null) scrollInfo.MouseWheelDown(); - } - - void IScrollInfo.MouseWheelLeft() - { - if (scrollInfo != null) scrollInfo.MouseWheelLeft(); - } - - void IScrollInfo.MouseWheelRight() - { - if (scrollInfo != null) scrollInfo.MouseWheelRight(); - } - - void IScrollInfo.SetHorizontalOffset(double offset) - { - if (scrollInfo != null) scrollInfo.SetHorizontalOffset(offset); - } - - void IScrollInfo.SetVerticalOffset(double offset) - { - if (scrollInfo != null) scrollInfo.SetVerticalOffset(offset); - } - - Rect IScrollInfo.MakeVisible(System.Windows.Media.Visual visual, Rect rectangle) - { - if (scrollInfo != null) - return scrollInfo.MakeVisible(visual, rectangle); - else - return Rect.Empty; - } - #endregion - - #region Focus Handling (Show/Hide Caret) - /// - protected override void OnMouseDown(MouseButtonEventArgs e) - { - base.OnMouseDown(e); - Focus(); - } - - /// - protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) - { - base.OnGotKeyboardFocus(e); - // First activate IME, then show caret - ime.OnGotKeyboardFocus(e); - caret.Show(); - } - - /// - protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) - { - base.OnLostKeyboardFocus(e); - caret.Hide(); - ime.OnLostKeyboardFocus(e); - } - #endregion - #region OnTextInput / RemoveSelectedText / ReplaceSelectionWithText - /// - /// Occurs when the TextArea receives text input. - /// This is like the event, - /// but occurs immediately before the TextArea handles the TextInput event. - /// - public event TextCompositionEventHandler TextEntering; - - /// - /// Occurs when the TextArea receives text input. - /// This is like the event, - /// but occurs immediately after the TextArea handles the TextInput event. - /// - public event TextCompositionEventHandler TextEntered; - - /// - /// Raises the TextEntering event. - /// - protected virtual void OnTextEntering(TextCompositionEventArgs e) - { - if (TextEntering != null) { - TextEntering(this, e); - } - } - - /// - /// Raises the TextEntered event. - /// - protected virtual void OnTextEntered(TextCompositionEventArgs e) - { - if (TextEntered != null) { - TextEntered(this, e); - } - } - - /// - protected override void OnTextInput(TextCompositionEventArgs e) - { - //Debug.WriteLine("TextInput: Text='" + e.Text + "' SystemText='" + e.SystemText + "' ControlText='" + e.ControlText + "'"); - base.OnTextInput(e); - if (!e.Handled && this.Document != null) { - if (string.IsNullOrEmpty(e.Text) || e.Text == "\x1b" || e.Text == "\b") { - // ASCII 0x1b = ESC. - // WPF produces a TextInput event with that old ASCII control char - // when Escape is pressed. We'll just ignore it. - - // A deadkey followed by backspace causes a textinput event for the BS character. - - // Similarly, some shortcuts like Alt+Space produce an empty TextInput event. - // We have to ignore those (not handle them) to keep the shortcut working. - return; - } - HideMouseCursor(); - PerformTextInput(e); - e.Handled = true; - } - } - - /// - /// Performs text input. - /// This raises the event, replaces the selection with the text, - /// and then raises the event. - /// - public void PerformTextInput(string text) - { - TextComposition textComposition = new TextComposition(InputManager.Current, this, text); - TextCompositionEventArgs e = new TextCompositionEventArgs(Keyboard.PrimaryDevice, textComposition); - e.RoutedEvent = TextInputEvent; - PerformTextInput(e); - } - - /// - /// Performs text input. - /// This raises the event, replaces the selection with the text, - /// and then raises the event. - /// - public void PerformTextInput(TextCompositionEventArgs e) - { - if (e == null) - throw new ArgumentNullException("e"); - if (this.Document == null) - throw ThrowUtil.NoDocumentAssigned(); - OnTextEntering(e); - if (!e.Handled) { - if (e.Text == "\n" || e.Text == "\r" || e.Text == "\r\n") - ReplaceSelectionWithNewLine(); - else { - if (OverstrikeMode && Selection.IsEmpty && Document.GetLineByNumber(Caret.Line).EndOffset > Caret.Offset) - EditingCommands.SelectRightByCharacter.Execute(null, this); - ReplaceSelectionWithText(e.Text); - } - OnTextEntered(e); - caret.BringCaretToView(); - } - } - void ReplaceSelectionWithNewLine() { string newLine = TextUtilities.GetNewLineFromDocument(this.Document, this.Caret.Line); @@ -975,6 +659,18 @@ internal ISegment[] GetDeletableSegments(ISegment segment) throw new InvalidOperationException("ReadOnlySectionProvider returned incorrect segments (outside of input segment / wrong order)"); return array; } + + /// Core text insertion used by both the shared path and the WPF TextComposition path. + internal void InsertText(string text) + { + if (text == "\n" || text == "\r" || text == "\r\n") + ReplaceSelectionWithNewLine(); + else { + if (OverstrikeMode && Selection.IsEmpty && Document.GetLineByNumber(Caret.Line).EndOffset > Caret.Offset) + EditingCommands.SelectRightByCharacter.Execute(null, this); + ReplaceSelectionWithText(text); + } + } #endregion #region IndentationStrategy property @@ -994,78 +690,6 @@ public IIndentationStrategy IndentationStrategy { } #endregion - #region OnKeyDown/OnKeyUp - /// - protected override void OnPreviewKeyDown(KeyEventArgs e) - { - base.OnPreviewKeyDown(e); - foreach (TextAreaStackedInputHandler h in stackedInputHandlers) { - if (e.Handled) - break; - h.OnPreviewKeyDown(e); - } - } - - /// - protected override void OnPreviewKeyUp(KeyEventArgs e) - { - base.OnPreviewKeyUp(e); - foreach (TextAreaStackedInputHandler h in stackedInputHandlers) { - if (e.Handled) - break; - h.OnPreviewKeyUp(e); - } - } - - // Make life easier for text editor extensions that use a different cursor based on the pressed modifier keys. - /// - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - TextView.InvalidateCursorIfMouseWithinTextView(); - } - - /// - protected override void OnKeyUp(KeyEventArgs e) - { - base.OnKeyUp(e); - TextView.InvalidateCursorIfMouseWithinTextView(); - } - #endregion - - #region Hide Mouse Cursor While Typing - - bool isMouseCursorHidden; - - void AttachTypingEvents() - { - // Use the PreviewMouseMove event in case some other editor layer consumes the MouseMove event (e.g. SD's InsertionCursorLayer) - this.MouseEnter += delegate { ShowMouseCursor(); }; - this.MouseLeave += delegate { ShowMouseCursor(); }; - this.PreviewMouseMove += delegate { ShowMouseCursor(); }; - this.TouchEnter += delegate { ShowMouseCursor(); }; - this.TouchLeave += delegate { ShowMouseCursor(); }; - this.PreviewTouchMove += delegate { ShowMouseCursor(); }; - } - - void ShowMouseCursor() - { - if (this.isMouseCursorHidden) { - System.Windows.Forms.Cursor.Show(); - this.isMouseCursorHidden = false; - } - } - - void HideMouseCursor() - { - if (Options.HideCursorWhileTyping && !this.isMouseCursorHidden && this.IsMouseOver) { - this.isMouseCursorHidden = true; - System.Windows.Forms.Cursor.Hide(); - } - } - - #endregion - #region Overstrike mode /// @@ -1085,33 +709,6 @@ public bool OverstrikeMode { #endregion - /// - protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer() - { - return new TextAreaAutomationPeer(this); - } - - /// - protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) - { - // accept clicks even where the text area draws no background - return new PointHitTestResult(this, hitTestParameters.HitPoint); - } - - /// - protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) - { - base.OnPropertyChanged(e); - if (e.Property == SelectionBrushProperty - || e.Property == SelectionBorderProperty - || e.Property == SelectionForegroundProperty - || e.Property == SelectionCornerRadiusProperty) { - textView.Redraw(); - } else if (e.Property == OverstrikeModeProperty) { - caret.UpdateIfVisible(); - } - } - /// /// Gets the requested service. /// diff --git a/ICSharpCode.AvalonEdit/Editing/TextArea.wpf.cs b/ICSharpCode.AvalonEdit/Editing/TextArea.wpf.cs new file mode 100644 index 00000000..afffb9d9 --- /dev/null +++ b/ICSharpCode.AvalonEdit/Editing/TextArea.wpf.cs @@ -0,0 +1,495 @@ +// 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.Windows; +using System.Windows.Automation.Peers; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; + +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Indentation; +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit.Editing +{ + public partial class TextArea : IScrollInfo, IWeakEventListener + { + internal ImeSupport ime; + + IScrollInfo scrollInfo; + ScrollViewer scrollOwner; + bool canVerticallyScroll, canHorizontallyScroll; + bool isMouseCursorHidden; + + static partial void InitializeWpfDefaults() + { + 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)); + } + + partial void InitializeIme() + { + ime = new ImeSupport(this); + } + + partial void AttachTypingEvents() + { + // Use the PreviewMouseMove event in case some other editor layer consumes the MouseMove event (e.g. SD's InsertionCursorLayer) + this.MouseEnter += delegate { ShowMouseCursor(); }; + this.MouseLeave += delegate { ShowMouseCursor(); }; + this.PreviewMouseMove += delegate { ShowMouseCursor(); }; + this.TouchEnter += delegate { ShowMouseCursor(); }; + this.TouchLeave += delegate { ShowMouseCursor(); }; + this.PreviewTouchMove += delegate { ShowMouseCursor(); }; + } + + partial void RequestSelectionValidationAsync() + { + Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(EnsureSelectionValid)); + } + + partial void InvalidateRequery() + { + CommandManager.InvalidateRequerySuggested(); + } + + partial void SubscribeToDocumentEvents(TextDocument document) + { + if (document != null) { + TextDocumentWeakEventManager.Changing.AddListener(document, this); + TextDocumentWeakEventManager.Changed.AddListener(document, this); + TextDocumentWeakEventManager.UpdateStarted.AddListener(document, this); + TextDocumentWeakEventManager.UpdateFinished.AddListener(document, this); + } + } + + partial void UnsubscribeFromDocumentEvents(TextDocument document) + { + if (document != null) { + TextDocumentWeakEventManager.Changing.RemoveListener(document, this); + TextDocumentWeakEventManager.Changed.RemoveListener(document, this); + TextDocumentWeakEventManager.UpdateStarted.RemoveListener(document, this); + TextDocumentWeakEventManager.UpdateFinished.RemoveListener(document, this); + } + } + + partial void SubscribeToOptionsEvents(TextEditorOptions options) + { + if (options != null) { + PropertyChangedWeakEventManager.AddListener(options, this); + } + } + + partial void UnsubscribeFromOptionsEvents(TextEditorOptions options) + { + if (options != null) { + PropertyChangedWeakEventManager.RemoveListener(options, this); + } + } + + void ShowMouseCursor() + { + if (this.isMouseCursorHidden) { + System.Windows.Forms.Cursor.Show(); + this.isMouseCursorHidden = false; + } + } + + void HideMouseCursor() + { + if (Options.HideCursorWhileTyping && !this.isMouseCursorHidden && this.IsMouseOver) { + this.isMouseCursorHidden = true; + System.Windows.Forms.Cursor.Hide(); + } + } + + void ApplyScrollInfo() + { + if (scrollInfo != null) { + scrollInfo.ScrollOwner = scrollOwner; + scrollInfo.CanVerticallyScroll = canVerticallyScroll; + scrollInfo.CanHorizontallyScroll = canHorizontallyScroll; + scrollOwner = null; + } + } + + #region ReceiveWeakEvent + /// + protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + if (managerType == typeof(TextDocumentWeakEventManager.Changing)) { + OnDocumentChanging(); + return true; + } else if (managerType == typeof(TextDocumentWeakEventManager.Changed)) { + OnDocumentChanged((DocumentChangeEventArgs)e); + return true; + } else if (managerType == typeof(TextDocumentWeakEventManager.UpdateStarted)) { + OnUpdateStarted(); + return true; + } else if (managerType == typeof(TextDocumentWeakEventManager.UpdateFinished)) { + OnUpdateFinished(); + return true; + } else if (managerType == typeof(PropertyChangedWeakEventManager)) { + OnOptionChanged((System.ComponentModel.PropertyChangedEventArgs)e); + return true; + } + return false; + } + + bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + return ReceiveWeakEvent(managerType, sender, e); + } + #endregion + + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + scrollInfo = textView; + ApplyScrollInfo(); + } + + #region IScrollInfo implementation + bool IScrollInfo.CanVerticallyScroll { + get { return scrollInfo != null ? scrollInfo.CanVerticallyScroll : false; } + set { + canVerticallyScroll = value; + if (scrollInfo != null) + scrollInfo.CanVerticallyScroll = value; + } + } + + bool IScrollInfo.CanHorizontallyScroll { + get { return scrollInfo != null ? scrollInfo.CanHorizontallyScroll : false; } + set { + canHorizontallyScroll = value; + if (scrollInfo != null) + scrollInfo.CanHorizontallyScroll = value; + } + } + + double IScrollInfo.ExtentWidth { + get { return scrollInfo != null ? scrollInfo.ExtentWidth : 0; } + } + + double IScrollInfo.ExtentHeight { + get { return scrollInfo != null ? scrollInfo.ExtentHeight : 0; } + } + + double IScrollInfo.ViewportWidth { + get { return scrollInfo != null ? scrollInfo.ViewportWidth : 0; } + } + + double IScrollInfo.ViewportHeight { + get { return scrollInfo != null ? scrollInfo.ViewportHeight : 0; } + } + + double IScrollInfo.HorizontalOffset { + get { return scrollInfo != null ? scrollInfo.HorizontalOffset : 0; } + } + + double IScrollInfo.VerticalOffset { + get { return scrollInfo != null ? scrollInfo.VerticalOffset : 0; } + } + + ScrollViewer IScrollInfo.ScrollOwner { + get { return scrollInfo != null ? scrollInfo.ScrollOwner : null; } + set { + if (scrollInfo != null) + scrollInfo.ScrollOwner = value; + else + scrollOwner = value; + } + } + + void IScrollInfo.LineUp() + { + if (scrollInfo != null) scrollInfo.LineUp(); + } + + void IScrollInfo.LineDown() + { + if (scrollInfo != null) scrollInfo.LineDown(); + } + + void IScrollInfo.LineLeft() + { + if (scrollInfo != null) scrollInfo.LineLeft(); + } + + void IScrollInfo.LineRight() + { + if (scrollInfo != null) scrollInfo.LineRight(); + } + + void IScrollInfo.PageUp() + { + if (scrollInfo != null) scrollInfo.PageUp(); + } + + void IScrollInfo.PageDown() + { + if (scrollInfo != null) scrollInfo.PageDown(); + } + + void IScrollInfo.PageLeft() + { + if (scrollInfo != null) scrollInfo.PageLeft(); + } + + void IScrollInfo.PageRight() + { + if (scrollInfo != null) scrollInfo.PageRight(); + } + + void IScrollInfo.MouseWheelUp() + { + if (scrollInfo != null) scrollInfo.MouseWheelUp(); + } + + void IScrollInfo.MouseWheelDown() + { + if (scrollInfo != null) scrollInfo.MouseWheelDown(); + } + + void IScrollInfo.MouseWheelLeft() + { + if (scrollInfo != null) scrollInfo.MouseWheelLeft(); + } + + void IScrollInfo.MouseWheelRight() + { + if (scrollInfo != null) scrollInfo.MouseWheelRight(); + } + + void IScrollInfo.SetHorizontalOffset(double offset) + { + if (scrollInfo != null) scrollInfo.SetHorizontalOffset(offset); + } + + void IScrollInfo.SetVerticalOffset(double offset) + { + if (scrollInfo != null) scrollInfo.SetVerticalOffset(offset); + } + + Rect IScrollInfo.MakeVisible(System.Windows.Media.Visual visual, Rect rectangle) + { + if (scrollInfo != null) + return scrollInfo.MakeVisible(visual, rectangle); + else + return Rect.Empty; + } + #endregion + + #region Focus Handling (Show/Hide Caret) + /// + protected override void OnMouseDown(MouseButtonEventArgs e) + { + base.OnMouseDown(e); + Focus(); + } + + /// + protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + base.OnGotKeyboardFocus(e); + // First activate IME, then show caret + ime.OnGotKeyboardFocus(e); + caret.Show(); + } + + /// + protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + base.OnLostKeyboardFocus(e); + caret.Hide(); + ime.OnLostKeyboardFocus(e); + } + #endregion + + #region OnTextInput / RemoveSelectedText / ReplaceSelectionWithText + /// + /// Occurs when the TextArea receives text input. + /// This is like the event, + /// but occurs immediately before the TextArea handles the TextInput event. + /// + public event TextCompositionEventHandler TextEntering; + + /// + /// Occurs when the TextArea receives text input. + /// This is like the event, + /// but occurs immediately after the TextArea handles the TextInput event. + /// + public event TextCompositionEventHandler TextEntered; + + /// + /// Raises the TextEntering event. + /// + protected virtual void OnTextEntering(TextCompositionEventArgs e) + { + if (TextEntering != null) { + TextEntering(this, e); + } + } + + /// + /// Raises the TextEntered event. + /// + protected virtual void OnTextEntered(TextCompositionEventArgs e) + { + if (TextEntered != null) { + TextEntered(this, e); + } + } + + /// + protected override void OnTextInput(TextCompositionEventArgs e) + { + //Debug.WriteLine("TextInput: Text='" + e.Text + "' SystemText='" + e.SystemText + "' ControlText='" + e.ControlText + "'"); + base.OnTextInput(e); + if (!e.Handled && this.Document != null) { + if (string.IsNullOrEmpty(e.Text) || e.Text == "\x1b" || e.Text == "\b") { + // ASCII 0x1b = ESC. + // WPF produces a TextInput event with that old ASCII control char + // when Escape is pressed. We'll just ignore it. + + // A deadkey followed by backspace causes a textinput event for the BS character. + + // Similarly, some shortcuts like Alt+Space produce an empty TextInput event. + // We have to ignore those (not handle them) to keep the shortcut working. + return; + } + HideMouseCursor(); + PerformTextInput(e); + e.Handled = true; + } + } + + /// + /// Performs text input. + /// This raises the event, replaces the selection with the text, + /// and then raises the event. + /// + public void PerformTextInput(string text) + { + TextComposition textComposition = new TextComposition(InputManager.Current, this, text); + TextCompositionEventArgs e = new TextCompositionEventArgs(Keyboard.PrimaryDevice, textComposition); + e.RoutedEvent = TextInputEvent; + PerformTextInput(e); + } + + /// + /// Performs text input. + /// This raises the event, replaces the selection with the text, + /// and then raises the event. + /// + public void PerformTextInput(TextCompositionEventArgs e) + { + if (e == null) + throw new ArgumentNullException("e"); + if (this.Document == null) + throw ThrowUtil.NoDocumentAssigned(); + OnTextEntering(e); + if (!e.Handled) { + InsertText(e.Text); + OnTextEntered(e); + caret.BringCaretToView(); + } + } + #endregion + + #region OnKeyDown/OnKeyUp + /// + protected override void OnPreviewKeyDown(KeyEventArgs e) + { + base.OnPreviewKeyDown(e); + foreach (TextAreaStackedInputHandler h in stackedInputHandlers) { + if (e.Handled) + break; + h.OnPreviewKeyDown(e); + } + } + + /// + protected override void OnPreviewKeyUp(KeyEventArgs e) + { + base.OnPreviewKeyUp(e); + foreach (TextAreaStackedInputHandler h in stackedInputHandlers) { + if (e.Handled) + break; + h.OnPreviewKeyUp(e); + } + } + + // Make life easier for text editor extensions that use a different cursor based on the pressed modifier keys. + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + TextView.InvalidateCursorIfMouseWithinTextView(); + } + + /// + protected override void OnKeyUp(KeyEventArgs e) + { + base.OnKeyUp(e); + TextView.InvalidateCursorIfMouseWithinTextView(); + } + #endregion + + /// + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TextAreaAutomationPeer(this); + } + + /// + protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) + { + // accept clicks even where the text area draws no background + return new PointHitTestResult(this, hitTestParameters.HitPoint); + } + + /// + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + if (e.Property == SelectionBrushProperty + || e.Property == SelectionBorderProperty + || e.Property == SelectionForegroundProperty + || e.Property == SelectionCornerRadiusProperty) { + textView.Redraw(); + } else if (e.Property == OverstrikeModeProperty) { + caret.UpdateIfVisible(); + } + } + } +} diff --git a/ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.cs b/ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.cs index e89de36d..e56870f5 100644 --- a/ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.cs +++ b/ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.cs @@ -18,20 +18,16 @@ using System; using System.Collections.Generic; -using System.Windows; -using System.Windows.Input; using System.Windows.Media; -using System.Windows.Media.TextFormatting; using ICSharpCode.AvalonEdit.Rendering; -using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Folding { /// /// A that produces line elements for folded s. /// - public sealed class FoldingElementGenerator : VisualLineElementGenerator, ITextViewConnect + public sealed partial class FoldingElementGenerator : VisualLineElementGenerator, ITextViewConnect { readonly List textViews = new List(); FoldingManager foldingManager; @@ -48,13 +44,14 @@ public FoldingManager FoldingManager { if (foldingManager != value) { if (foldingManager != null) { foreach (TextView v in textViews) - foldingManager.RemoveFromTextView(v); + RemoveFoldingManagerFromTextView(foldingManager, v); } foldingManager = value; if (foldingManager != null) { foreach (TextView v in textViews) - foldingManager.AddToTextView(v); + AddFoldingManagerToTextView(foldingManager, v); } + OnFoldingManagerChanged(); } } } @@ -63,14 +60,14 @@ void ITextViewConnect.AddToTextView(TextView textView) { textViews.Add(textView); if (foldingManager != null) - foldingManager.AddToTextView(textView); + AddFoldingManagerToTextView(foldingManager, textView); } void ITextViewConnect.RemoveFromTextView(TextView textView) { textViews.Remove(textView); if (foldingManager != null) - foldingManager.RemoveFromTextView(textView); + RemoveFoldingManagerFromTextView(foldingManager, textView); } #endregion @@ -79,8 +76,7 @@ public override void StartGeneration(ITextRunConstructionContext context) { base.StartGeneration(context); if (foldingManager != null) { - if (!foldingManager.textViews.Contains(context.TextView)) - throw new ArgumentException("Invalid TextView"); + ValidateTextView(context); if (context.Document != foldingManager.document) throw new ArgumentException("Invalid document"); } @@ -137,60 +133,17 @@ public override VisualLineElement ConstructElement(int offset) string title = foldingSection.Title; if (string.IsNullOrEmpty(title)) title = "..."; - var p = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties); - p.SetForegroundBrush(textBrush); - var textFormatter = TextFormatterFactory.Create(CurrentContext.TextView); - var text = FormattedTextElement.PrepareText(textFormatter, title, p); - return new FoldingLineElement(foldingSection, text, foldedUntil - offset) { textBrush = textBrush }; + return CreateFoldingElement(foldingSection, title, foldedUntil - offset); } else { return null; } } - sealed class FoldingLineElement : FormattedTextElement - { - readonly FoldingSection fs; - - internal Brush textBrush; - - public FoldingLineElement(FoldingSection fs, TextLine text, int documentLength) : base(text, documentLength) - { - this.fs = fs; - } - - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - return new FoldingLineTextRun(this, this.TextRunProperties) { textBrush = textBrush }; - } - - protected internal override void OnMouseDown(MouseButtonEventArgs e) - { - if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left) { - fs.IsFolded = false; - e.Handled = true; - } else { - base.OnMouseDown(e); - } - } - } - - sealed class FoldingLineTextRun : FormattedTextRun - { - internal Brush textBrush; - - public FoldingLineTextRun(FormattedTextElement element, TextRunProperties properties) - : base(element, properties) - { - } - - public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways) - { - var metrics = Format(double.PositiveInfinity); - Rect r = new Rect(origin.X, origin.Y - metrics.Baseline, metrics.Width, metrics.Height); - drawingContext.DrawRectangle(null, new Pen(textBrush, 1), r); - base.Draw(drawingContext, origin, rightToLeft, sideways); - } - } + partial void AddFoldingManagerToTextView(FoldingManager manager, TextView textView); + partial void RemoveFoldingManagerFromTextView(FoldingManager manager, TextView textView); + partial void ValidateTextView(ITextRunConstructionContext context); + partial void OnFoldingManagerChanged(); + private partial VisualLineElement CreateFoldingElement(FoldingSection foldingSection, string title, int documentLength); /// /// Default brush for folding element text. Value: Brushes.Gray diff --git a/ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.wpf.cs b/ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.wpf.cs new file mode 100644 index 00000000..b3190f50 --- /dev/null +++ b/ICSharpCode.AvalonEdit/Folding/FoldingElementGenerator.wpf.cs @@ -0,0 +1,102 @@ +// 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.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; + +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit.Folding +{ + public sealed partial class FoldingElementGenerator + { + partial void AddFoldingManagerToTextView(FoldingManager manager, TextView textView) + { + manager.AddToTextView(textView); + } + + partial void RemoveFoldingManagerFromTextView(FoldingManager manager, TextView textView) + { + manager.RemoveFromTextView(textView); + } + + partial void ValidateTextView(ITextRunConstructionContext context) + { + if (!foldingManager.textViews.Contains(context.TextView)) + throw new ArgumentException("Invalid TextView"); + } + + private partial VisualLineElement CreateFoldingElement(FoldingSection foldingSection, string title, int documentLength) + { + var p = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties); + p.SetForegroundBrush(textBrush); + var textFormatter = TextFormatterFactory.Create(CurrentContext.TextView); + var text = FormattedTextElement.PrepareText(textFormatter, title, p); + return new FoldingLineElement(foldingSection, text, documentLength) { textBrush = textBrush }; + } + + sealed class FoldingLineElement : FormattedTextElement + { + readonly FoldingSection fs; + + internal Brush textBrush; + + public FoldingLineElement(FoldingSection fs, TextLine text, int documentLength) : base(text, documentLength) + { + this.fs = fs; + } + + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + return new FoldingLineTextRun(this, this.TextRunProperties) { textBrush = textBrush }; + } + + protected internal override void OnMouseDown(MouseButtonEventArgs e) + { + if (e.ClickCount == 2 && e.ChangedButton == MouseButton.Left) { + fs.IsFolded = false; + e.Handled = true; + } else { + base.OnMouseDown(e); + } + } + } + + sealed class FoldingLineTextRun : FormattedTextRun + { + internal Brush textBrush; + + public FoldingLineTextRun(FormattedTextElement element, TextRunProperties properties) + : base(element, properties) + { + } + + public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways) + { + var metrics = Format(double.PositiveInfinity); + Rect r = new Rect(origin.X, origin.Y - metrics.Baseline, metrics.Width, metrics.Height); + drawingContext.DrawRectangle(null, new Pen(textBrush, 1), r); + base.Draw(drawingContext, origin, rightToLeft, sideways); + } + } + } +} diff --git a/ICSharpCode.AvalonEdit/Folding/FoldingManager.cs b/ICSharpCode.AvalonEdit/Folding/FoldingManager.cs index 27ae5bd4..d339fc13 100644 --- a/ICSharpCode.AvalonEdit/Folding/FoldingManager.cs +++ b/ICSharpCode.AvalonEdit/Folding/FoldingManager.cs @@ -23,23 +23,31 @@ using System.Windows; using ICSharpCode.AvalonEdit.Document; -using ICSharpCode.AvalonEdit.Editing; -using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Folding { /// - /// Stores a list of foldings for a specific TextView and TextDocument. + /// Stores a list of foldings for a specific TextDocument. /// - public class FoldingManager : IWeakEventListener + public partial class FoldingManager : IWeakEventListener { internal readonly TextDocument document; - - internal readonly List textViews = new List(); readonly TextSegmentCollection foldings; bool isFirstUpdate = true; + /// + /// Raised whenever the set of foldings or the IsFolded state changes. + /// + public event EventHandler FoldingsChanged; + + /// + /// Gets the document whose foldings are tracked. + /// + public TextDocument Document { + get { return document; } + } + #region Constructor /// /// Creates a new FoldingManager instance. @@ -78,57 +86,14 @@ void OnDocumentChanged(DocumentChangeEventArgs e) // extend end offset to the end of the line (including delimiter) var endLine = document.GetLineByOffset(newEndOffset); newEndOffset = endLine.Offset + endLine.TotalLength; - foreach (var affectedFolding in foldings.FindOverlappingSegments(e.Offset, newEndOffset - e.Offset)) { + foreach (var affectedFolding in foldings.FindOverlappingSegments(e.Offset, newEndOffset - e.Offset).ToList()) { if (affectedFolding.Length == 0) { RemoveFolding(affectedFolding); } else { affectedFolding.ValidateCollapsedLineSections(); } } - } - #endregion - - #region Manage TextViews - internal void AddToTextView(TextView textView) - { - if (textView == null || textViews.Contains(textView)) - throw new ArgumentException(); - textViews.Add(textView); - foreach (FoldingSection fs in foldings) { - if (fs.collapsedSections != null) { - Array.Resize(ref fs.collapsedSections, textViews.Count); - fs.ValidateCollapsedLineSections(); - } - } - } - - internal void RemoveFromTextView(TextView textView) - { - int pos = textViews.IndexOf(textView); - if (pos < 0) - throw new ArgumentException(); - textViews.RemoveAt(pos); - foreach (FoldingSection fs in foldings) { - if (fs.collapsedSections != null) { - var c = new CollapsedLineSection[textViews.Count]; - Array.Copy(fs.collapsedSections, 0, c, 0, pos); - fs.collapsedSections[pos].Uncollapse(); - Array.Copy(fs.collapsedSections, pos + 1, c, pos, c.Length - pos); - fs.collapsedSections = c; - } - } - } - - internal void Redraw() - { - foreach (TextView textView in textViews) - textView.Redraw(); - } - - internal void Redraw(FoldingSection fs) - { - foreach (TextView textView in textViews) - textView.Redraw(fs); + RaiseFoldingsChanged(); } #endregion @@ -144,7 +109,7 @@ public FoldingSection CreateFolding(int startOffset, int endOffset) throw new ArgumentException("Folding must be within document boundary"); FoldingSection fs = new FoldingSection(this, startOffset, endOffset); foldings.Add(fs); - Redraw(fs); + RaiseFoldingsChanged(fs); return fs; } @@ -157,7 +122,7 @@ public void RemoveFolding(FoldingSection fs) throw new ArgumentNullException("fs"); fs.IsFolded = false; foldings.Remove(fs); - Redraw(fs); + RaiseFoldingsChanged(fs); } /// @@ -169,7 +134,7 @@ public void Clear() foreach (FoldingSection s in foldings) s.IsFolded = false; foldings.Clear(); - Redraw(); + RaiseFoldingsChanged(); } #endregion @@ -272,10 +237,10 @@ public void UpdateFoldings(IEnumerable newFoldings, int firstErrorOf // reuse current folding if its matching: if (oldFoldingIndex < oldFoldings.Length && newFolding.StartOffset == oldFoldings[oldFoldingIndex].StartOffset) { section = oldFoldings[oldFoldingIndex++]; - section.Length = newFolding.EndOffset - newFolding.StartOffset; + section.Length = endOffset - startOffset; } else { // no matching current folding; create a new one: - section = this.CreateFolding(newFolding.StartOffset, newFolding.EndOffset); + section = this.CreateFolding(startOffset, endOffset); // auto-close #regions only when opening the document if (isFirstUpdate) { section.IsFolded = newFolding.DefaultClosed; @@ -295,103 +260,17 @@ public void UpdateFoldings(IEnumerable newFoldings, int firstErrorOf } #endregion - #region Install - /// - /// Adds Folding support to the specified text area. - /// Warning: The folding manager is only valid for the text area's current document. The folding manager - /// must be uninstalled before the text area is bound to a different document. - /// - /// The that manages the list of foldings inside the text area. - public static FoldingManager Install(TextArea textArea) + internal void RaiseFoldingsChanged() { - if (textArea == null) - throw new ArgumentNullException("textArea"); - return new FoldingManagerInstallation(textArea); + RaiseFoldingsChanged(null); } - /// - /// Uninstalls the folding manager. - /// - /// The specified manager was not created using . - public static void Uninstall(FoldingManager manager) + internal void RaiseFoldingsChanged(FoldingSection fs) { - if (manager == null) - throw new ArgumentNullException("manager"); - FoldingManagerInstallation installation = manager as FoldingManagerInstallation; - if (installation != null) { - installation.Uninstall(); - } else { - throw new ArgumentException("FoldingManager was not created using FoldingManager.Install"); - } + FoldingsChanged?.Invoke(this, EventArgs.Empty); + OnFoldingsChanged(fs); } - sealed class FoldingManagerInstallation : FoldingManager - { - TextArea textArea; - FoldingMargin margin; - FoldingElementGenerator generator; - - public FoldingManagerInstallation(TextArea textArea) : base(textArea.Document) - { - this.textArea = textArea; - margin = new FoldingMargin() { FoldingManager = this }; - generator = new FoldingElementGenerator() { FoldingManager = this }; - textArea.LeftMargins.Add(margin); - textArea.TextView.Services.AddService(typeof(FoldingManager), this); - // HACK: folding only works correctly when it has highest priority - textArea.TextView.ElementGenerators.Insert(0, generator); - textArea.Caret.PositionChanged += textArea_Caret_PositionChanged; - } - - /* - void DemoMode() - { - foldingGenerator = new FoldingElementGenerator() { FoldingManager = fm }; - foldingMargin = new FoldingMargin { FoldingManager = fm }; - foldingMarginBorder = new Border { - Child = foldingMargin, - Background = new LinearGradientBrush(Colors.White, Colors.Transparent, 0) - }; - foldingMarginBorder.SizeChanged += UpdateTextViewClip; - textEditor.TextArea.TextView.ElementGenerators.Add(foldingGenerator); - textEditor.TextArea.LeftMargins.Add(foldingMarginBorder); - } - - void UpdateTextViewClip(object sender, SizeChangedEventArgs e) - { - textEditor.TextArea.TextView.Clip = new RectangleGeometry( - new Rect(-foldingMarginBorder.ActualWidth, - 0, - textEditor.TextArea.TextView.ActualWidth + foldingMarginBorder.ActualWidth, - textEditor.TextArea.TextView.ActualHeight)); - } - */ - - public void Uninstall() - { - Clear(); - if (textArea != null) { - textArea.Caret.PositionChanged -= textArea_Caret_PositionChanged; - textArea.LeftMargins.Remove(margin); - textArea.TextView.ElementGenerators.Remove(generator); - textArea.TextView.Services.RemoveService(typeof(FoldingManager)); - margin = null; - generator = null; - textArea = null; - } - } - - void textArea_Caret_PositionChanged(object sender, EventArgs e) - { - // Expand Foldings when Caret is moved into them. - int caretOffset = textArea.Caret.Offset; - foreach (FoldingSection s in GetFoldingsContaining(caretOffset)) { - if (s.IsFolded && s.StartOffset < caretOffset && caretOffset < s.EndOffset) { - s.IsFolded = false; - } - } - } - } - #endregion + partial void OnFoldingsChanged(FoldingSection fs); } } diff --git a/ICSharpCode.AvalonEdit/Folding/FoldingManager.wpf.cs b/ICSharpCode.AvalonEdit/Folding/FoldingManager.wpf.cs new file mode 100644 index 00000000..db85ffa9 --- /dev/null +++ b/ICSharpCode.AvalonEdit/Folding/FoldingManager.wpf.cs @@ -0,0 +1,154 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; + +using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Rendering; + +namespace ICSharpCode.AvalonEdit.Folding +{ + public partial class FoldingManager + { + internal readonly List textViews = new List(); + + internal void AddToTextView(TextView textView) + { + if (textView == null || textViews.Contains(textView)) + throw new ArgumentException(); + textViews.Add(textView); + foreach (FoldingSection fs in AllFoldings) { + if (fs.collapsedSections != null) { + Array.Resize(ref fs.collapsedSections, textViews.Count); + fs.ValidateCollapsedLineSections(); + } + } + } + + internal void RemoveFromTextView(TextView textView) + { + int pos = textViews.IndexOf(textView); + if (pos < 0) + throw new ArgumentException(); + textViews.RemoveAt(pos); + foreach (FoldingSection fs in AllFoldings) { + if (fs.collapsedSections != null) { + var c = new CollapsedLineSection[textViews.Count]; + Array.Copy(fs.collapsedSections, 0, c, 0, pos); + fs.collapsedSections[pos].Uncollapse(); + Array.Copy(fs.collapsedSections, pos + 1, c, pos, c.Length - pos); + fs.collapsedSections = c; + } + } + } + + internal void Redraw() + { + foreach (TextView textView in textViews) + textView.Redraw(); + } + + internal void Redraw(FoldingSection fs) + { + foreach (TextView textView in textViews) + textView.Redraw(fs); + } + + partial void OnFoldingsChanged(FoldingSection fs) + { + if (fs != null) + Redraw(fs); + else + Redraw(); + } + + /// + /// Adds Folding support to the specified text area. + /// Warning: The folding manager is only valid for the text area's current document. The folding manager + /// must be uninstalled before the text area is bound to a different document. + /// + /// The that manages the list of foldings inside the text area. + public static FoldingManager Install(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + return new FoldingManagerInstallation(textArea); + } + + /// + /// Uninstalls the folding manager. + /// + /// The specified manager was not created using . + public static void Uninstall(FoldingManager manager) + { + if (manager == null) + throw new ArgumentNullException("manager"); + FoldingManagerInstallation installation = manager as FoldingManagerInstallation; + if (installation != null) { + installation.Uninstall(); + } else { + throw new ArgumentException("FoldingManager was not created using FoldingManager.Install"); + } + } + + sealed class FoldingManagerInstallation : FoldingManager + { + TextArea textArea; + FoldingMargin margin; + FoldingElementGenerator generator; + + public FoldingManagerInstallation(TextArea textArea) : base(textArea.Document) + { + this.textArea = textArea; + margin = new FoldingMargin() { FoldingManager = this }; + generator = new FoldingElementGenerator() { FoldingManager = this }; + textArea.LeftMargins.Add(margin); + textArea.TextView.Services.AddService(typeof(FoldingManager), this); + // HACK: folding only works correctly when it has highest priority + textArea.TextView.ElementGenerators.Insert(0, generator); + textArea.Caret.PositionChanged += textArea_Caret_PositionChanged; + } + + public void Uninstall() + { + Clear(); + if (textArea != null) { + textArea.Caret.PositionChanged -= textArea_Caret_PositionChanged; + textArea.LeftMargins.Remove(margin); + textArea.TextView.ElementGenerators.Remove(generator); + textArea.TextView.Services.RemoveService(typeof(FoldingManager)); + margin = null; + generator = null; + textArea = null; + } + } + + void textArea_Caret_PositionChanged(object sender, EventArgs e) + { + // Expand Foldings when Caret is moved into them. + int caretOffset = textArea.Caret.Offset; + foreach (FoldingSection s in GetFoldingsContaining(caretOffset)) { + if (s.IsFolded && s.StartOffset < caretOffset && caretOffset < s.EndOffset) { + s.IsFolded = false; + } + } + } + } + } +} diff --git a/ICSharpCode.AvalonEdit/Folding/FoldingSection.cs b/ICSharpCode.AvalonEdit/Folding/FoldingSection.cs index 3dc6d971..d85122f2 100644 --- a/ICSharpCode.AvalonEdit/Folding/FoldingSection.cs +++ b/ICSharpCode.AvalonEdit/Folding/FoldingSection.cs @@ -19,19 +19,16 @@ using System.Diagnostics; using ICSharpCode.AvalonEdit.Document; -using ICSharpCode.AvalonEdit.Rendering; -using ICSharpCode.AvalonEdit.Utils; namespace ICSharpCode.AvalonEdit.Folding { /// /// A section that can be folded. /// - public sealed class FoldingSection : TextSegment + public sealed partial class FoldingSection : TextSegment { readonly FoldingManager manager; bool isFolded; - internal CollapsedLineSection[] collapsedSections; string title; /// @@ -42,43 +39,19 @@ public bool IsFolded { set { if (isFolded != value) { isFolded = value; - ValidateCollapsedLineSections(); // create/destroy CollapsedLineSection - manager.Redraw(this); + ValidateCollapsedLineSections(); + manager.RaiseFoldingsChanged(this); } } } internal void ValidateCollapsedLineSections() { - if (!isFolded) { - RemoveCollapsedLineSection(); - return; - } - // It is possible that StartOffset/EndOffset get set to invalid values via the property setters in TextSegment, - // so we coerce those values into the valid range. - DocumentLine startLine = manager.document.GetLineByOffset(StartOffset.CoerceValue(0, manager.document.TextLength)); - DocumentLine endLine = manager.document.GetLineByOffset(EndOffset.CoerceValue(0, manager.document.TextLength)); - if (startLine == endLine) { - RemoveCollapsedLineSection(); - } else { - if (collapsedSections == null) - collapsedSections = new CollapsedLineSection[manager.textViews.Count]; - // Validate collapsed line sections - DocumentLine startLinePlusOne = startLine.NextLine; - for (int i = 0; i < collapsedSections.Length; i++) { - var collapsedSection = collapsedSections[i]; - if (collapsedSection == null || collapsedSection.Start != startLinePlusOne || collapsedSection.End != endLine) { - // recreate this collapsed section - if (collapsedSection != null) { - Debug.WriteLine("CollapsedLineSection validation - recreate collapsed section from " + startLinePlusOne + " to " + endLine); - collapsedSection.Uncollapse(); - } - collapsedSections[i] = manager.textViews[i].CollapseLines(startLinePlusOne, endLine); - } - } - } + ValidateCollapsedLineSectionsCore(); } + partial void ValidateCollapsedLineSectionsCore(); + /// protected override void OnSegmentChanged() { @@ -86,7 +59,7 @@ protected override void OnSegmentChanged() base.OnSegmentChanged(); // don't redraw if the FoldingSection wasn't added to the FoldingManager's collection yet if (IsConnectedToCollection) - manager.Redraw(this); + manager.RaiseFoldingsChanged(this); } /// @@ -100,7 +73,7 @@ public string Title { if (title != value) { title = value; if (this.IsFolded) - manager.Redraw(this); + manager.RaiseFoldingsChanged(this); } } } @@ -126,16 +99,5 @@ internal FoldingSection(FoldingManager manager, int startOffset, int endOffset) this.StartOffset = startOffset; this.Length = endOffset - startOffset; } - - void RemoveCollapsedLineSection() - { - if (collapsedSections != null) { - foreach (var collapsedSection in collapsedSections) { - if (collapsedSection != null && collapsedSection.Start != null) - collapsedSection.Uncollapse(); - } - collapsedSections = null; - } - } } } diff --git a/ICSharpCode.AvalonEdit/Folding/FoldingSection.wpf.cs b/ICSharpCode.AvalonEdit/Folding/FoldingSection.wpf.cs new file mode 100644 index 00000000..89a3a410 --- /dev/null +++ b/ICSharpCode.AvalonEdit/Folding/FoldingSection.wpf.cs @@ -0,0 +1,73 @@ +// 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.Diagnostics; + +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit.Folding +{ + public sealed partial class FoldingSection + { + internal CollapsedLineSection[] collapsedSections; + + partial void ValidateCollapsedLineSectionsCore() + { + if (!IsFolded) { + RemoveCollapsedLineSection(); + return; + } + // It is possible that StartOffset/EndOffset get set to invalid values via the property setters in TextSegment, + // so we coerce those values into the valid range. + DocumentLine startLine = manager.document.GetLineByOffset(StartOffset.CoerceValue(0, manager.document.TextLength)); + DocumentLine endLine = manager.document.GetLineByOffset(EndOffset.CoerceValue(0, manager.document.TextLength)); + if (startLine == endLine) { + RemoveCollapsedLineSection(); + } else { + if (collapsedSections == null) + collapsedSections = new CollapsedLineSection[manager.textViews.Count]; + // Validate collapsed line sections + DocumentLine startLinePlusOne = startLine.NextLine; + for (int i = 0; i < collapsedSections.Length; i++) { + var collapsedSection = collapsedSections[i]; + if (collapsedSection == null || collapsedSection.Start != startLinePlusOne || collapsedSection.End != endLine) { + // recreate this collapsed section + if (collapsedSection != null) { + Debug.WriteLine("CollapsedLineSection validation - recreate collapsed section from " + startLinePlusOne + " to " + endLine); + collapsedSection.Uncollapse(); + } + collapsedSections[i] = manager.textViews[i].CollapseLines(startLinePlusOne, endLine); + } + } + } + } + + void RemoveCollapsedLineSection() + { + if (collapsedSections != null) { + foreach (var collapsedSection in collapsedSections) { + if (collapsedSection != null && collapsedSection.Start != null) + collapsedSection.Uncollapse(); + } + collapsedSections = null; + } + } + } +} diff --git a/ICSharpCode.AvalonEdit/Highlighting/Xshd/XshdColor.cs b/ICSharpCode.AvalonEdit/Highlighting/Xshd/XshdColor.cs index 18a4000a..12e00915 100644 --- a/ICSharpCode.AvalonEdit/Highlighting/Xshd/XshdColor.cs +++ b/ICSharpCode.AvalonEdit/Highlighting/Xshd/XshdColor.cs @@ -98,7 +98,11 @@ protected XshdColor(SerializationInfo info, StreamingContext context) this.Foreground = (HighlightingBrush)info.GetValue("Foreground", typeof(HighlightingBrush)); this.Background = (HighlightingBrush)info.GetValue("Background", typeof(HighlightingBrush)); if (info.GetBoolean("HasWeight")) - this.FontWeight = System.Windows.FontWeight.FromOpenTypeWeight(info.GetInt32("Weight")); +#if HAS_UNO || WINDOWS_APP_SDK + this.FontWeight = global::Windows.UI.Text.FontWeight.FromOpenTypeWeight(info.GetInt32("Weight")); +#else + this.FontWeight = System.Windows.FontWeight.FromOpenTypeWeight(info.GetInt32("Weight")); +#endif if (info.GetBoolean("HasStyle")) this.FontStyle = (FontStyle?)new FontStyleConverter().ConvertFromInvariantString(info.GetString("Style")); this.ExampleText = info.GetString("ExampleText"); diff --git a/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index caf7d97d..56413051 100644 --- a/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -48,6 +48,7 @@ TRACE + 9.0 diff --git a/ICSharpCode.AvalonEdit/Rendering/LayerPosition.cs b/ICSharpCode.AvalonEdit/Rendering/LayerPosition.cs index 479c763b..97a00ef1 100644 --- a/ICSharpCode.AvalonEdit/Rendering/LayerPosition.cs +++ b/ICSharpCode.AvalonEdit/Rendering/LayerPosition.cs @@ -70,10 +70,12 @@ public enum LayerInsertionPosition Above } - sealed class LayerPosition : IComparable + sealed partial class LayerPosition : IComparable { internal static readonly DependencyProperty LayerPositionProperty = - DependencyProperty.RegisterAttached("LayerPosition", typeof(LayerPosition), typeof(LayerPosition)); + RegisterLayerPositionProperty(); + + private static partial DependencyProperty RegisterLayerPositionProperty(); public static void SetLayerPosition(UIElement layer, LayerPosition value) { diff --git a/ICSharpCode.AvalonEdit/Rendering/LayerPosition.wpf.cs b/ICSharpCode.AvalonEdit/Rendering/LayerPosition.wpf.cs new file mode 100644 index 00000000..faad3982 --- /dev/null +++ b/ICSharpCode.AvalonEdit/Rendering/LayerPosition.wpf.cs @@ -0,0 +1,30 @@ +// 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.Windows; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + partial class LayerPosition + { + private static partial DependencyProperty RegisterLayerPositionProperty() + { + return DependencyProperty.RegisterAttached("LayerPosition", typeof(LayerPosition), typeof(LayerPosition)); + } + } +} diff --git a/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs b/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs index 66dc8371..aaa4b1af 100644 --- a/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs +++ b/ICSharpCode.AvalonEdit/Rendering/VisualLine.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team -// +// 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 @@ -20,11 +20,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; -using System.Linq; -using System.Windows; using System.Windows.Documents; -using System.Windows.Media; -using System.Windows.Media.TextFormatting; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Utils; @@ -36,9 +32,9 @@ namespace ICSharpCode.AvalonEdit.Rendering /// A visual line usually corresponds to one DocumentLine, but it can span multiple lines if /// all but the first are collapsed. /// - public sealed class VisualLine + public partial class VisualLine { - enum LifetimePhase : byte + internal enum LifetimePhase : byte { Generating, Transforming, @@ -46,10 +42,10 @@ enum LifetimePhase : byte Disposed } - TextView textView; - List elements; + internal TextView textView; + internal List elements; internal bool hasInlineObjects; - LifetimePhase phase; + internal LifetimePhase phase; /// /// Gets the document to which this VisualLine belongs. @@ -64,25 +60,12 @@ enum LifetimePhase : byte /// /// Gets the last document line displayed by this visual line. /// - public DocumentLine LastDocumentLine { get; private set; } + public DocumentLine LastDocumentLine { get; internal set; } /// /// Gets a read-only collection of line elements. /// - public ReadOnlyCollection Elements { get; private set; } - - ReadOnlyCollection textLines; - - /// - /// Gets a read-only collection of text lines. - /// - public ReadOnlyCollection TextLines { - get { - if (phase < LifetimePhase.Live) - throw new InvalidOperationException(); - return textLines; - } - } + public ReadOnlyCollection Elements { get; internal set; } /// /// Gets the start offset of the VisualLine inside the document. @@ -97,7 +80,7 @@ public int StartOffset { /// /// Length in visual line coordinates. /// - public int VisualLength { get; private set; } + public int VisualLength { get; internal set; } /// /// Length in visual line coordinates including the end of line marker, if TextEditorOptions.ShowEndOfLine is enabled. @@ -113,7 +96,7 @@ public int VisualLengthWithEndOfLineMarker { /// /// Gets the height of the visual line in device-independent pixels. /// - public double Height { get; private set; } + public double Height { get; internal set; } /// /// Gets the Y position of the line. This is measured in device-independent pixels relative to the start of the document. @@ -129,84 +112,7 @@ internal VisualLine(TextView textView, DocumentLine firstDocumentLine) this.FirstDocumentLine = firstDocumentLine; } - internal void ConstructVisualElements(ITextRunConstructionContext context, VisualLineElementGenerator[] generators) - { - Debug.Assert(phase == LifetimePhase.Generating); - foreach (VisualLineElementGenerator g in generators) { - g.StartGeneration(context); - } - elements = new List(); - PerformVisualElementConstruction(generators); - foreach (VisualLineElementGenerator g in generators) { - g.FinishGeneration(); - } - - var globalTextRunProperties = context.GlobalTextRunProperties; - foreach (var element in elements) { - element.SetTextRunProperties(new VisualLineElementTextRunProperties(globalTextRunProperties)); - } - this.Elements = elements.AsReadOnly(); - CalculateOffsets(); - phase = LifetimePhase.Transforming; - } - - void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) - { - TextDocument document = this.Document; - int offset = FirstDocumentLine.Offset; - int currentLineEnd = offset + FirstDocumentLine.Length; - LastDocumentLine = FirstDocumentLine; - int askInterestOffset = 0; // 0 or 1 - while (offset + askInterestOffset <= currentLineEnd) { - int textPieceEndOffset = currentLineEnd; - foreach (VisualLineElementGenerator g in generators) { - g.cachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset); - if (g.cachedInterest != -1) { - if (g.cachedInterest < offset) - throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset", - g.cachedInterest, - "GetFirstInterestedOffset must not return an offset less than startOffset. Return -1 to signal no interest."); - if (g.cachedInterest < textPieceEndOffset) - textPieceEndOffset = g.cachedInterest; - } - } - Debug.Assert(textPieceEndOffset >= offset); - if (textPieceEndOffset > offset) { - int textPieceLength = textPieceEndOffset - offset; - elements.Add(new VisualLineText(this, textPieceLength)); - offset = textPieceEndOffset; - } - // If no elements constructed / only zero-length elements constructed: - // do not asking the generators again for the same location (would cause endless loop) - askInterestOffset = 1; - foreach (VisualLineElementGenerator g in generators) { - if (g.cachedInterest == offset) { - VisualLineElement element = g.ConstructElement(offset); - if (element != null) { - elements.Add(element); - if (element.DocumentLength > 0) { - // a non-zero-length element was constructed - askInterestOffset = 0; - offset += element.DocumentLength; - if (offset > currentLineEnd) { - DocumentLine newEndLine = document.GetLineByOffset(offset); - currentLineEnd = newEndLine.Offset + newEndLine.Length; - this.LastDocumentLine = newEndLine; - if (currentLineEnd < offset) { - throw new InvalidOperationException( - "The VisualLineElementGenerator " + g.GetType().Name + - " produced an element which ends within the line delimiter"); - } - } - break; - } - } - } - } - } - } - - void CalculateOffsets() + internal void CalculateOffsets() { int visualOffset = 0; int textOffset = 0; @@ -220,24 +126,6 @@ void CalculateOffsets() Debug.Assert(textOffset == LastDocumentLine.EndOffset - FirstDocumentLine.Offset); } - internal void RunTransformers(ITextRunConstructionContext context, IVisualLineTransformer[] transformers) - { - Debug.Assert(phase == LifetimePhase.Transforming); - foreach (IVisualLineTransformer transformer in transformers) { - transformer.Transform(context, elements); - } - // For some strange reason, WPF requires that either all or none of the typography properties are set. - if (elements.Any(e => e.TextRunProperties.TypographyProperties != null)) { - // Fix typographic properties - foreach (VisualLineElement element in elements) { - if (element.TextRunProperties.TypographyProperties == null) { - element.TextRunProperties.SetTypographyProperties(new DefaultTextRunTypographyProperties()); - } - } - } - phase = LifetimePhase.Live; - } - /// /// Replaces the single element at with the specified elements. /// The replacement operation must preserve the document length, but may change the visual length. @@ -276,14 +164,6 @@ public void ReplaceElement(int elementIndex, int count, params VisualLineElement CalculateOffsets(); } - internal void SetTextLines(List textLines) - { - this.textLines = textLines.AsReadOnly(); - Height = 0; - foreach (TextLine line in textLines) - Height += line.Height; - } - /// /// Gets the visual column from a document offset relative to the first line start. /// @@ -316,180 +196,6 @@ public int GetRelativeOffset(int visualColumn) return documentLength; } - /// - /// Gets the text line containing the specified visual column. - /// - public TextLine GetTextLine(int visualColumn) - { - return GetTextLine(visualColumn, false); - } - - /// - /// Gets the text line containing the specified visual column. - /// - public TextLine GetTextLine(int visualColumn, bool isAtEndOfLine) - { - if (visualColumn < 0) - throw new ArgumentOutOfRangeException("visualColumn"); - if (visualColumn >= VisualLengthWithEndOfLineMarker) - return TextLines[TextLines.Count - 1]; - foreach (TextLine line in TextLines) { - if (isAtEndOfLine ? visualColumn <= line.Length : visualColumn < line.Length) - return line; - else - visualColumn -= line.Length; - } - throw new InvalidOperationException("Shouldn't happen (VisualLength incorrect?)"); - } - - /// - /// Gets the visual top from the specified text line. - /// - /// Distance in device-independent pixels - /// from the top of the document to the top of the specified text line. - public double GetTextLineVisualYPosition(TextLine textLine, VisualYPosition yPositionMode) - { - if (textLine == null) - throw new ArgumentNullException("textLine"); - double pos = VisualTop; - foreach (TextLine tl in TextLines) { - if (tl == textLine) { - switch (yPositionMode) { - case VisualYPosition.LineTop: - return pos; - case VisualYPosition.LineMiddle: - return pos + tl.Height / 2; - case VisualYPosition.LineBottom: - return pos + tl.Height; - case VisualYPosition.TextTop: - return pos + tl.Baseline - textView.DefaultBaseline; - case VisualYPosition.TextBottom: - return pos + tl.Baseline - textView.DefaultBaseline + textView.DefaultLineHeight; - case VisualYPosition.TextMiddle: - return pos + tl.Baseline - textView.DefaultBaseline + textView.DefaultLineHeight / 2; - case VisualYPosition.Baseline: - return pos + tl.Baseline; - default: - throw new ArgumentException("Invalid yPositionMode:" + yPositionMode); - } - } else { - pos += tl.Height; - } - } - throw new ArgumentException("textLine is not a line in this VisualLine"); - } - - /// - /// Gets the start visual column from the specified text line. - /// - public int GetTextLineVisualStartColumn(TextLine textLine) - { - if (!TextLines.Contains(textLine)) - throw new ArgumentException("textLine is not a line in this VisualLine"); - int col = 0; - foreach (TextLine tl in TextLines) { - if (tl == textLine) - break; - else - col += tl.Length; - } - return col; - } - - /// - /// Gets a TextLine by the visual position. - /// - public TextLine GetTextLineByVisualYPosition(double visualTop) - { - const double epsilon = 0.0001; - double pos = this.VisualTop; - foreach (TextLine tl in TextLines) { - pos += tl.Height; - if (visualTop + epsilon < pos) - return tl; - } - return TextLines[TextLines.Count - 1]; - } - - /// - /// Gets the visual position from the specified visualColumn. - /// - /// Position in device-independent pixels - /// relative to the top left of the document. - public Point GetVisualPosition(int visualColumn, VisualYPosition yPositionMode) - { - TextLine textLine = GetTextLine(visualColumn); - double xPos = GetTextLineVisualXPosition(textLine, visualColumn); - double yPos = GetTextLineVisualYPosition(textLine, yPositionMode); - return new Point(xPos, yPos); - } - - internal Point GetVisualPosition(int visualColumn, bool isAtEndOfLine, VisualYPosition yPositionMode) - { - TextLine textLine = GetTextLine(visualColumn, isAtEndOfLine); - double xPos = GetTextLineVisualXPosition(textLine, visualColumn); - double yPos = GetTextLineVisualYPosition(textLine, yPositionMode); - return new Point(xPos, yPos); - } - - /// - /// Gets the distance to the left border of the text area of the specified visual column. - /// The visual column must belong to the specified text line. - /// - public double GetTextLineVisualXPosition(TextLine textLine, int visualColumn) - { - if (textLine == null) - throw new ArgumentNullException("textLine"); - double xPos = textLine.GetDistanceFromCharacterHit( - new CharacterHit(Math.Min(visualColumn, VisualLengthWithEndOfLineMarker), 0)); - if (visualColumn > VisualLengthWithEndOfLineMarker) { - xPos += (visualColumn - VisualLengthWithEndOfLineMarker) * textView.WideSpaceWidth; - } - return xPos; - } - - /// - /// Gets the visual column from a document position (relative to top left of the document). - /// If the user clicks between two visual columns, rounds to the nearest column. - /// - public int GetVisualColumn(Point point) - { - return GetVisualColumn(point, textView.Options.EnableVirtualSpace); - } - - /// - /// Gets the visual column from a document position (relative to top left of the document). - /// If the user clicks between two visual columns, rounds to the nearest column. - /// - public int GetVisualColumn(Point point, bool allowVirtualSpace) - { - return GetVisualColumn(GetTextLineByVisualYPosition(point.Y), point.X, allowVirtualSpace); - } - - internal int GetVisualColumn(Point point, bool allowVirtualSpace, out bool isAtEndOfLine) - { - var textLine = GetTextLineByVisualYPosition(point.Y); - int vc = GetVisualColumn(textLine, point.X, allowVirtualSpace); - isAtEndOfLine = (vc >= GetTextLineVisualStartColumn(textLine) + textLine.Length); - return vc; - } - - /// - /// Gets the visual column from a document position (relative to top left of the document). - /// If the user clicks between two visual columns, rounds to the nearest column. - /// - public int GetVisualColumn(TextLine textLine, double xPos, bool allowVirtualSpace) - { - if (xPos > textLine.WidthIncludingTrailingWhitespace) { - if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) { - int virtualX = (int)Math.Round((xPos - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth, MidpointRounding.AwayFromZero); - return VisualLengthWithEndOfLineMarker + virtualX; - } - } - CharacterHit ch = textLine.GetCharacterHitFromDistance(xPos); - return ch.FirstCharacterIndex + ch.TrailingLength; - } - /// /// Validates the visual column and returns the correct one. /// @@ -520,47 +226,6 @@ public int ValidateVisualColumn(int offset, int visualColumn, bool allowVirtualS return visualColumn; } - /// - /// Gets the visual column from a document position (relative to top left of the document). - /// If the user clicks between two visual columns, returns the first of those columns. - /// - public int GetVisualColumnFloor(Point point) - { - return GetVisualColumnFloor(point, textView.Options.EnableVirtualSpace); - } - - /// - /// Gets the visual column from a document position (relative to top left of the document). - /// If the user clicks between two visual columns, returns the first of those columns. - /// - public int GetVisualColumnFloor(Point point, bool allowVirtualSpace) - { - bool tmp; - return GetVisualColumnFloor(point, allowVirtualSpace, out tmp); - } - - internal int GetVisualColumnFloor(Point point, bool allowVirtualSpace, out bool isAtEndOfLine) - { - TextLine textLine = GetTextLineByVisualYPosition(point.Y); - if (point.X > textLine.WidthIncludingTrailingWhitespace) { - isAtEndOfLine = true; - if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) { - // clicking virtual space in the last line - int virtualX = (int)((point.X - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth); - return VisualLengthWithEndOfLineMarker + virtualX; - } else { - // GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character in line - // and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case - // specially and return the line's end column instead. - return GetTextLineVisualStartColumn(textLine) + textLine.Length; - } - } else { - isAtEndOfLine = false; - } - CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X); - return ch.FirstCharacterIndex; - } - /// /// Gets the text view position from the specified visual column. /// @@ -570,40 +235,6 @@ public TextViewPosition GetTextViewPosition(int visualColumn) return new TextViewPosition(this.Document.GetLocation(documentOffset), visualColumn); } - /// - /// Gets the text view position from the specified visual position. - /// If the position is within a character, it is rounded to the next character boundary. - /// - /// The position in WPF device-independent pixels relative - /// to the top left corner of the document. - /// Controls whether positions in virtual space may be returned. - public TextViewPosition GetTextViewPosition(Point visualPosition, bool allowVirtualSpace) - { - bool isAtEndOfLine; - int visualColumn = GetVisualColumn(visualPosition, allowVirtualSpace, out isAtEndOfLine); - int documentOffset = GetRelativeOffset(visualColumn) + this.FirstDocumentLine.Offset; - TextViewPosition pos = new TextViewPosition(this.Document.GetLocation(documentOffset), visualColumn); - pos.IsAtEndOfLine = isAtEndOfLine; - return pos; - } - - /// - /// Gets the text view position from the specified visual position. - /// If the position is inside a character, the position in front of the character is returned. - /// - /// The position in WPF device-independent pixels relative - /// to the top left corner of the document. - /// Controls whether positions in virtual space may be returned. - public TextViewPosition GetTextViewPositionFloor(Point visualPosition, bool allowVirtualSpace) - { - bool isAtEndOfLine; - int visualColumn = GetVisualColumnFloor(visualPosition, allowVirtualSpace, out isAtEndOfLine); - int documentOffset = GetRelativeOffset(visualColumn) + this.FirstDocumentLine.Offset; - TextViewPosition pos = new TextViewPosition(this.Document.GetLocation(documentOffset), visualColumn); - pos.IsAtEndOfLine = isAtEndOfLine; - return pos; - } - /// /// Gets whether the visual line was disposed. /// @@ -611,15 +242,15 @@ public bool IsDisposed { get { return phase == LifetimePhase.Disposed; } } + partial void DisposeCore(); + internal void Dispose() { if (phase == LifetimePhase.Disposed) return; Debug.Assert(phase == LifetimePhase.Live); phase = LifetimePhase.Disposed; - foreach (TextLine textLine in TextLines) { - textLine.Dispose(); - } + DisposeCore(); } /// @@ -655,7 +286,6 @@ public int GetNextCaretPosition(int visualColumn, LogicalDirection direction, Ca if (direction == LogicalDirection.Backward) { // Search Backwards: // If the last element doesn't handle line borders, return the line end as caret stop - if (visualColumn > this.VisualLength && !elements[elements.Count - 1].HandlesLineBorders && HasImplicitStopAtLineEnd(mode)) { if (allowVirtualSpace) return visualColumn - 1; @@ -726,50 +356,5 @@ static bool HasImplicitStopAtLineEnd(CaretPositioningMode mode) { return true; } - - VisualLineDrawingVisual visual; - - internal VisualLineDrawingVisual Render() - { - Debug.Assert(phase == LifetimePhase.Live); - if (visual == null) - visual = new VisualLineDrawingVisual(this, textView.FlowDirection); - return visual; - } - } - - sealed class VisualLineDrawingVisual : DrawingVisual - { - public readonly VisualLine VisualLine; - public readonly double Height; - internal bool IsAdded; - - public VisualLineDrawingVisual(VisualLine visualLine, FlowDirection flow) - { - this.VisualLine = visualLine; - var drawingContext = RenderOpen(); - double pos = 0; - foreach (TextLine textLine in visualLine.TextLines) { - if (flow == FlowDirection.LeftToRight) { - textLine.Draw(drawingContext, new Point(0, pos), InvertAxes.None); - } else { - // Invert Axis for RightToLeft (Arabic language) support - textLine.Draw(drawingContext, new Point(0, pos), InvertAxes.Horizontal); - } - pos += textLine.Height; - } - this.Height = pos; - drawingContext.Close(); - } - - protected override GeometryHitTestResult HitTestCore(GeometryHitTestParameters hitTestParameters) - { - return null; - } - - protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) - { - return null; - } } } diff --git a/ICSharpCode.AvalonEdit/Rendering/VisualLine.wpf.cs b/ICSharpCode.AvalonEdit/Rendering/VisualLine.wpf.cs new file mode 100644 index 00000000..40a00d58 --- /dev/null +++ b/ICSharpCode.AvalonEdit/Rendering/VisualLine.wpf.cs @@ -0,0 +1,452 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; + +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + public partial class VisualLine + { + ReadOnlyCollection textLines; + VisualLineDrawingVisual visual; + + /// + /// Gets a read-only collection of text lines. + /// + public ReadOnlyCollection TextLines { + get { + if (phase < LifetimePhase.Live) + throw new InvalidOperationException(); + return textLines; + } + } + + internal void ConstructVisualElements(ITextRunConstructionContext context, VisualLineElementGenerator[] generators) + { + Debug.Assert(phase == LifetimePhase.Generating); + foreach (VisualLineElementGenerator g in generators) { + g.StartGeneration(context); + } + elements = new List(); + PerformVisualElementConstruction(generators); + foreach (VisualLineElementGenerator g in generators) { + g.FinishGeneration(); + } + + var globalTextRunProperties = context.GlobalTextRunProperties; + foreach (var element in elements) { + element.SetTextRunProperties(new VisualLineElementTextRunProperties(globalTextRunProperties)); + } + this.Elements = elements.AsReadOnly(); + CalculateOffsets(); + phase = LifetimePhase.Transforming; + } + + void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) + { + TextDocument document = this.Document; + int offset = FirstDocumentLine.Offset; + int currentLineEnd = offset + FirstDocumentLine.Length; + LastDocumentLine = FirstDocumentLine; + int askInterestOffset = 0; // 0 or 1 + while (offset + askInterestOffset <= currentLineEnd) { + int textPieceEndOffset = currentLineEnd; + foreach (VisualLineElementGenerator g in generators) { + g.cachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset); + if (g.cachedInterest != -1) { + if (g.cachedInterest < offset) + throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset", + g.cachedInterest, + "GetFirstInterestedOffset must not return an offset less than startOffset. Return -1 to signal no interest."); + if (g.cachedInterest < textPieceEndOffset) + textPieceEndOffset = g.cachedInterest; + } + } + Debug.Assert(textPieceEndOffset >= offset); + if (textPieceEndOffset > offset) { + int textPieceLength = textPieceEndOffset - offset; + elements.Add(new VisualLineText(this, textPieceLength)); + offset = textPieceEndOffset; + } + // If no elements constructed / only zero-length elements constructed: + // do not asking the generators again for the same location (would cause endless loop) + askInterestOffset = 1; + foreach (VisualLineElementGenerator g in generators) { + if (g.cachedInterest == offset) { + VisualLineElement element = g.ConstructElement(offset); + if (element != null) { + elements.Add(element); + if (element.DocumentLength > 0) { + // a non-zero-length element was constructed + askInterestOffset = 0; + offset += element.DocumentLength; + if (offset > currentLineEnd) { + DocumentLine newEndLine = document.GetLineByOffset(offset); + currentLineEnd = newEndLine.Offset + newEndLine.Length; + this.LastDocumentLine = newEndLine; + if (currentLineEnd < offset) { + throw new InvalidOperationException( + "The VisualLineElementGenerator " + g.GetType().Name + + " produced an element which ends within the line delimiter"); + } + } + break; + } + } + } + } + } + } + + internal void RunTransformers(ITextRunConstructionContext context, IVisualLineTransformer[] transformers) + { + Debug.Assert(phase == LifetimePhase.Transforming); + foreach (IVisualLineTransformer transformer in transformers) { + transformer.Transform(context, elements); + } + // For some strange reason, WPF requires that either all or none of the typography properties are set. + if (elements.Any(e => e.TextRunProperties.TypographyProperties != null)) { + // Fix typographic properties + foreach (VisualLineElement element in elements) { + if (element.TextRunProperties.TypographyProperties == null) { + element.TextRunProperties.SetTypographyProperties(new DefaultTextRunTypographyProperties()); + } + } + } + phase = LifetimePhase.Live; + } + + internal void SetTextLines(List textLines) + { + this.textLines = textLines.AsReadOnly(); + Height = 0; + foreach (TextLine line in textLines) + Height += line.Height; + } + + partial void DisposeCore() + { + foreach (TextLine textLine in textLines) { + textLine.Dispose(); + } + } + + /// + /// Gets the text line containing the specified visual column. + /// + public TextLine GetTextLine(int visualColumn) + { + return GetTextLine(visualColumn, false); + } + + /// + /// Gets the text line containing the specified visual column. + /// + public TextLine GetTextLine(int visualColumn, bool isAtEndOfLine) + { + if (visualColumn < 0) + throw new ArgumentOutOfRangeException("visualColumn"); + if (visualColumn >= VisualLengthWithEndOfLineMarker) + return TextLines[TextLines.Count - 1]; + foreach (TextLine line in TextLines) { + if (isAtEndOfLine ? visualColumn <= line.Length : visualColumn < line.Length) + return line; + else + visualColumn -= line.Length; + } + throw new InvalidOperationException("Shouldn't happen (VisualLength incorrect?)"); + } + + /// + /// Gets the visual top from the specified text line. + /// + /// Distance in device-independent pixels + /// from the top of the document to the top of the specified text line. + public double GetTextLineVisualYPosition(TextLine textLine, VisualYPosition yPositionMode) + { + if (textLine == null) + throw new ArgumentNullException("textLine"); + double pos = VisualTop; + foreach (TextLine tl in TextLines) { + if (tl == textLine) { + switch (yPositionMode) { + case VisualYPosition.LineTop: + return pos; + case VisualYPosition.LineMiddle: + return pos + tl.Height / 2; + case VisualYPosition.LineBottom: + return pos + tl.Height; + case VisualYPosition.TextTop: + return pos + tl.Baseline - textView.DefaultBaseline; + case VisualYPosition.TextBottom: + return pos + tl.Baseline - textView.DefaultBaseline + textView.DefaultLineHeight; + case VisualYPosition.TextMiddle: + return pos + tl.Baseline - textView.DefaultBaseline + textView.DefaultLineHeight / 2; + case VisualYPosition.Baseline: + return pos + tl.Baseline; + default: + throw new ArgumentException("Invalid yPositionMode:" + yPositionMode); + } + } else { + pos += tl.Height; + } + } + throw new ArgumentException("textLine is not a line in this VisualLine"); + } + + /// + /// Gets the start visual column from the specified text line. + /// + public int GetTextLineVisualStartColumn(TextLine textLine) + { + if (!TextLines.Contains(textLine)) + throw new ArgumentException("textLine is not a line in this VisualLine"); + int col = 0; + foreach (TextLine tl in TextLines) { + if (tl == textLine) + break; + else + col += tl.Length; + } + return col; + } + + /// + /// Gets a TextLine by the visual position. + /// + public TextLine GetTextLineByVisualYPosition(double visualTop) + { + const double epsilon = 0.0001; + double pos = this.VisualTop; + foreach (TextLine tl in TextLines) { + pos += tl.Height; + if (visualTop + epsilon < pos) + return tl; + } + return TextLines[TextLines.Count - 1]; + } + + /// + /// Gets the visual position from the specified visualColumn. + /// + /// Position in device-independent pixels + /// relative to the top left of the document. + public Point GetVisualPosition(int visualColumn, VisualYPosition yPositionMode) + { + TextLine textLine = GetTextLine(visualColumn); + double xPos = GetTextLineVisualXPosition(textLine, visualColumn); + double yPos = GetTextLineVisualYPosition(textLine, yPositionMode); + return new Point(xPos, yPos); + } + + internal Point GetVisualPosition(int visualColumn, bool isAtEndOfLine, VisualYPosition yPositionMode) + { + TextLine textLine = GetTextLine(visualColumn, isAtEndOfLine); + double xPos = GetTextLineVisualXPosition(textLine, visualColumn); + double yPos = GetTextLineVisualYPosition(textLine, yPositionMode); + return new Point(xPos, yPos); + } + + /// + /// Gets the distance to the left border of the text area of the specified visual column. + /// The visual column must belong to the specified text line. + /// + public double GetTextLineVisualXPosition(TextLine textLine, int visualColumn) + { + if (textLine == null) + throw new ArgumentNullException("textLine"); + double xPos = textLine.GetDistanceFromCharacterHit( + new CharacterHit(Math.Min(visualColumn, VisualLengthWithEndOfLineMarker), 0)); + if (visualColumn > VisualLengthWithEndOfLineMarker) { + xPos += (visualColumn - VisualLengthWithEndOfLineMarker) * textView.WideSpaceWidth; + } + return xPos; + } + + /// + /// Gets the visual column from a document position (relative to top left of the document). + /// If the user clicks between two visual columns, rounds to the nearest column. + /// + public int GetVisualColumn(Point point) + { + return GetVisualColumn(point, textView.Options.EnableVirtualSpace); + } + + /// + /// Gets the visual column from a document position (relative to top left of the document). + /// If the user clicks between two visual columns, rounds to the nearest column. + /// + public int GetVisualColumn(Point point, bool allowVirtualSpace) + { + return GetVisualColumn(GetTextLineByVisualYPosition(point.Y), point.X, allowVirtualSpace); + } + + internal int GetVisualColumn(Point point, bool allowVirtualSpace, out bool isAtEndOfLine) + { + var textLine = GetTextLineByVisualYPosition(point.Y); + int vc = GetVisualColumn(textLine, point.X, allowVirtualSpace); + isAtEndOfLine = (vc >= GetTextLineVisualStartColumn(textLine) + textLine.Length); + return vc; + } + + /// + /// Gets the visual column from a document position (relative to top left of the document). + /// If the user clicks between two visual columns, rounds to the nearest column. + /// + public int GetVisualColumn(TextLine textLine, double xPos, bool allowVirtualSpace) + { + if (xPos > textLine.WidthIncludingTrailingWhitespace) { + if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) { + int virtualX = (int)Math.Round((xPos - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth, MidpointRounding.AwayFromZero); + return VisualLengthWithEndOfLineMarker + virtualX; + } + } + CharacterHit ch = textLine.GetCharacterHitFromDistance(xPos); + return ch.FirstCharacterIndex + ch.TrailingLength; + } + + /// + /// Gets the visual column from a document position (relative to top left of the document). + /// If the user clicks between two visual columns, returns the first of those columns. + /// + public int GetVisualColumnFloor(Point point) + { + return GetVisualColumnFloor(point, textView.Options.EnableVirtualSpace); + } + + /// + /// Gets the visual column from a document position (relative to top left of the document). + /// If the user clicks between two visual columns, returns the first of those columns. + /// + public int GetVisualColumnFloor(Point point, bool allowVirtualSpace) + { + bool tmp; + return GetVisualColumnFloor(point, allowVirtualSpace, out tmp); + } + + internal int GetVisualColumnFloor(Point point, bool allowVirtualSpace, out bool isAtEndOfLine) + { + TextLine textLine = GetTextLineByVisualYPosition(point.Y); + if (point.X > textLine.WidthIncludingTrailingWhitespace) { + isAtEndOfLine = true; + if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) { + // clicking virtual space in the last line + int virtualX = (int)((point.X - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth); + return VisualLengthWithEndOfLineMarker + virtualX; + } else { + // GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character in line + // and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case + // specially and return the line's end column instead. + return GetTextLineVisualStartColumn(textLine) + textLine.Length; + } + } else { + isAtEndOfLine = false; + } + CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X); + return ch.FirstCharacterIndex; + } + + /// + /// Gets the text view position from the specified visual position. + /// If the position is within a character, it is rounded to the next character boundary. + /// + /// The position in WPF device-independent pixels relative + /// to the top left corner of the document. + /// Controls whether positions in virtual space may be returned. + public TextViewPosition GetTextViewPosition(Point visualPosition, bool allowVirtualSpace) + { + bool isAtEndOfLine; + int visualColumn = GetVisualColumn(visualPosition, allowVirtualSpace, out isAtEndOfLine); + int documentOffset = GetRelativeOffset(visualColumn) + this.FirstDocumentLine.Offset; + TextViewPosition pos = new TextViewPosition(this.Document.GetLocation(documentOffset), visualColumn); + pos.IsAtEndOfLine = isAtEndOfLine; + return pos; + } + + /// + /// Gets the text view position from the specified visual position. + /// If the position is inside a character, the position in front of the character is returned. + /// + /// The position in WPF device-independent pixels relative + /// to the top left corner of the document. + /// Controls whether positions in virtual space may be returned. + public TextViewPosition GetTextViewPositionFloor(Point visualPosition, bool allowVirtualSpace) + { + bool isAtEndOfLine; + int visualColumn = GetVisualColumnFloor(visualPosition, allowVirtualSpace, out isAtEndOfLine); + int documentOffset = GetRelativeOffset(visualColumn) + this.FirstDocumentLine.Offset; + TextViewPosition pos = new TextViewPosition(this.Document.GetLocation(documentOffset), visualColumn); + pos.IsAtEndOfLine = isAtEndOfLine; + return pos; + } + + internal VisualLineDrawingVisual Render() + { + Debug.Assert(phase == LifetimePhase.Live); + if (visual == null) + visual = new VisualLineDrawingVisual(this, textView.FlowDirection); + return visual; + } + } + + sealed class VisualLineDrawingVisual : DrawingVisual + { + public readonly VisualLine VisualLine; + public readonly double Height; + internal bool IsAdded; + + public VisualLineDrawingVisual(VisualLine visualLine, FlowDirection flow) + { + this.VisualLine = visualLine; + var drawingContext = RenderOpen(); + double pos = 0; + foreach (TextLine textLine in visualLine.TextLines) { + if (flow == FlowDirection.LeftToRight) { + textLine.Draw(drawingContext, new Point(0, pos), InvertAxes.None); + } else { + // Invert Axis for RightToLeft (Arabic language) support + textLine.Draw(drawingContext, new Point(0, pos), InvertAxes.Horizontal); + } + pos += textLine.Height; + } + this.Height = pos; + drawingContext.Close(); + } + + protected override GeometryHitTestResult HitTestCore(GeometryHitTestParameters hitTestParameters) + { + return null; + } + + protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) + { + return null; + } + } +} diff --git a/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.cs b/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.cs index f5dd7642..ee0d88c6 100644 --- a/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.cs +++ b/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.cs @@ -17,19 +17,14 @@ // DEALINGS IN THE SOFTWARE. using System; -using System.Diagnostics; -using System.Windows; -using System.Windows.Documents; -using System.Windows.Input; using System.Windows.Media.TextFormatting; -using System.Windows.Navigation; namespace ICSharpCode.AvalonEdit.Rendering { /// /// VisualLineElement that represents a piece of text and is a clickable link. /// - public class VisualLineLinkText : VisualLineText + public partial class VisualLineLinkText : VisualLineText { /// /// Gets/Sets the URL that is navigated to when the link is clicked. @@ -63,57 +58,17 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio this.TextRunProperties.SetForegroundBrush(context.TextView.LinkTextForegroundBrush); this.TextRunProperties.SetBackgroundBrush(context.TextView.LinkTextBackgroundBrush); if (context.TextView.LinkTextUnderline) - this.TextRunProperties.SetTextDecorations(TextDecorations.Underline); + ApplyLinkTextDecorations(); return base.CreateTextRun(startVisualColumn, context); } /// /// Gets whether the link is currently clickable. /// - /// Returns true when control is pressed; or when - /// is disabled. - protected virtual bool LinkIsClickable() - { - if (NavigateUri == null) - return false; - if (RequireControlModifierForClick) - return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control; - else - return true; - } + /// Platform partials refine this when modifier-state information is available. + protected virtual partial bool LinkIsClickable(); - /// - protected internal override void OnQueryCursor(QueryCursorEventArgs e) - { - if (LinkIsClickable()) { - e.Handled = true; - e.Cursor = Cursors.Hand; - } - } - - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", - Justification = "I've seen Process.Start throw undocumented exceptions when the mail client / web browser is installed incorrectly")] - protected internal override void OnMouseDown(MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left && !e.Handled && LinkIsClickable()) { - RequestNavigateEventArgs args = new RequestNavigateEventArgs(this.NavigateUri, this.TargetName); - args.RoutedEvent = Hyperlink.RequestNavigateEvent; - FrameworkElement element = e.Source as FrameworkElement; - if (element != null) { - // allow user code to handle the navigation request - element.RaiseEvent(args); - } - if (!args.Handled) { - try { - Process.Start(new ProcessStartInfo { FileName = this.NavigateUri.ToString(), UseShellExecute = true }); - } catch { - // ignore all kinds of errors during web browser start - } - } - e.Handled = true; - } - } + partial void ApplyLinkTextDecorations(); /// protected override VisualLineText CreateInstance(int length) diff --git a/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.wpf.cs b/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.wpf.cs new file mode 100644 index 00000000..2e67670e --- /dev/null +++ b/ICSharpCode.AvalonEdit/Rendering/VisualLineLinkText.wpf.cs @@ -0,0 +1,82 @@ +// 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.Diagnostics; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Navigation; + +namespace ICSharpCode.AvalonEdit.Rendering +{ + public partial class VisualLineLinkText + { + /// + /// Gets whether the link is currently clickable. + /// + /// Returns true when control is pressed; or when + /// is disabled. + protected virtual partial bool LinkIsClickable() + { + if (NavigateUri == null) + return false; + if (RequireControlModifierForClick) + return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control; + else + return true; + } + + partial void ApplyLinkTextDecorations() + { + this.TextRunProperties.SetTextDecorations(TextDecorations.Underline); + } + + /// + protected internal override void OnQueryCursor(QueryCursorEventArgs e) + { + if (LinkIsClickable()) { + e.Handled = true; + e.Cursor = Cursors.Hand; + } + } + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", + Justification = "I've seen Process.Start throw undocumented exceptions when the mail client / web browser is installed incorrectly")] + protected internal override void OnMouseDown(MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left && !e.Handled && LinkIsClickable()) { + RequestNavigateEventArgs args = new RequestNavigateEventArgs(this.NavigateUri, this.TargetName); + args.RoutedEvent = Hyperlink.RequestNavigateEvent; + FrameworkElement element = e.Source as FrameworkElement; + if (element != null) { + // allow user code to handle the navigation request + element.RaiseEvent(args); + } + if (!args.Handled) { + try { + Process.Start(new ProcessStartInfo { FileName = this.NavigateUri.ToString(), UseShellExecute = true }); + } catch { + // ignore all kinds of errors during web browser start + } + } + e.Handled = true; + } + } + } +} diff --git a/ICSharpCode.AvalonEdit/TextEditor.cs b/ICSharpCode.AvalonEdit/TextEditor.cs index 5325ed84..aa5e6c2c 100644 --- a/ICSharpCode.AvalonEdit/TextEditor.cs +++ b/ICSharpCode.AvalonEdit/TextEditor.cs @@ -1,14 +1,14 @@ -// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team -// +// 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 @@ -22,13 +22,7 @@ using System.Linq; using System.Text; using System.Windows; -using System.Windows.Controls; -using System.Windows.Controls.Primitives; -using System.Windows.Data; -using System.Windows.Input; -using System.Windows.Markup; using System.Windows.Media; -using System.Windows.Shapes; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Editing; @@ -42,18 +36,23 @@ namespace ICSharpCode.AvalonEdit /// The text editor control. /// Contains a scrollable TextArea. /// - [Localizability(LocalizationCategory.Text), ContentProperty("Text")] - public class TextEditor : Control, ITextEditorComponent, IServiceProvider, IWeakEventListener + public partial class TextEditor : ITextEditorComponent, IServiceProvider { #region Constructors static TextEditor() { - DefaultStyleKeyProperty.OverrideMetadata(typeof(TextEditor), - new FrameworkPropertyMetadata(typeof(TextEditor))); - FocusableProperty.OverrideMetadata(typeof(TextEditor), - new FrameworkPropertyMetadata(Boxes.True)); + InitializeWpfDefaults(); } + static partial void InitializeWpfDefaults(); + + // Platform-specific hooks for property-changed callbacks. + partial void OnDocumentChangedCore(TextDocument oldValue, TextDocument newValue); + partial void OnOptionsChangedCore(TextEditorOptions oldValue, TextEditorOptions newValue); + partial void OnIsReadOnlyChangedCore(TextEditor editor, bool oldValue, bool newValue); + partial void OnShowLineNumbersChangedCore(TextEditor editor, bool oldValue, bool newValue); + partial void OnLineNumbersForegroundChangedCore(TextEditor editor, object newValue); + /// /// Creates a new TextEditor instance. /// @@ -75,33 +74,11 @@ protected TextEditor(TextArea textArea) SetCurrentValue(OptionsProperty, textArea.Options); SetCurrentValue(DocumentProperty, new TextDocument()); } - #endregion - /// - protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer() - { - return new TextEditorAutomationPeer(this); - } - - /// Forward focus to TextArea. - /// - protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) - { - base.OnGotKeyboardFocus(e); - if (e.NewFocus == this) { - Keyboard.Focus(textArea); - e.Handled = true; - } - } - #region Document property - /// - /// Document property. - /// - public static readonly DependencyProperty DocumentProperty - = TextView.DocumentProperty.AddOwner( - typeof(TextEditor), new FrameworkPropertyMetadata(OnDocumentChanged)); + // DocumentProperty is declared in TextEditor.wpf.cs (WPF: AddOwner from TextView) + // or TextEditor.uno.cs (Uno: DependencyProperty.Register) for each platform. /// /// Gets/Sets the document displayed by the text editor. @@ -134,26 +111,16 @@ static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEven void OnDocumentChanged(TextDocument oldValue, TextDocument newValue) { - if (oldValue != null) { - TextDocumentWeakEventManager.TextChanged.RemoveListener(oldValue, this); - PropertyChangedEventManager.RemoveListener(oldValue.UndoStack, this, "IsOriginalFile"); - } + OnDocumentChangedCore(oldValue, newValue); textArea.Document = newValue; - if (newValue != null) { - TextDocumentWeakEventManager.TextChanged.AddListener(newValue, this); - PropertyChangedEventManager.AddListener(newValue.UndoStack, this, "IsOriginalFile"); - } OnDocumentChanged(EventArgs.Empty); OnTextChanged(EventArgs.Empty); } #endregion #region Options property - /// - /// Options property. - /// - public static readonly DependencyProperty OptionsProperty - = TextView.OptionsProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(OnOptionsChanged)); + // OptionsProperty is declared in TextEditor.wpf.cs (WPF: AddOwner from TextView) + // or TextEditor.uno.cs (Uno: DependencyProperty.Register) for each platform. /// /// Gets/Sets the options currently used by the text editor. @@ -185,42 +152,17 @@ static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEvent void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue) { - if (oldValue != null) { - PropertyChangedWeakEventManager.RemoveListener(oldValue, this); - } + OnOptionsChangedCore(oldValue, newValue); textArea.Options = newValue; - if (newValue != null) { - PropertyChangedWeakEventManager.AddListener(newValue, this); - } OnOptionChanged(new PropertyChangedEventArgs(null)); } - - /// - protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) - { - if (managerType == typeof(PropertyChangedWeakEventManager)) { - OnOptionChanged((PropertyChangedEventArgs)e); - return true; - } else if (managerType == typeof(TextDocumentWeakEventManager.TextChanged)) { - OnTextChanged(e); - return true; - } else if (managerType == typeof(PropertyChangedEventManager)) { - return HandleIsOriginalChanged((PropertyChangedEventArgs)e); - } - return false; - } - - bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) - { - return ReceiveWeakEvent(managerType, sender, e); - } #endregion #region Text property /// /// Gets/Sets the text of the current document. /// - [Localizability(LocalizationCategory.Text), DefaultValue("")] + [DefaultValue("")] public string Text { get { TextDocument document = this.Document; @@ -260,44 +202,19 @@ protected virtual void OnTextChanged(EventArgs e) } #endregion - #region TextArea / ScrollViewer properties + #region TextArea property readonly TextArea textArea; - ScrollViewer scrollViewer; - - /// - /// Is called after the template was applied. - /// - public override void OnApplyTemplate() - { - base.OnApplyTemplate(); - scrollViewer = (ScrollViewer)Template.FindName("PART_ScrollViewer", this); - } /// /// Gets the text area. /// public TextArea TextArea { - get { - return textArea; - } + get { return textArea; } } - /// - /// Gets the scroll viewer used by the text editor. - /// This property can return null if the template has not been applied / does not contain a scroll viewer. - /// - internal ScrollViewer ScrollViewer { - get { return scrollViewer; } - } - - bool CanExecute(RoutedUICommand command) - { - return command.CanExecute(null, textArea); - } - - void Execute(RoutedUICommand command) + object IServiceProvider.GetService(Type serviceType) { - command.Execute(null, textArea); + return textArea.GetService(serviceType); } #endregion @@ -309,7 +226,6 @@ void Execute(RoutedUICommand command) DependencyProperty.Register("SyntaxHighlighting", typeof(IHighlightingDefinition), typeof(TextEditor), new FrameworkPropertyMetadata(OnSyntaxHighlightingChanged)); - /// /// Gets/sets the syntax highlighting definition used to colorize the text. /// @@ -342,7 +258,6 @@ void OnSyntaxHighlightingChanged(IHighlightingDefinition newValue) /// Creates the highlighting colorizer for the specified highlighting definition. /// Allows derived classes to provide custom colorizer implementations for special highlighting definitions. /// - /// protected virtual IVisualLineTransformer CreateColorizer(IHighlightingDefinition highlightingDefinition) { if (highlightingDefinition == null) @@ -398,11 +313,7 @@ static void OnIsReadOnlyChanged(DependencyObject d, DependencyPropertyChangedEve editor.TextArea.ReadOnlySectionProvider = ReadOnlySectionDocument.Instance; else editor.TextArea.ReadOnlySectionProvider = NoReadOnlySections.Instance; - - TextEditorAutomationPeer peer = TextEditorAutomationPeer.FromElement(editor) as TextEditorAutomationPeer; - if (peer != null) { - peer.RaiseIsReadOnlyChanged((bool)e.OldValue, (bool)e.NewValue); - } + editor.OnIsReadOnlyChangedCore(editor, (bool)e.OldValue, (bool)e.NewValue); } } #endregion @@ -473,36 +384,13 @@ public bool ShowLineNumbers { static void OnShowLineNumbersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { TextEditor editor = (TextEditor)d; - var leftMargins = editor.TextArea.LeftMargins; - if ((bool)e.NewValue) { - LineNumberMargin lineNumbers = new LineNumberMargin(); - Line line = (Line)DottedLineMargin.Create(); - leftMargins.Insert(0, lineNumbers); - leftMargins.Insert(1, line); - var lineNumbersForeground = new Binding("LineNumbersForeground") { Source = editor }; - line.SetBinding(Line.StrokeProperty, lineNumbersForeground); - lineNumbers.SetBinding(Control.ForegroundProperty, lineNumbersForeground); - } else { - for (int i = 0; i < leftMargins.Count; i++) { - if (leftMargins[i] is LineNumberMargin) { - leftMargins.RemoveAt(i); - if (i < leftMargins.Count && DottedLineMargin.IsDottedLineMargin(leftMargins[i])) { - leftMargins.RemoveAt(i); - } - break; - } - } - } + editor.OnShowLineNumbersChangedCore(editor, (bool)e.OldValue, (bool)e.NewValue); } #endregion #region LineNumbersForeground - /// - /// LineNumbersForeground dependency property. - /// - public static readonly DependencyProperty LineNumbersForegroundProperty = - DependencyProperty.Register("LineNumbersForeground", typeof(Brush), typeof(TextEditor), - new FrameworkPropertyMetadata(Brushes.Gray, OnLineNumbersForegroundChanged)); + // LineNumbersForegroundProperty is declared in TextEditor.wpf.cs (default=Brushes.Gray, WPF-specific) + // or TextEditor.uno.cs for the Uno platform. /// /// Gets/sets the Brush used for displaying the foreground color of line numbers. @@ -514,12 +402,7 @@ public Brush LineNumbersForeground { static void OnLineNumbersForegroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - TextEditor editor = (TextEditor)d; - var lineNumberMargin = editor.TextArea.LeftMargins.FirstOrDefault(margin => margin is LineNumberMargin) as LineNumberMargin; ; - - if (lineNumberMargin != null) { - lineNumberMargin.SetValue(Control.ForegroundProperty, e.NewValue); - } + ((TextEditor)d).OnLineNumbersForegroundChangedCore((TextEditor)d, e.NewValue); } #endregion @@ -541,22 +424,6 @@ public void BeginChange() GetDocument().BeginUpdate(); } - /// - /// Copies the current selection to the clipboard. - /// - public void Copy() - { - Execute(ApplicationCommands.Copy); - } - - /// - /// Removes the current selection and copies it to the clipboard. - /// - public void Cut() - { - Execute(ApplicationCommands.Cut); - } - /// /// Begins a group of document changes and returns an object that ends the group of document /// changes when it is disposed. @@ -566,14 +433,6 @@ public IDisposable DeclareChangeBlock() return GetDocument().RunUpdate(); } - /// - /// Removes the current selection without copying it to the clipboard. - /// - public void Delete() - { - Execute(ApplicationCommands.Delete); - } - /// /// Ends the current group of document changes. /// @@ -583,225 +442,11 @@ public void EndChange() } /// - /// Scrolls one line down. - /// - public void LineDown() - { - if (scrollViewer != null) - scrollViewer.LineDown(); - } - - /// - /// Scrolls to the left. - /// - public void LineLeft() - { - if (scrollViewer != null) - scrollViewer.LineLeft(); - } - - /// - /// Scrolls to the right. - /// - public void LineRight() - { - if (scrollViewer != null) - scrollViewer.LineRight(); - } - - /// - /// Scrolls one line up. - /// - public void LineUp() - { - if (scrollViewer != null) - scrollViewer.LineUp(); - } - - /// - /// Scrolls one page down. - /// - public void PageDown() - { - if (scrollViewer != null) - scrollViewer.PageDown(); - } - - /// - /// Scrolls one page up. - /// - public void PageUp() - { - if (scrollViewer != null) - scrollViewer.PageUp(); - } - - /// - /// Scrolls one page left. - /// - public void PageLeft() - { - if (scrollViewer != null) - scrollViewer.PageLeft(); - } - - /// - /// Scrolls one page right. - /// - public void PageRight() - { - if (scrollViewer != null) - scrollViewer.PageRight(); - } - - /// - /// Pastes the clipboard content. - /// - public void Paste() - { - Execute(ApplicationCommands.Paste); - } - - /// - /// Redoes the most recent undone command. - /// - /// True is the redo operation was successful, false is the redo stack is empty. - public bool Redo() - { - if (CanExecute(ApplicationCommands.Redo)) { - Execute(ApplicationCommands.Redo); - return true; - } - return false; - } - - /// - /// Scrolls to the end of the document. - /// - public void ScrollToEnd() - { - ApplyTemplate(); // ensure scrollViewer is created - if (scrollViewer != null) - scrollViewer.ScrollToEnd(); - } - - /// - /// Scrolls to the start of the document. - /// - public void ScrollToHome() - { - ApplyTemplate(); // ensure scrollViewer is created - if (scrollViewer != null) - scrollViewer.ScrollToHome(); - } - - /// - /// Scrolls to the specified position in the document. - /// - public void ScrollToHorizontalOffset(double offset) - { - ApplyTemplate(); // ensure scrollViewer is created - if (scrollViewer != null) - scrollViewer.ScrollToHorizontalOffset(offset); - } - - /// - /// Scrolls to the specified position in the document. - /// - public void ScrollToVerticalOffset(double offset) - { - ApplyTemplate(); // ensure scrollViewer is created - if (scrollViewer != null) - scrollViewer.ScrollToVerticalOffset(offset); - } - - /// - /// Selects the entire text. - /// - public void SelectAll() - { - Execute(ApplicationCommands.SelectAll); - } - - /// - /// Undoes the most recent command. + /// Clears the text. /// - /// True is the undo operation was successful, false is the undo stack is empty. - public bool Undo() + public void Clear() { - if (CanExecute(ApplicationCommands.Undo)) { - Execute(ApplicationCommands.Undo); - return true; - } - return false; - } - - /// - /// Gets if the most recent undone command can be redone. - /// - public bool CanRedo { - get { return CanExecute(ApplicationCommands.Redo); } - } - - /// - /// Gets if the most recent command can be undone. - /// - public bool CanUndo { - get { return CanExecute(ApplicationCommands.Undo); } - } - - /// - /// Gets the vertical size of the document. - /// - public double ExtentHeight { - get { - return scrollViewer != null ? scrollViewer.ExtentHeight : 0; - } - } - - /// - /// Gets the horizontal size of the current document region. - /// - public double ExtentWidth { - get { - return scrollViewer != null ? scrollViewer.ExtentWidth : 0; - } - } - - /// - /// Gets the horizontal size of the viewport. - /// - public double ViewportHeight { - get { - return scrollViewer != null ? scrollViewer.ViewportHeight : 0; - } - } - - /// - /// Gets the horizontal size of the viewport. - /// - public double ViewportWidth { - get { - return scrollViewer != null ? scrollViewer.ViewportWidth : 0; - } - } - - /// - /// Gets the vertical scroll position. - /// - public double VerticalOffset { - get { - return scrollViewer != null ? scrollViewer.VerticalOffset : 0; - } - } - - /// - /// Gets the horizontal scroll position. - /// - public double HorizontalOffset { - get { - return scrollViewer != null ? scrollViewer.HorizontalOffset : 0; - } + this.Text = string.Empty; } #endregion @@ -837,12 +482,8 @@ public string SelectedText { /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public int CaretOffset { - get { - return textArea.Caret.Offset; - } - set { - textArea.Caret.Offset = value; - } + get { return textArea.Caret.Offset; } + set { textArea.Caret.Offset = value; } } /// @@ -856,9 +497,7 @@ public int SelectionStart { else return textArea.Selection.SurroundingSegment.Offset; } - set { - Select(value, SelectionLength); - } + set { Select(value, SelectionLength); } } /// @@ -872,9 +511,7 @@ public int SelectionLength { else return 0; } - set { - Select(SelectionStart, value); - } + set { Select(SelectionStart, value); } } /// @@ -904,14 +541,6 @@ public int LineCount { return 1; } } - - /// - /// Clears the text. - /// - public void Clear() - { - this.Text = string.Empty; - } #endregion #region Loading from stream @@ -993,185 +622,5 @@ public void Save(string fileName) } } #endregion - - #region MouseHover events - /// - /// The PreviewMouseHover event. - /// - public static readonly RoutedEvent PreviewMouseHoverEvent = - TextView.PreviewMouseHoverEvent.AddOwner(typeof(TextEditor)); - - /// - /// The MouseHover event. - /// - public static readonly RoutedEvent MouseHoverEvent = - TextView.MouseHoverEvent.AddOwner(typeof(TextEditor)); - - - /// - /// The PreviewMouseHoverStopped event. - /// - public static readonly RoutedEvent PreviewMouseHoverStoppedEvent = - TextView.PreviewMouseHoverStoppedEvent.AddOwner(typeof(TextEditor)); - - /// - /// The MouseHoverStopped event. - /// - public static readonly RoutedEvent MouseHoverStoppedEvent = - TextView.MouseHoverStoppedEvent.AddOwner(typeof(TextEditor)); - - - /// - /// Occurs when the mouse has hovered over a fixed location for some time. - /// - public event MouseEventHandler PreviewMouseHover { - add { AddHandler(PreviewMouseHoverEvent, value); } - remove { RemoveHandler(PreviewMouseHoverEvent, value); } - } - - /// - /// Occurs when the mouse has hovered over a fixed location for some time. - /// - public event MouseEventHandler MouseHover { - add { AddHandler(MouseHoverEvent, value); } - remove { RemoveHandler(MouseHoverEvent, value); } - } - - /// - /// Occurs when the mouse had previously hovered but now started moving again. - /// - public event MouseEventHandler PreviewMouseHoverStopped { - add { AddHandler(PreviewMouseHoverStoppedEvent, value); } - remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); } - } - - /// - /// Occurs when the mouse had previously hovered but now started moving again. - /// - public event MouseEventHandler MouseHoverStopped { - add { AddHandler(MouseHoverStoppedEvent, value); } - remove { RemoveHandler(MouseHoverStoppedEvent, value); } - } - #endregion - - #region ScrollBarVisibility - /// - /// Dependency property for - /// - public static readonly DependencyProperty HorizontalScrollBarVisibilityProperty = ScrollViewer.HorizontalScrollBarVisibilityProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(ScrollBarVisibility.Visible)); - - /// - /// Gets/Sets the horizontal scroll bar visibility. - /// - public ScrollBarVisibility HorizontalScrollBarVisibility { - get { return (ScrollBarVisibility)GetValue(HorizontalScrollBarVisibilityProperty); } - set { SetValue(HorizontalScrollBarVisibilityProperty, value); } - } - - /// - /// Dependency property for - /// - public static readonly DependencyProperty VerticalScrollBarVisibilityProperty = ScrollViewer.VerticalScrollBarVisibilityProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(ScrollBarVisibility.Visible)); - - /// - /// Gets/Sets the vertical scroll bar visibility. - /// - public ScrollBarVisibility VerticalScrollBarVisibility { - get { return (ScrollBarVisibility)GetValue(VerticalScrollBarVisibilityProperty); } - set { SetValue(VerticalScrollBarVisibilityProperty, value); } - } - #endregion - - object IServiceProvider.GetService(Type serviceType) - { - return textArea.GetService(serviceType); - } - - /// - /// Gets the text view position from a point inside the editor. - /// - /// The position, relative to top left - /// corner of TextEditor control - /// The text view position, or null if the point is outside the document. - public TextViewPosition? GetPositionFromPoint(Point point) - { - if (this.Document == null) - return null; - TextView textView = this.TextArea.TextView; - return textView.GetPosition(TranslatePoint(point, textView) + textView.ScrollOffset); - } - - /// - /// Scrolls to the specified line. - /// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior). - /// - public void ScrollToLine(int line) - { - ScrollTo(line, -1); - } - - /// - /// Scrolls to the specified line/column. - /// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior). - /// - public void ScrollTo(int line, int column) - { - const double MinimumScrollFraction = 0.3; - ScrollTo(line, column, VisualYPosition.LineMiddle, null != scrollViewer ? scrollViewer.ViewportHeight / 2 : 0.0, MinimumScrollFraction); - } - - /// - /// Scrolls to the specified line/column. - /// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior). - /// - /// Line to scroll to. - /// Column to scroll to (important if wrapping is 'on', and for the horizontal scroll position). - /// The mode how to reference the Y position of the line. - /// Offset from the top of the viewport to where the referenced line/column should be positioned. - /// The minimum vertical and/or horizontal scroll offset, expressed as fraction of the height or width of the viewport window, respectively. - public void ScrollTo(int line, int column, VisualYPosition yPositionMode, double referencedVerticalViewPortOffset, double minimumScrollFraction) - { - TextView textView = textArea.TextView; - TextDocument document = textView.Document; - if (scrollViewer != null && document != null) { - if (line < 1) - line = 1; - if (line > document.LineCount) - line = document.LineCount; - - IScrollInfo scrollInfo = textView; - if (!scrollInfo.CanHorizontallyScroll) { - // Word wrap is enabled. Ensure that we have up-to-date info about line height so that we scroll - // to the correct position. - // This avoids that the user has to repeat the ScrollTo() call several times when there are very long lines. - VisualLine vl = textView.GetOrConstructVisualLine(document.GetLineByNumber(line)); - double remainingHeight = referencedVerticalViewPortOffset; - - while (remainingHeight > 0) { - DocumentLine prevLine = vl.FirstDocumentLine.PreviousLine; - if (prevLine == null) - break; - vl = textView.GetOrConstructVisualLine(prevLine); - remainingHeight -= vl.Height; - } - } - - Point p = textArea.TextView.GetVisualPosition(new TextViewPosition(line, Math.Max(1, column)), yPositionMode); - double verticalPos = p.Y - referencedVerticalViewPortOffset; - if (Math.Abs(verticalPos - scrollViewer.VerticalOffset) > minimumScrollFraction * scrollViewer.ViewportHeight) { - scrollViewer.ScrollToVerticalOffset(Math.Max(0, verticalPos)); - } - if (column > 0) { - if (p.X > scrollViewer.ViewportWidth - Caret.MinimumDistanceToViewBorder * 2) { - double horizontalPos = Math.Max(0, p.X - scrollViewer.ViewportWidth / 2); - if (Math.Abs(horizontalPos - scrollViewer.HorizontalOffset) > minimumScrollFraction * scrollViewer.ViewportWidth) { - scrollViewer.ScrollToHorizontalOffset(horizontalPos); - } - } else { - scrollViewer.ScrollToHorizontalOffset(0); - } - } - } - } } } diff --git a/ICSharpCode.AvalonEdit/TextEditor.wpf.cs b/ICSharpCode.AvalonEdit/TextEditor.wpf.cs new file mode 100644 index 00000000..8980a913 --- /dev/null +++ b/ICSharpCode.AvalonEdit/TextEditor.wpf.cs @@ -0,0 +1,620 @@ +// 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 System.Windows; +using System.Windows.Automation.Peers; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Markup; +using System.Windows.Media; +using System.Windows.Shapes; + +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Editing; +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit +{ + [Localizability(LocalizationCategory.Text)] + [ContentProperty("Text")] + public partial class TextEditor : Control, IWeakEventListener + { + static partial void InitializeWpfDefaults() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(TextEditor), + new FrameworkPropertyMetadata(typeof(TextEditor))); + FocusableProperty.OverrideMetadata(typeof(TextEditor), + new FrameworkPropertyMetadata(Boxes.True)); + } + + /// + protected override AutomationPeer OnCreateAutomationPeer() + { + return new TextEditorAutomationPeer(this); + } + + /// Forward focus to TextArea. + /// + protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + base.OnGotKeyboardFocus(e); + if (e.NewFocus == this) { + Keyboard.Focus(textArea); + e.Handled = true; + } + } + + #region Document property + /// + /// Document property. + /// + public static readonly DependencyProperty DocumentProperty + = TextView.DocumentProperty.AddOwner( + typeof(TextEditor), new FrameworkPropertyMetadata(OnDocumentChanged)); + #endregion + + #region Options property + /// + /// Options property. + /// + public static readonly DependencyProperty OptionsProperty + = TextView.OptionsProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(OnOptionsChanged)); + + partial void OnDocumentChangedCore(TextDocument oldValue, TextDocument newValue) + { + if (oldValue != null) { + TextDocumentWeakEventManager.TextChanged.RemoveListener(oldValue, this); + PropertyChangedEventManager.RemoveListener(oldValue.UndoStack, this, "IsOriginalFile"); + } + if (newValue != null) { + TextDocumentWeakEventManager.TextChanged.AddListener(newValue, this); + PropertyChangedEventManager.AddListener(newValue.UndoStack, this, "IsOriginalFile"); + } + } + + partial void OnOptionsChangedCore(TextEditorOptions oldValue, TextEditorOptions newValue) + { + if (oldValue != null) { + PropertyChangedWeakEventManager.RemoveListener(oldValue, this); + } + if (newValue != null) { + PropertyChangedWeakEventManager.AddListener(newValue, this); + } + } + + /// + protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + if (managerType == typeof(PropertyChangedWeakEventManager)) { + OnOptionChanged((System.ComponentModel.PropertyChangedEventArgs)e); + return true; + } else if (managerType == typeof(TextDocumentWeakEventManager.TextChanged)) { + OnTextChanged(e); + return true; + } else if (managerType == typeof(PropertyChangedEventManager)) { + return HandleIsOriginalChanged((System.ComponentModel.PropertyChangedEventArgs)e); + } + return false; + } + + bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + return ReceiveWeakEvent(managerType, sender, e); + } + #endregion + + #region TextArea / ScrollViewer + ScrollViewer scrollViewer; + + /// + /// Is called after the template was applied. + /// + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + scrollViewer = (ScrollViewer)Template.FindName("PART_ScrollViewer", this); + } + + /// + /// Gets the scroll viewer used by the text editor. + /// This property can return null if the template has not been applied / does not contain a scroll viewer. + /// + internal ScrollViewer ScrollViewer { + get { return scrollViewer; } + } + + bool CanExecute(RoutedUICommand command) + { + return command.CanExecute(null, textArea); + } + + void Execute(RoutedUICommand command) + { + command.Execute(null, textArea); + } + #endregion + + #region IsReadOnly automation peer + partial void OnIsReadOnlyChangedCore(TextEditor editor, bool oldValue, bool newValue) + { + TextEditorAutomationPeer peer = TextEditorAutomationPeer.FromElement(editor) as TextEditorAutomationPeer; + if (peer != null) { + peer.RaiseIsReadOnlyChanged(oldValue, newValue); + } + } + #endregion + + #region ShowLineNumbers WPF implementation + partial void OnShowLineNumbersChangedCore(TextEditor editor, bool oldValue, bool newValue) + { + var leftMargins = editor.TextArea.LeftMargins; + if (newValue) { + LineNumberMargin lineNumbers = new LineNumberMargin(); + Line line = (Line)DottedLineMargin.Create(); + leftMargins.Insert(0, lineNumbers); + leftMargins.Insert(1, line); + var lineNumbersForeground = new Binding("LineNumbersForeground") { Source = editor }; + line.SetBinding(Line.StrokeProperty, lineNumbersForeground); + lineNumbers.SetBinding(Control.ForegroundProperty, lineNumbersForeground); + } else { + for (int i = 0; i < leftMargins.Count; i++) { + if (leftMargins[i] is LineNumberMargin) { + leftMargins.RemoveAt(i); + if (i < leftMargins.Count && DottedLineMargin.IsDottedLineMargin(leftMargins[i])) { + leftMargins.RemoveAt(i); + } + break; + } + } + } + } + #endregion + + #region LineNumbersForeground WPF implementation + /// + /// LineNumbersForeground dependency property. + /// + public static readonly DependencyProperty LineNumbersForegroundProperty = + DependencyProperty.Register("LineNumbersForeground", typeof(Brush), typeof(TextEditor), + new FrameworkPropertyMetadata(Brushes.Gray, OnLineNumbersForegroundChanged)); + + partial void OnLineNumbersForegroundChangedCore(TextEditor editor, object newValue) + { + var lineNumberMargin = editor.TextArea.LeftMargins.FirstOrDefault(margin => margin is LineNumberMargin) as LineNumberMargin; + if (lineNumberMargin != null) { + lineNumberMargin.SetValue(Control.ForegroundProperty, newValue); + } + } + #endregion + + #region TextBoxBase-like methods (WPF command-based) + /// + /// Copies the current selection to the clipboard. + /// + public void Copy() + { + Execute(ApplicationCommands.Copy); + } + + /// + /// Removes the current selection and copies it to the clipboard. + /// + public void Cut() + { + Execute(ApplicationCommands.Cut); + } + + /// + /// Removes the current selection without copying it to the clipboard. + /// + public void Delete() + { + Execute(ApplicationCommands.Delete); + } + + /// + /// Scrolls one line down. + /// + public void LineDown() + { + if (scrollViewer != null) + scrollViewer.LineDown(); + } + + /// + /// Scrolls to the left. + /// + public void LineLeft() + { + if (scrollViewer != null) + scrollViewer.LineLeft(); + } + + /// + /// Scrolls to the right. + /// + public void LineRight() + { + if (scrollViewer != null) + scrollViewer.LineRight(); + } + + /// + /// Scrolls one line up. + /// + public void LineUp() + { + if (scrollViewer != null) + scrollViewer.LineUp(); + } + + /// + /// Scrolls one page down. + /// + public void PageDown() + { + if (scrollViewer != null) + scrollViewer.PageDown(); + } + + /// + /// Scrolls one page up. + /// + public void PageUp() + { + if (scrollViewer != null) + scrollViewer.PageUp(); + } + + /// + /// Scrolls one page left. + /// + public void PageLeft() + { + if (scrollViewer != null) + scrollViewer.PageLeft(); + } + + /// + /// Scrolls one page right. + /// + public void PageRight() + { + if (scrollViewer != null) + scrollViewer.PageRight(); + } + + /// + /// Pastes the clipboard content. + /// + public void Paste() + { + Execute(ApplicationCommands.Paste); + } + + /// + /// Redoes the most recent undone command. + /// + /// True is the redo operation was successful, false is the redo stack is empty. + public bool Redo() + { + if (CanExecute(ApplicationCommands.Redo)) { + Execute(ApplicationCommands.Redo); + return true; + } + return false; + } + + /// + /// Scrolls to the end of the document. + /// + public void ScrollToEnd() + { + ApplyTemplate(); // ensure scrollViewer is created + if (scrollViewer != null) + scrollViewer.ScrollToEnd(); + } + + /// + /// Scrolls to the start of the document. + /// + public void ScrollToHome() + { + ApplyTemplate(); // ensure scrollViewer is created + if (scrollViewer != null) + scrollViewer.ScrollToHome(); + } + + /// + /// Scrolls to the specified position in the document. + /// + public void ScrollToHorizontalOffset(double offset) + { + ApplyTemplate(); // ensure scrollViewer is created + if (scrollViewer != null) + scrollViewer.ScrollToHorizontalOffset(offset); + } + + /// + /// Scrolls to the specified position in the document. + /// + public void ScrollToVerticalOffset(double offset) + { + ApplyTemplate(); // ensure scrollViewer is created + if (scrollViewer != null) + scrollViewer.ScrollToVerticalOffset(offset); + } + + /// + /// Selects the entire text. + /// + public void SelectAll() + { + Execute(ApplicationCommands.SelectAll); + } + + /// + /// Undoes the most recent command. + /// + /// True is the undo operation was successful, false is the undo stack is empty. + public bool Undo() + { + if (CanExecute(ApplicationCommands.Undo)) { + Execute(ApplicationCommands.Undo); + return true; + } + return false; + } + + /// + /// Gets if the most recent undone command can be redone. + /// + public bool CanRedo { + get { return CanExecute(ApplicationCommands.Redo); } + } + + /// + /// Gets if the most recent command can be undone. + /// + public bool CanUndo { + get { return CanExecute(ApplicationCommands.Undo); } + } + + /// + /// Gets the vertical size of the document. + /// + public double ExtentHeight { + get { return scrollViewer != null ? scrollViewer.ExtentHeight : 0; } + } + + /// + /// Gets the horizontal size of the current document region. + /// + public double ExtentWidth { + get { return scrollViewer != null ? scrollViewer.ExtentWidth : 0; } + } + + /// + /// Gets the horizontal size of the viewport. + /// + public double ViewportHeight { + get { return scrollViewer != null ? scrollViewer.ViewportHeight : 0; } + } + + /// + /// Gets the horizontal size of the viewport. + /// + public double ViewportWidth { + get { return scrollViewer != null ? scrollViewer.ViewportWidth : 0; } + } + + /// + /// Gets the vertical scroll position. + /// + public double VerticalOffset { + get { return scrollViewer != null ? scrollViewer.VerticalOffset : 0; } + } + + /// + /// Gets the horizontal scroll position. + /// + public double HorizontalOffset { + get { return scrollViewer != null ? scrollViewer.HorizontalOffset : 0; } + } + #endregion + + #region MouseHover events + /// + /// The PreviewMouseHover event. + /// + public static readonly RoutedEvent PreviewMouseHoverEvent = + TextView.PreviewMouseHoverEvent.AddOwner(typeof(TextEditor)); + + /// + /// The MouseHover event. + /// + public static readonly RoutedEvent MouseHoverEvent = + TextView.MouseHoverEvent.AddOwner(typeof(TextEditor)); + + /// + /// The PreviewMouseHoverStopped event. + /// + public static readonly RoutedEvent PreviewMouseHoverStoppedEvent = + TextView.PreviewMouseHoverStoppedEvent.AddOwner(typeof(TextEditor)); + + /// + /// The MouseHoverStopped event. + /// + public static readonly RoutedEvent MouseHoverStoppedEvent = + TextView.MouseHoverStoppedEvent.AddOwner(typeof(TextEditor)); + + /// + /// Occurs when the mouse has hovered over a fixed location for some time. + /// + public event MouseEventHandler PreviewMouseHover { + add { AddHandler(PreviewMouseHoverEvent, value); } + remove { RemoveHandler(PreviewMouseHoverEvent, value); } + } + + /// + /// Occurs when the mouse has hovered over a fixed location for some time. + /// + public event MouseEventHandler MouseHover { + add { AddHandler(MouseHoverEvent, value); } + remove { RemoveHandler(MouseHoverEvent, value); } + } + + /// + /// Occurs when the mouse had previously hovered but now started moving again. + /// + public event MouseEventHandler PreviewMouseHoverStopped { + add { AddHandler(PreviewMouseHoverStoppedEvent, value); } + remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); } + } + + /// + /// Occurs when the mouse had previously hovered but now started moving again. + /// + public event MouseEventHandler MouseHoverStopped { + add { AddHandler(MouseHoverStoppedEvent, value); } + remove { RemoveHandler(MouseHoverStoppedEvent, value); } + } + #endregion + + #region ScrollBarVisibility + /// + /// Dependency property for + /// + public static readonly DependencyProperty HorizontalScrollBarVisibilityProperty = ScrollViewer.HorizontalScrollBarVisibilityProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(ScrollBarVisibility.Visible)); + + /// + /// Gets/Sets the horizontal scroll bar visibility. + /// + public ScrollBarVisibility HorizontalScrollBarVisibility { + get { return (ScrollBarVisibility)GetValue(HorizontalScrollBarVisibilityProperty); } + set { SetValue(HorizontalScrollBarVisibilityProperty, value); } + } + + /// + /// Dependency property for + /// + public static readonly DependencyProperty VerticalScrollBarVisibilityProperty = ScrollViewer.VerticalScrollBarVisibilityProperty.AddOwner(typeof(TextEditor), new FrameworkPropertyMetadata(ScrollBarVisibility.Visible)); + + /// + /// Gets/Sets the vertical scroll bar visibility. + /// + public ScrollBarVisibility VerticalScrollBarVisibility { + get { return (ScrollBarVisibility)GetValue(VerticalScrollBarVisibilityProperty); } + set { SetValue(VerticalScrollBarVisibilityProperty, value); } + } + #endregion + + /// + /// Gets the text view position from a point inside the editor. + /// + /// The position, relative to top left + /// corner of TextEditor control + /// The text view position, or null if the point is outside the document. + public TextViewPosition? GetPositionFromPoint(Point point) + { + if (this.Document == null) + return null; + TextView textView = this.TextArea.TextView; + return textView.GetPosition(TranslatePoint(point, textView) + textView.ScrollOffset); + } + + /// + /// Scrolls to the specified line. + /// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior). + /// + public void ScrollToLine(int line) + { + ScrollTo(line, -1); + } + + /// + /// Scrolls to the specified line/column. + /// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior). + /// + public void ScrollTo(int line, int column) + { + const double MinimumScrollFraction = 0.3; + ScrollTo(line, column, VisualYPosition.LineMiddle, null != scrollViewer ? scrollViewer.ViewportHeight / 2 : 0.0, MinimumScrollFraction); + } + + /// + /// Scrolls to the specified line/column. + /// This method requires that the TextEditor was already assigned a size (WPF layout must have run prior). + /// + /// Line to scroll to. + /// Column to scroll to (important if wrapping is 'on', and for the horizontal scroll position). + /// The mode how to reference the Y position of the line. + /// Offset from the top of the viewport to where the referenced line/column should be positioned. + /// The minimum vertical and/or horizontal scroll offset, expressed as fraction of the height or width of the viewport window, respectively. + public void ScrollTo(int line, int column, VisualYPosition yPositionMode, double referencedVerticalViewPortOffset, double minimumScrollFraction) + { + TextView textView = textArea.TextView; + TextDocument document = textView.Document; + if (scrollViewer != null && document != null) { + if (line < 1) + line = 1; + if (line > document.LineCount) + line = document.LineCount; + + IScrollInfo scrollInfo = textView; + if (!scrollInfo.CanHorizontallyScroll) { + // Word wrap is enabled. Ensure that we have up-to-date info about line height so that we scroll + // to the correct position. + // This avoids that the user has to repeat the ScrollTo() call several times when there are very long lines. + VisualLine vl = textView.GetOrConstructVisualLine(document.GetLineByNumber(line)); + double remainingHeight = referencedVerticalViewPortOffset; + + while (remainingHeight > 0) { + DocumentLine prevLine = vl.FirstDocumentLine.PreviousLine; + if (prevLine == null) + break; + vl = textView.GetOrConstructVisualLine(prevLine); + remainingHeight -= vl.Height; + } + } + + Point p = textArea.TextView.GetVisualPosition(new TextViewPosition(line, Math.Max(1, column)), yPositionMode); + double verticalPos = p.Y - referencedVerticalViewPortOffset; + if (Math.Abs(verticalPos - scrollViewer.VerticalOffset) > minimumScrollFraction * scrollViewer.ViewportHeight) { + scrollViewer.ScrollToVerticalOffset(Math.Max(0, verticalPos)); + } + if (column > 0) { + if (p.X > scrollViewer.ViewportWidth - Caret.MinimumDistanceToViewBorder * 2) { + double horizontalPos = Math.Max(0, p.X - scrollViewer.ViewportWidth / 2); + if (Math.Abs(horizontalPos - scrollViewer.HorizontalOffset) > minimumScrollFraction * scrollViewer.ViewportWidth) { + scrollViewer.ScrollToHorizontalOffset(horizontalPos); + } + } else { + scrollViewer.ScrollToHorizontalOffset(0); + } + } + } + } + } +}