|
|
|
// 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.Diagnostics;
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
using System.Linq;
|
|
|
|
using Avalonia;
|
|
|
|
using Avalonia.Controls;
|
|
|
|
using Avalonia.Controls.Primitives;
|
|
|
|
using Avalonia.Input;
|
|
|
|
using Avalonia.Interactivity;
|
|
|
|
using Avalonia.LogicalTree;
|
|
|
|
using Avalonia.Threading;
|
|
|
|
using Avalonia.VisualTree;
|
|
|
|
using AvaloniaEdit;
|
|
|
|
using AvaloniaEdit.Document;
|
|
|
|
using AvaloniaEdit.Editing;
|
|
|
|
using AvaloniaEdit.Rendering;
|
|
|
|
|
|
|
|
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Base class for completion windows. Handles positioning the window at the caret.
|
|
|
|
/// </summary>
|
|
|
|
[SuppressMessage("ReSharper", "MemberCanBeProtected.Global")]
|
|
|
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
|
|
|
public class CompletionWindowBase : Popup
|
|
|
|
{
|
|
|
|
protected override Type StyleKeyOverride => typeof(PopupRoot);
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the parent TextArea.
|
|
|
|
/// </summary>
|
|
|
|
public TextArea TextArea { get; }
|
|
|
|
|
|
|
|
private readonly Window? _parentWindow;
|
|
|
|
private TextDocument? _document;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets/Sets the start of the text range in which the completion window stays open.
|
|
|
|
/// This text portion is used to determine the text used to select an entry in the completion list by typing.
|
|
|
|
/// </summary>
|
|
|
|
public int StartOffset { get; set; }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets/Sets the end of the text range in which the completion window stays open.
|
|
|
|
/// This text portion is used to determine the text used to select an entry in the completion list by typing.
|
|
|
|
/// </summary>
|
|
|
|
public int EndOffset { get; set; }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets whether the window was opened above the current line.
|
|
|
|
/// </summary>
|
|
|
|
protected bool IsUp { get; private set; }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Creates a new CompletionWindowBase.
|
|
|
|
/// </summary>
|
|
|
|
public CompletionWindowBase(TextArea textArea)
|
|
|
|
{
|
|
|
|
TextArea = textArea ?? throw new ArgumentNullException(nameof(textArea));
|
|
|
|
_parentWindow = textArea.GetVisualRoot() as Window ??
|
|
|
|
throw new InvalidOperationException("CompletionWindow requires a visual root.");
|
|
|
|
|
|
|
|
AddHandler(PointerReleasedEvent, OnMouseUp, handledEventsToo: true);
|
|
|
|
|
|
|
|
StartOffset = EndOffset = TextArea.Caret.Offset;
|
|
|
|
|
|
|
|
PlacementTarget = TextArea.TextView;
|
|
|
|
Placement = PlacementMode.AnchorAndGravity;
|
|
|
|
PlacementAnchor = global::Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor.TopLeft;
|
|
|
|
PlacementGravity = global::Avalonia.Controls.Primitives.PopupPositioning.PopupGravity.BottomRight;
|
|
|
|
|
|
|
|
//Deactivated += OnDeactivated; //Not needed?
|
|
|
|
|
|
|
|
Closed += (_, _) => DetachEvents();
|
|
|
|
|
|
|
|
AttachEvents();
|
|
|
|
|
|
|
|
Initialize();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected virtual void OnClosed()
|
|
|
|
{
|
|
|
|
DetachEvents();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void Initialize()
|
|
|
|
{
|
|
|
|
if (_document != null && StartOffset != TextArea.Caret.Offset)
|
|
|
|
{
|
|
|
|
SetPosition(new TextViewPosition(_document.GetLocation(StartOffset)));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
SetPosition(TextArea.Caret.Position);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Show()
|
|
|
|
{
|
|
|
|
UpdatePosition();
|
|
|
|
|
|
|
|
Open();
|
|
|
|
Height = double.NaN;
|
|
|
|
MinHeight = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Hide()
|
|
|
|
{
|
|
|
|
Close();
|
|
|
|
OnClosed();
|
|
|
|
}
|
|
|
|
|
|
|
|
#region Event Handlers
|
|
|
|
|
|
|
|
private void AttachEvents()
|
|
|
|
{
|
|
|
|
((ISetLogicalParent)this).SetParent(TextArea.GetVisualRoot() as ILogical);
|
|
|
|
|
|
|
|
_document = TextArea.Document;
|
|
|
|
if (_document != null)
|
|
|
|
{
|
|
|
|
_document.Changing += TextArea_Document_Changing;
|
|
|
|
}
|
|
|
|
|
|
|
|
// LostKeyboardFocus seems to be more reliable than PreviewLostKeyboardFocus - see SD-1729
|
|
|
|
TextArea.LostFocus += TextAreaLostFocus;
|
|
|
|
TextArea.TextView.ScrollOffsetChanged += TextViewScrollOffsetChanged;
|
|
|
|
TextArea.DocumentChanged += TextAreaDocumentChanged;
|
|
|
|
if (_parentWindow != null)
|
|
|
|
{
|
|
|
|
_parentWindow.PositionChanged += ParentWindow_LocationChanged;
|
|
|
|
_parentWindow.Deactivated += ParentWindow_Deactivated;
|
|
|
|
}
|
|
|
|
|
|
|
|
// close previous completion windows of same type
|
|
|
|
foreach (var x in TextArea.StackedInputHandlers.OfType<InputHandler>())
|
|
|
|
{
|
|
|
|
if (x.Window.GetType() == GetType())
|
|
|
|
TextArea.PopStackedInputHandler(x);
|
|
|
|
}
|
|
|
|
|
|
|
|
_myInputHandler = new InputHandler(this);
|
|
|
|
TextArea.PushStackedInputHandler(_myInputHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Detaches events from the text area.
|
|
|
|
/// </summary>
|
|
|
|
protected virtual void DetachEvents()
|
|
|
|
{
|
|
|
|
((ISetLogicalParent)this).SetParent(null);
|
|
|
|
|
|
|
|
if (_document != null)
|
|
|
|
{
|
|
|
|
_document.Changing -= TextArea_Document_Changing;
|
|
|
|
}
|
|
|
|
TextArea.LostFocus -= TextAreaLostFocus;
|
|
|
|
TextArea.TextView.ScrollOffsetChanged -= TextViewScrollOffsetChanged;
|
|
|
|
TextArea.DocumentChanged -= TextAreaDocumentChanged;
|
|
|
|
if (_parentWindow != null)
|
|
|
|
{
|
|
|
|
_parentWindow.PositionChanged -= ParentWindow_LocationChanged;
|
|
|
|
_parentWindow.Deactivated -= ParentWindow_Deactivated;
|
|
|
|
}
|
|
|
|
TextArea.PopStackedInputHandler(_myInputHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
#region InputHandler
|
|
|
|
|
|
|
|
private InputHandler? _myInputHandler;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A dummy input handler (that justs invokes the default input handler).
|
|
|
|
/// This is used to ensure the completion window closes when any other input handler
|
|
|
|
/// becomes active.
|
|
|
|
/// </summary>
|
|
|
|
private sealed class InputHandler : TextAreaStackedInputHandler
|
|
|
|
{
|
|
|
|
internal readonly CompletionWindowBase Window;
|
|
|
|
|
|
|
|
public InputHandler(CompletionWindowBase window)
|
|
|
|
: base(window.TextArea)
|
|
|
|
{
|
|
|
|
Debug.Assert(window != null);
|
|
|
|
Window = window;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Detach()
|
|
|
|
{
|
|
|
|
base.Detach();
|
|
|
|
Window.Hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void OnPreviewKeyDown(KeyEventArgs e)
|
|
|
|
{
|
|
|
|
// prevents crash when typing deadchar while CC window is open
|
|
|
|
if (e.Key == Key.DeadCharProcessed)
|
|
|
|
return;
|
|
|
|
e.Handled = RaiseEventPair(Window, null, KeyDownEvent,
|
|
|
|
new KeyEventArgs { Key = e.Key });
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void OnPreviewKeyUp(KeyEventArgs e)
|
|
|
|
{
|
|
|
|
if (e.Key == Key.DeadCharProcessed)
|
|
|
|
return;
|
|
|
|
e.Handled = RaiseEventPair(Window, null, KeyUpEvent,
|
|
|
|
new KeyEventArgs { Key = e.Key });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
private void TextViewScrollOffsetChanged(object? sender, EventArgs e)
|
|
|
|
{
|
|
|
|
ILogicalScrollable textView = TextArea;
|
|
|
|
var visibleRect = new Rect(textView.Offset.X, textView.Offset.Y, textView.Viewport.Width, textView.Viewport.Height);
|
|
|
|
//close completion window when the user scrolls so far that the anchor position is leaving the visible area
|
|
|
|
if (visibleRect.Contains(_visualLocation) || visibleRect.Contains(_visualLocationTop))
|
|
|
|
{
|
|
|
|
UpdatePosition();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void TextAreaDocumentChanged(object? sender, EventArgs e)
|
|
|
|
{
|
|
|
|
Hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void TextAreaLostFocus(object? sender, RoutedEventArgs e)
|
|
|
|
{
|
|
|
|
Dispatcher.UIThread.Post(CloseIfFocusLost, DispatcherPriority.Background);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void ParentWindow_Deactivated(object? sender, EventArgs e)
|
|
|
|
{
|
|
|
|
Hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void ParentWindow_LocationChanged(object? sender, EventArgs e)
|
|
|
|
{
|
|
|
|
UpdatePosition();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
private void OnDeactivated(object sender, EventArgs e)
|
|
|
|
{
|
|
|
|
Dispatcher.UIThread.Post(CloseIfFocusLost, DispatcherPriority.Background);
|
|
|
|
}*/
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Raises a tunnel/bubble event pair for a control.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="target">The control for which the event should be raised.</param>
|
|
|
|
/// <param name="previewEvent">The tunneling event.</param>
|
|
|
|
/// <param name="event">The bubbling event.</param>
|
|
|
|
/// <param name="args">The event args to use.</param>
|
|
|
|
/// <returns>The <see cref="RoutedEventArgs.Handled"/> value of the event args.</returns>
|
|
|
|
protected static bool RaiseEventPair(Control target, RoutedEvent? previewEvent, RoutedEvent @event, RoutedEventArgs args)
|
|
|
|
{
|
|
|
|
if (target == null)
|
|
|
|
throw new ArgumentNullException(nameof(target));
|
|
|
|
if (args == null)
|
|
|
|
throw new ArgumentNullException(nameof(args));
|
|
|
|
if (previewEvent != null)
|
|
|
|
{
|
|
|
|
args.RoutedEvent = previewEvent;
|
|
|
|
target.RaiseEvent(args);
|
|
|
|
}
|
|
|
|
args.RoutedEvent = @event ?? throw new ArgumentNullException(nameof(@event));
|
|
|
|
target.RaiseEvent(args);
|
|
|
|
return args.Handled;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Special handler: handledEventsToo
|
|
|
|
private void OnMouseUp(object? sender, PointerReleasedEventArgs e)
|
|
|
|
{
|
|
|
|
ActivateParentWindow();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Activates the parent window.
|
|
|
|
/// </summary>
|
|
|
|
protected virtual void ActivateParentWindow()
|
|
|
|
{
|
|
|
|
_parentWindow?.Activate();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void CloseIfFocusLost()
|
|
|
|
{
|
|
|
|
if (CloseOnFocusLost)
|
|
|
|
{
|
|
|
|
Debug.WriteLine("CloseIfFocusLost: this.IsFocues=" + IsFocused + " IsTextAreaFocused=" + IsTextAreaFocused);
|
|
|
|
if (!IsFocused && !IsTextAreaFocused)
|
|
|
|
{
|
|
|
|
Hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets whether the completion window should automatically close when the text editor looses focus.
|
|
|
|
/// </summary>
|
|
|
|
protected virtual bool CloseOnFocusLost => true;
|
|
|
|
|
|
|
|
private bool IsTextAreaFocused
|
|
|
|
{
|
|
|
|
get
|
|
|
|
{
|
|
|
|
if (_parentWindow != null && !_parentWindow.IsActive)
|
|
|
|
return false;
|
|
|
|
return TextArea.IsFocused;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
protected override void OnKeyDown(KeyEventArgs e)
|
|
|
|
{
|
|
|
|
base.OnKeyDown(e);
|
|
|
|
if (!e.Handled && e.Key == Key.Escape)
|
|
|
|
{
|
|
|
|
e.Handled = true;
|
|
|
|
Hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private Point _visualLocation;
|
|
|
|
private Point _visualLocationTop;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Positions the completion window at the specified position.
|
|
|
|
/// </summary>
|
|
|
|
protected void SetPosition(TextViewPosition position)
|
|
|
|
{
|
|
|
|
var textView = TextArea.TextView;
|
|
|
|
|
|
|
|
_visualLocation = textView.GetVisualPosition(position, VisualYPosition.LineBottom);
|
|
|
|
_visualLocationTop = textView.GetVisualPosition(position, VisualYPosition.LineTop);
|
|
|
|
|
|
|
|
UpdatePosition();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Updates the position of the CompletionWindow based on the parent TextView position and the screen working area.
|
|
|
|
/// It ensures that the CompletionWindow is completely visible on the screen.
|
|
|
|
/// </summary>
|
|
|
|
protected void UpdatePosition()
|
|
|
|
{
|
|
|
|
var textView = TextArea.TextView;
|
|
|
|
|
|
|
|
var position = _visualLocation - textView.ScrollOffset;
|
|
|
|
|
|
|
|
this.HorizontalOffset = position.X;
|
|
|
|
this.VerticalOffset = position.Y;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: check if needed
|
|
|
|
//protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
|
|
|
|
//{
|
|
|
|
// base.OnRenderSizeChanged(sizeInfo);
|
|
|
|
// if (sizeInfo.HeightChanged && IsUp)
|
|
|
|
// {
|
|
|
|
// this.Top += sizeInfo.PreviousSize.Height - sizeInfo.NewSize.Height;
|
|
|
|
// }
|
|
|
|
//}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets/sets whether the completion window should expect text insertion at the start offset,
|
|
|
|
/// which not go into the completion region, but before it.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>This property allows only a single insertion, it is reset to false
|
|
|
|
/// when that insertion has occurred.</remarks>
|
|
|
|
public bool ExpectInsertionBeforeStart { get; set; }
|
|
|
|
|
|
|
|
protected virtual void TextArea_Document_Changing(object? sender, DocumentChangeEventArgs e)
|
|
|
|
{
|
|
|
|
if (e.Offset + e.RemovalLength == StartOffset && e.RemovalLength > 0)
|
|
|
|
{
|
|
|
|
Hide(); // removal immediately in front of completion segment: close the window
|
|
|
|
// this is necessary when pressing backspace after dot-completion
|
|
|
|
}
|
|
|
|
if (e.Offset == StartOffset && e.RemovalLength == 0 && ExpectInsertionBeforeStart)
|
|
|
|
{
|
|
|
|
StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.AfterInsertion);
|
|
|
|
ExpectInsertionBeforeStart = false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.BeforeInsertion);
|
|
|
|
}
|
|
|
|
EndOffset = e.GetNewOffset(EndOffset, AnchorMovementType.AfterInsertion);
|
|
|
|
}
|
|
|
|
}
|