Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to insert completion item on PointerPressed/PointerReleased/DoubleTapped #451

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/AvaloniaEdit.Demo/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public MainWindow()
_textEditor.TextArea.Caret.PositionChanged += Caret_PositionChanged;
_textEditor.TextArea.RightClickMovesCaret = true;
_textEditor.Options.HighlightCurrentLine = true;
_textEditor.Options.CompletionAcceptAction = CompletionAcceptAction.DoubleTapped;

_addControlButton = this.FindControl<Button>("addControlBtn");
_addControlButton.Click += AddControlButton_Click;
Expand Down
25 changes: 25 additions & 0 deletions src/AvaloniaEdit/CodeCompletion/CompletionAcceptAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace AvaloniaEdit.CodeCompletion;

/// <summary>
/// Defines the pointer action used to request the insertion of a completion item.
/// </summary>
public enum CompletionAcceptAction
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like it could be a flags enum, with a possibility to set None option at least.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current options PointerPressed/PointerReleased/DoubleTapped are mutually exclusive and can't be combined as flags.

I am not sure if we want to allow None. That would prevent users from using the mouse for insertions. I suggest to only add None if there is practical use case.

The current defaults (TextEditorOptions.CompletionAcceptAction == CompletionAcceptAction.PointerPressed) ensure that the behavior in existing apps is unchanged.

{
/// <summary>
/// Insert the completion item when the pointer is pressed. (This option makes the completion
/// list behave similar to the completion list in Visual Studio Code.)
/// </summary>
PointerPressed,

/// <summary>
/// Insert the completion item when the pointer is pressed. (This option makes the completion
/// list behave similar to a context menu.)
/// </summary>
PointerReleased,

/// <summary>
/// Insert the code completion item when the item is double-tapped. (This option makes the
/// completion list behave similar to the completion list in Visual Studio.)
/// </summary>
DoubleTapped
}
115 changes: 88 additions & 27 deletions src/AvaloniaEdit/CodeCompletion/CompletionList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Threading;
using Avalonia.VisualTree;
using AvaloniaEdit.Utils;

namespace AvaloniaEdit.CodeCompletion
Expand All @@ -37,18 +38,6 @@ namespace AvaloniaEdit.CodeCompletion
/// </summary>
public class CompletionList : TemplatedControl
{
public CompletionList()
{
AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Bubble, true);

CompletionAcceptKeys = new[]
{
Key.Enter,
Key.Tab,
};
}


/// <summary>
/// If true, the CompletionList is filtered to show only matching items. Also enables search by substring.
/// If false, enables the old behavior: no filtering, search by string.StartsWith.
Expand Down Expand Up @@ -95,6 +84,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
if (_listBox != null)
{
_listBox.ItemsSource = _completionData;
AddPointerHandler(CompletionAcceptAction);
}
}

Expand All @@ -111,10 +101,29 @@ public CompletionListBox ListBox
}
}

/// <summary>
/// Gets or sets the pointer action used to request insertion of a completion item.
/// </summary>
public CompletionAcceptAction CompletionAcceptAction
{
get => _completionAcceptAction;
set
{
if (_completionAcceptAction == value)
return;

RemovePointerHandler(_completionAcceptAction);
_completionAcceptAction = value;
AddPointerHandler(value);
}
}

private CompletionAcceptAction _completionAcceptAction;

/// <summary>
/// Gets or sets the array of keys that are supposed to request insertion of the completion.
/// </summary>
public Key[] CompletionAcceptKeys { get; set; }
public Key[] CompletionAcceptKeys { get; set; } = new[] { Key.Enter, Key.Tab };

/// <summary>
/// Gets the scroll viewer used in this list box.
Expand Down Expand Up @@ -192,26 +201,78 @@ public void HandleKey(KeyEventArgs e)
}
}

private void OnPointerPressed(object sender, PointerPressedEventArgs e)
private void AddPointerHandler(CompletionAcceptAction completionAcceptAction)
{
var visual = e.Source as Visual;
if (e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
if (_listBox == null)
return;

switch (completionAcceptAction)
{
var listBoxItem = visual.VisualAncestorsAndSelf()
.TakeWhile(v => v != this)
.OfType<ListBoxItem>()
.FirstOrDefault();
case CompletionAcceptAction.PointerPressed:
_listBox.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Bubble, true);
break;
case CompletionAcceptAction.PointerReleased:
_listBox.AddHandler(PointerReleasedEvent, OnPointerReleased);
break;
case CompletionAcceptAction.DoubleTapped:
AddHandler(DoubleTappedEvent, OnDoubleTapped);
_listBox.AddHandler(DoubleTappedEvent, OnDoubleTapped);
break;
default:
Debug.Fail("Invalid CompletionAcceptAction");
break;
}
}

if (listBoxItem != null)
{
// A completion item was clicked.
Debug.Assert(e.Handled, "Click expected to be handled by ListBoxItem.");
Debug.Assert(listBoxItem.IsSelected, "Completion item expected to be selected.");
RequestInsertion(e);
}
private void RemovePointerHandler(CompletionAcceptAction completionAcceptAction)
{
if (_listBox == null)
return;

switch (completionAcceptAction)
{
case CompletionAcceptAction.PointerPressed:
_listBox.RemoveHandler(PointerPressedEvent, OnPointerPressed);
break;
case CompletionAcceptAction.PointerReleased:
_listBox.RemoveHandler(PointerReleasedEvent, OnPointerReleased);
break;
case CompletionAcceptAction.DoubleTapped:
_listBox.RemoveHandler(DoubleTappedEvent, OnDoubleTapped);
break;
default:
Debug.Fail("Invalid CompletionAcceptAction");
break;
}
}

private void OnPointerPressed(object sender, PointerPressedEventArgs e)
{
var visual = e.Source as Visual;
if (!e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
return;

RequestInsertion(e);
}

private void OnPointerReleased(object sender, PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton != MouseButton.Left)
return;

// Ignore event if pointer is released outside the selected item.
var listBoxItem = _listBox.ContainerFromIndex(_listBox.SelectedIndex);
if (listBoxItem == null || !this.GetVisualsAt(e.GetPosition(this)).Any(v => v == listBoxItem || listBoxItem.IsVisualAncestorOf(v)))
return;

RequestInsertion(e);
}

private void OnDoubleTapped(object sender, TappedEventArgs e)
{
RequestInsertion(e);
}

/// <summary>
/// Gets/Sets the selected item.
/// </summary>
Expand Down
13 changes: 9 additions & 4 deletions src/AvaloniaEdit/CodeCompletion/CompletionWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;

namespace AvaloniaEdit.CodeCompletion
{
Expand All @@ -45,7 +43,11 @@ public class CompletionWindow : CompletionWindowBase
/// </summary>
public CompletionWindow(TextArea textArea) : base(textArea)
{
CompletionList = new CompletionList();
CompletionList = new CompletionList
{
CompletionAcceptAction = textArea.Options.CompletionAcceptAction
};

// keep height automatic
CloseAutomatically = true;
MaxHeight = 225;
Expand All @@ -59,7 +61,10 @@ public CompletionWindow(TextArea textArea) : base(textArea)

_toolTip = new PopupWithCustomPosition
{
IsLightDismissEnabled = true,
// The popup should not interfere with the pointer input. Popup visibility is
// controlled explicitly.
IsLightDismissEnabled = false,

PlacementTarget = this,
Child = _toolTipContent,
};
Expand Down
1 change: 0 additions & 1 deletion src/AvaloniaEdit/CodeCompletion/CompletionWindowBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ private void ParentWindow_LocationChanged(object sender, EventArgs e)
UpdatePosition();
}

/// <inheritdoc/>
private void OnDeactivated(object sender, EventArgs e)
{
Dispatcher.UIThread.Post(CloseIfFocusLost, DispatcherPriority.Background);
Expand Down
20 changes: 20 additions & 0 deletions src/AvaloniaEdit/TextEditorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using AvaloniaEdit.CodeCompletion;

namespace AvaloniaEdit
{
Expand Down Expand Up @@ -699,5 +700,24 @@ public bool ExtendSelectionOnMouseUp
}
}
}

private CompletionAcceptAction _completionAcceptAction = CompletionAcceptAction.PointerPressed;

/// <summary>
/// Gets/Sets the pointer action used to request the insertion of a completion item.
/// </summary>
[DefaultValue(CompletionAcceptAction.PointerPressed)]
public CompletionAcceptAction CompletionAcceptAction
{
get { return _completionAcceptAction; }
set
{
if (_completionAcceptAction != value)
{
_completionAcceptAction = value;
OnPropertyChanged(nameof(CompletionAcceptAction));
}
}
}
}
}