mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
397 lines
13 KiB
397 lines
13 KiB
// 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.Linq; |
|
using System.Windows; |
|
using System.Windows.Controls.Primitives; |
|
using System.Windows.Input; |
|
using System.Windows.Threading; |
|
|
|
using ICSharpCode.AvalonEdit.Document; |
|
using ICSharpCode.AvalonEdit.Editing; |
|
using ICSharpCode.AvalonEdit.Rendering; |
|
using ICSharpCode.AvalonEdit.Utils; |
|
using ICSharpCode.NRefactory.Editor; |
|
|
|
namespace ICSharpCode.AvalonEdit.CodeCompletion |
|
{ |
|
/// <summary> |
|
/// Base class for completion windows. Handles positioning the window at the caret. |
|
/// </summary> |
|
public class CompletionWindowBase : Window |
|
{ |
|
static CompletionWindowBase() |
|
{ |
|
WindowStyleProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(WindowStyle.None)); |
|
ShowActivatedProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(Boxes.False)); |
|
ShowInTaskbarProperty.OverrideMetadata(typeof(CompletionWindowBase), new FrameworkPropertyMetadata(Boxes.False)); |
|
} |
|
|
|
/// <summary> |
|
/// Gets the parent TextArea. |
|
/// </summary> |
|
public TextArea TextArea { get; private set; } |
|
|
|
Window parentWindow; |
|
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) |
|
{ |
|
if (textArea == null) |
|
throw new ArgumentNullException("textArea"); |
|
this.TextArea = textArea; |
|
parentWindow = Window.GetWindow(textArea); |
|
this.Owner = parentWindow; |
|
this.AddHandler(MouseUpEvent, new MouseButtonEventHandler(OnMouseUp), true); |
|
|
|
StartOffset = EndOffset = this.TextArea.Caret.Offset; |
|
|
|
AttachEvents(); |
|
} |
|
|
|
#region Event Handlers |
|
void AttachEvents() |
|
{ |
|
document = this.TextArea.Document; |
|
if (document != null) { |
|
document.Changing += textArea_Document_Changing; |
|
} |
|
// LostKeyboardFocus seems to be more reliable than PreviewLostKeyboardFocus - see SD-1729 |
|
this.TextArea.LostKeyboardFocus += TextAreaLostFocus; |
|
this.TextArea.TextView.ScrollOffsetChanged += TextViewScrollOffsetChanged; |
|
this.TextArea.DocumentChanged += TextAreaDocumentChanged; |
|
if (parentWindow != null) { |
|
parentWindow.LocationChanged += parentWindow_LocationChanged; |
|
} |
|
|
|
// close previous completion windows of same type |
|
foreach (InputHandler x in this.TextArea.StackedInputHandlers.OfType<InputHandler>()) { |
|
if (x.window.GetType() == this.GetType()) |
|
this.TextArea.PopStackedInputHandler(x); |
|
} |
|
|
|
myInputHandler = new InputHandler(this); |
|
this.TextArea.PushStackedInputHandler(myInputHandler); |
|
} |
|
|
|
/// <summary> |
|
/// Detaches events from the text area. |
|
/// </summary> |
|
protected virtual void DetachEvents() |
|
{ |
|
if (document != null) { |
|
document.Changing -= textArea_Document_Changing; |
|
} |
|
this.TextArea.LostKeyboardFocus -= TextAreaLostFocus; |
|
this.TextArea.TextView.ScrollOffsetChanged -= TextViewScrollOffsetChanged; |
|
this.TextArea.DocumentChanged -= TextAreaDocumentChanged; |
|
if (parentWindow != null) { |
|
parentWindow.LocationChanged -= parentWindow_LocationChanged; |
|
} |
|
this.TextArea.PopStackedInputHandler(myInputHandler); |
|
} |
|
|
|
#region InputHandler |
|
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> |
|
sealed class InputHandler : TextAreaStackedInputHandler |
|
{ |
|
internal readonly CompletionWindowBase window; |
|
|
|
public InputHandler(CompletionWindowBase window) |
|
: base(window.TextArea) |
|
{ |
|
Debug.Assert(window != null); |
|
this.window = window; |
|
} |
|
|
|
public override void Detach() |
|
{ |
|
base.Detach(); |
|
window.Close(); |
|
} |
|
|
|
const Key KeyDeadCharProcessed = (Key)0xac; // Key.DeadCharProcessed; // new in .NET 4 |
|
|
|
public override void OnPreviewKeyDown(KeyEventArgs e) |
|
{ |
|
// prevents crash when typing deadchar while CC window is open |
|
if (e.Key == KeyDeadCharProcessed) |
|
return; |
|
e.Handled = RaiseEventPair(window, PreviewKeyDownEvent, KeyDownEvent, |
|
new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key)); |
|
} |
|
|
|
public override void OnPreviewKeyUp(KeyEventArgs e) |
|
{ |
|
if (e.Key == KeyDeadCharProcessed) |
|
return; |
|
e.Handled = RaiseEventPair(window, PreviewKeyUpEvent, KeyUpEvent, |
|
new KeyEventArgs(e.KeyboardDevice, e.InputSource, e.Timestamp, e.Key)); |
|
} |
|
} |
|
#endregion |
|
|
|
void TextViewScrollOffsetChanged(object sender, EventArgs e) |
|
{ |
|
// Workaround for crash #1580 (reproduction steps unknown): |
|
// NullReferenceException in System.Windows.Window.CreateSourceWindow() |
|
if (!sourceIsInitialized) |
|
return; |
|
|
|
IScrollInfo scrollInfo = this.TextArea.TextView; |
|
Rect visibleRect = new Rect(scrollInfo.HorizontalOffset, scrollInfo.VerticalOffset, scrollInfo.ViewportWidth, scrollInfo.ViewportHeight); |
|
// 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 |
|
Close(); |
|
} |
|
|
|
void TextAreaDocumentChanged(object sender, EventArgs e) |
|
{ |
|
Close(); |
|
} |
|
|
|
void TextAreaLostFocus(object sender, RoutedEventArgs e) |
|
{ |
|
Dispatcher.BeginInvoke(new Action(CloseIfFocusLost), DispatcherPriority.Background); |
|
} |
|
|
|
void parentWindow_LocationChanged(object sender, EventArgs e) |
|
{ |
|
UpdatePosition(); |
|
} |
|
|
|
/// <inheritdoc/> |
|
protected override void OnDeactivated(EventArgs e) |
|
{ |
|
base.OnDeactivated(e); |
|
Dispatcher.BeginInvoke(new Action(CloseIfFocusLost), DispatcherPriority.Background); |
|
} |
|
#endregion |
|
|
|
/// <summary> |
|
/// Raises a tunnel/bubble event pair for a WPF control. |
|
/// </summary> |
|
/// <param name="target">The WPF 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> |
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")] |
|
protected static bool RaiseEventPair(UIElement target, RoutedEvent previewEvent, RoutedEvent @event, RoutedEventArgs args) |
|
{ |
|
if (target == null) |
|
throw new ArgumentNullException("target"); |
|
if (previewEvent == null) |
|
throw new ArgumentNullException("previewEvent"); |
|
if (@event == null) |
|
throw new ArgumentNullException("event"); |
|
if (args == null) |
|
throw new ArgumentNullException("args"); |
|
args.RoutedEvent = previewEvent; |
|
target.RaiseEvent(args); |
|
args.RoutedEvent = @event; |
|
target.RaiseEvent(args); |
|
return args.Handled; |
|
} |
|
|
|
// Special handler: handledEventsToo |
|
void OnMouseUp(object sender, MouseButtonEventArgs e) |
|
{ |
|
ActivateParentWindow(); |
|
} |
|
|
|
/// <summary> |
|
/// Activates the parent window. |
|
/// </summary> |
|
protected virtual void ActivateParentWindow() |
|
{ |
|
if (parentWindow != null) |
|
parentWindow.Activate(); |
|
} |
|
|
|
void CloseIfFocusLost() |
|
{ |
|
if (CloseOnFocusLost) { |
|
Debug.WriteLine("CloseIfFocusLost: this.IsActive=" + this.IsActive + " IsTextAreaFocused=" + IsTextAreaFocused); |
|
if (!this.IsActive && !IsTextAreaFocused) { |
|
Close(); |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Gets whether the completion window should automatically close when the text editor looses focus. |
|
/// </summary> |
|
protected virtual bool CloseOnFocusLost { |
|
get { return true; } |
|
} |
|
|
|
bool IsTextAreaFocused { |
|
get { |
|
if (parentWindow != null && !parentWindow.IsActive) |
|
return false; |
|
return this.TextArea.IsKeyboardFocused; |
|
} |
|
} |
|
|
|
bool sourceIsInitialized; |
|
|
|
/// <inheritdoc/> |
|
protected override void OnSourceInitialized(EventArgs e) |
|
{ |
|
base.OnSourceInitialized(e); |
|
|
|
if (document != null && this.StartOffset != this.TextArea.Caret.Offset) { |
|
SetPosition(new TextViewPosition(document.GetLocation(this.StartOffset))); |
|
} else { |
|
SetPosition(this.TextArea.Caret.Position); |
|
} |
|
sourceIsInitialized = true; |
|
} |
|
|
|
/// <inheritdoc/> |
|
protected override void OnClosed(EventArgs e) |
|
{ |
|
base.OnClosed(e); |
|
DetachEvents(); |
|
} |
|
|
|
/// <inheritdoc/> |
|
protected override void OnKeyDown(KeyEventArgs e) |
|
{ |
|
base.OnKeyDown(e); |
|
if (!e.Handled && e.Key == Key.Escape) { |
|
e.Handled = true; |
|
Close(); |
|
} |
|
} |
|
|
|
Point visualLocation, visualLocationTop; |
|
|
|
/// <summary> |
|
/// Positions the completion window at the specified position. |
|
/// </summary> |
|
protected void SetPosition(TextViewPosition position) |
|
{ |
|
TextView textView = this.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() |
|
{ |
|
TextView textView = this.TextArea.TextView; |
|
// PointToScreen returns device dependent units (physical pixels) |
|
Point location = textView.PointToScreen(visualLocation - textView.ScrollOffset); |
|
Point locationTop = textView.PointToScreen(visualLocationTop - textView.ScrollOffset); |
|
|
|
// Let's use device dependent units for everything |
|
Size completionWindowSize = new Size(this.ActualWidth, this.ActualHeight).TransformToDevice(textView); |
|
Rect bounds = new Rect(location, completionWindowSize); |
|
Rect workingScreen = System.Windows.Forms.Screen.GetWorkingArea(location.ToSystemDrawing()).ToWpf(); |
|
if (!workingScreen.Contains(bounds)) { |
|
if (bounds.Left < workingScreen.Left) { |
|
bounds.X = workingScreen.Left; |
|
} else if (bounds.Right > workingScreen.Right) { |
|
bounds.X = workingScreen.Right - bounds.Width; |
|
} |
|
if (bounds.Bottom > workingScreen.Bottom) { |
|
bounds.Y = locationTop.Y - bounds.Height; |
|
IsUp = true; |
|
} else { |
|
IsUp = false; |
|
} |
|
if (bounds.Y < workingScreen.Top) { |
|
bounds.Y = workingScreen.Top; |
|
} |
|
} |
|
// Convert the window bounds to device independent units |
|
bounds = bounds.TransformFromDevice(textView); |
|
this.Left = bounds.X; |
|
this.Top = bounds.Y; |
|
} |
|
|
|
/// <inheritdoc/> |
|
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; } |
|
|
|
void textArea_Document_Changing(object sender, DocumentChangeEventArgs e) |
|
{ |
|
if (e.Offset + e.RemovalLength == this.StartOffset && e.RemovalLength > 0) { |
|
Close(); // 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); |
|
this.ExpectInsertionBeforeStart = false; |
|
} else { |
|
StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.BeforeInsertion); |
|
} |
|
EndOffset = e.GetNewOffset(EndOffset, AnchorMovementType.AfterInsertion); |
|
} |
|
} |
|
}
|
|
|