Ionite
1 year ago
19 changed files with 2178 additions and 11 deletions
@ -0,0 +1,162 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using Avalonia; |
||||
using Avalonia.Input; |
||||
using Avalonia.Xaml.Interactivity; |
||||
using AvaloniaEdit; |
||||
using AvaloniaEdit.Document; |
||||
using AvaloniaEdit.Editing; |
||||
using NLog; |
||||
using StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Behaviors; |
||||
|
||||
public class TextEditorCompletionBehavior : Behavior<TextEditor> |
||||
{ |
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||
|
||||
private TextEditor textEditor = null!; |
||||
|
||||
private CompletionWindow? completionWindow; |
||||
|
||||
// ReSharper disable once MemberCanBePrivate.Global |
||||
public static readonly StyledProperty<string> TextProperty = |
||||
AvaloniaProperty.Register<TextEditorCompletionBehavior, string>(nameof(Text)); |
||||
|
||||
public string Text |
||||
{ |
||||
get => GetValue(TextProperty); |
||||
set => SetValue(TextProperty, value); |
||||
} |
||||
|
||||
protected override void OnAttached() |
||||
{ |
||||
base.OnAttached(); |
||||
|
||||
if (AssociatedObject is not { } editor) |
||||
{ |
||||
throw new NullReferenceException("AssociatedObject is null"); |
||||
} |
||||
|
||||
textEditor = editor; |
||||
textEditor.TextArea.TextEntered += TextArea_TextEntered; |
||||
textEditor.TextArea.TextEntering += TextArea_TextEntering; |
||||
} |
||||
|
||||
protected override void OnDetaching() |
||||
{ |
||||
base.OnDetaching(); |
||||
|
||||
textEditor.TextArea.TextEntered -= TextArea_TextEntered; |
||||
textEditor.TextArea.TextEntering -= TextArea_TextEntering; |
||||
} |
||||
|
||||
private CompletionWindow CreateCompletionWindow(TextArea textArea) |
||||
{ |
||||
var window = new CompletionWindow(textArea) |
||||
{ |
||||
WindowManagerAddShadowHint = false, |
||||
CloseWhenCaretAtBeginning = true, |
||||
CloseAutomatically = true, |
||||
IsLightDismissEnabled = true, |
||||
CompletionList = |
||||
{ |
||||
IsFiltering = true |
||||
} |
||||
}; |
||||
|
||||
var completionList = window.CompletionList; |
||||
|
||||
completionList.CompletionData.Add(new CompletionData("item1")); |
||||
completionList.CompletionData.Add(new CompletionData("item2")); |
||||
completionList.CompletionData.Add(new CompletionData("item3")); |
||||
|
||||
return window; |
||||
} |
||||
|
||||
private void TextArea_TextEntered(object? sender, TextInputEventArgs e) |
||||
{ |
||||
if (e.Text is not { } triggerText) return; |
||||
|
||||
if (triggerText.All(char.IsLetterOrDigit)) |
||||
{ |
||||
// Create completion window if its not already created |
||||
if (completionWindow == null) |
||||
{ |
||||
// Get the segment of the token the caret is currently in |
||||
if (GetCaretToken(textEditor) is not { } tokenSegment) |
||||
{ |
||||
Logger.Trace("Token segment not found"); |
||||
return; |
||||
} |
||||
|
||||
var token = textEditor.Document.GetText(tokenSegment); |
||||
Logger.Trace("Using token {Token} ({@Segment})", token, tokenSegment); |
||||
|
||||
completionWindow = CreateCompletionWindow(textEditor.TextArea); |
||||
completionWindow.StartOffset = tokenSegment.Offset; |
||||
completionWindow.EndOffset = tokenSegment.EndOffset; |
||||
|
||||
completionWindow.CompletionList.SelectItem(token); |
||||
|
||||
completionWindow.Closed += delegate |
||||
{ |
||||
completionWindow = null; |
||||
}; |
||||
|
||||
completionWindow.Show(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void TextArea_TextEntering(object? sender, TextInputEventArgs e) |
||||
{ |
||||
if (completionWindow is null) return; |
||||
|
||||
// When completion window is open, parse and update token offsets |
||||
if (GetCaretToken(textEditor) is not { } tokenSegment) |
||||
{ |
||||
Logger.Trace("Token segment not found"); |
||||
return; |
||||
} |
||||
|
||||
completionWindow.StartOffset = tokenSegment.Offset; |
||||
completionWindow.EndOffset = tokenSegment.EndOffset; |
||||
|
||||
/*if (e.Text?.Length > 0) { |
||||
if (!char.IsLetterOrDigit(e.Text[0])) { |
||||
// Whenever a non-letter is typed while the completion window is open, |
||||
// insert the currently selected element. |
||||
completionWindow?.CompletionList.RequestInsertion(e); |
||||
} |
||||
}*/ |
||||
// Do not set e.Handled=true. |
||||
// We still want to insert the character that was typed. |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets a segment of the token the caret is currently in. |
||||
/// </summary> |
||||
private static ISegment? GetCaretToken(TextEditor textEditor) |
||||
{ |
||||
var caret = textEditor.CaretOffset; |
||||
|
||||
// Search for the start and end of a token |
||||
// A token is defined as either alphanumeric chars or a space |
||||
var start = caret; |
||||
while (start > 0 && char.IsLetterOrDigit(textEditor.Document.GetCharAt(start - 1))) |
||||
{ |
||||
start--; |
||||
} |
||||
|
||||
var end = caret; |
||||
while (end < textEditor.Document.TextLength && char.IsLetterOrDigit(textEditor.Document.GetCharAt(end))) |
||||
{ |
||||
end++; |
||||
} |
||||
|
||||
return start < end ? new TextSegment { StartOffset = start, EndOffset = end } : null; |
||||
} |
||||
} |
@ -0,0 +1,128 @@
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Documents; |
||||
using Avalonia.Media; |
||||
using AvaloniaEdit.Document; |
||||
using AvaloniaEdit.Editing; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.Styles; |
||||
using StabilityMatrix.Core.Extensions; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
|
||||
/// <summary> |
||||
/// Provides entries in AvaloniaEdit completion window. |
||||
/// </summary> |
||||
public class CompletionData : ICompletionData |
||||
{ |
||||
/// <inheritdoc /> |
||||
public string Text { get; } |
||||
|
||||
/// <inheritdoc /> |
||||
public string? Description { get; init; } |
||||
|
||||
/// <inheritdoc /> |
||||
public IImage? Image { get; set; } |
||||
|
||||
/// <inheritdoc /> |
||||
public IconData? Icon { get; init; } |
||||
|
||||
/// <summary> |
||||
/// Cached <see cref="TextBlock"/> instance. |
||||
/// </summary> |
||||
private object? _content; |
||||
|
||||
/// <inheritdoc /> |
||||
public object Content => _content ??= new TextBlock |
||||
{ |
||||
Inlines = CreateInlines() |
||||
}; |
||||
|
||||
/// <summary> |
||||
/// Get the current inlines |
||||
/// </summary> |
||||
public InlineCollection TextInlines => ((TextBlock) Content).Inlines!; |
||||
|
||||
/// <inheritdoc /> |
||||
public double Priority { get; } |
||||
|
||||
public CompletionData(string text) |
||||
{ |
||||
Text = text; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Create text block inline runs from text. |
||||
/// </summary> |
||||
private InlineCollection CreateInlines() |
||||
{ |
||||
// Create a span for each character in the text. |
||||
var chars = Text.ToCharArray(); |
||||
var inlines = new InlineCollection(); |
||||
|
||||
foreach (var c in chars) |
||||
{ |
||||
var run = new Run(c.ToString()); |
||||
inlines.Add(run); |
||||
} |
||||
|
||||
return inlines; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) |
||||
{ |
||||
textArea.Document.Replace(completionSegment, Text); |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void UpdateCharHighlighting(string searchText) |
||||
{ |
||||
if (TextInlines is null) |
||||
{ |
||||
throw new NullReferenceException("TextContent is null"); |
||||
} |
||||
|
||||
Debug.WriteLine($"Updating char highlighting for {Text} with search text {searchText}"); |
||||
|
||||
var defaultColor = ThemeColors.CompletionForegroundBrush; |
||||
var highlightColor = ThemeColors.CompletionSelectionForegroundBrush; |
||||
|
||||
// Match characters in the text with the search text from the start |
||||
foreach (var (i, currentChar) in Text.Enumerate()) |
||||
{ |
||||
var inline = TextInlines[i]; |
||||
|
||||
// If longer than text, set to default color |
||||
if (i >= searchText.Length) |
||||
{ |
||||
inline.Foreground = defaultColor; |
||||
continue; |
||||
} |
||||
|
||||
// If char matches, highlight it |
||||
if (currentChar == searchText[i]) |
||||
{ |
||||
inline.Foreground = highlightColor; |
||||
} |
||||
// For mismatch, set to default color |
||||
else |
||||
{ |
||||
inline.Foreground = defaultColor; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void ResetCharHighlighting() |
||||
{ |
||||
// TODO: handle light theme foreground variant |
||||
var defaultColor = ThemeColors.CompletionForegroundBrush; |
||||
|
||||
foreach (var inline in TextInlines) |
||||
{ |
||||
inline.Foreground = defaultColor; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,69 @@
|
||||
using System; |
||||
using System.Diagnostics.CodeAnalysis; |
||||
using Projektanker.Icons.Avalonia; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
using StabilityMatrix.Avalonia.Models.TagCompletion; |
||||
using StabilityMatrix.Avalonia.Styles; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] |
||||
public static class CompletionIcons |
||||
{ |
||||
public static readonly IconData General = new() |
||||
{ |
||||
FAIcon = "fa-solid fa-star-of-life", |
||||
Foreground = ThemeColors.LightSteelBlue, |
||||
}; |
||||
|
||||
public static readonly IconData Artist = new() |
||||
{ |
||||
FAIcon = "fa-solid fa-palette", |
||||
Foreground = ThemeColors.AmericanYellow, |
||||
}; |
||||
|
||||
public static readonly IconData Character = new() |
||||
{ |
||||
FAIcon = "fa-solid fa-user", |
||||
Foreground = ThemeColors.LuminousGreen, |
||||
}; |
||||
|
||||
public static readonly IconData Copyright = new() |
||||
{ |
||||
FAIcon = "fa-solid fa-copyright", |
||||
Foreground = ThemeColors.DeepMagenta, |
||||
}; |
||||
|
||||
public static readonly IconData Species = new() |
||||
{ |
||||
FAIcon = "fa-solid fa-dragon", |
||||
FontSize = 14, |
||||
Foreground = ThemeColors.HalloweenOrange, |
||||
}; |
||||
|
||||
public static readonly IconData Invalid = new() |
||||
{ |
||||
FAIcon = "fa-solid fa-question", |
||||
Foreground = ThemeColors.CompletionForegroundBrush, |
||||
}; |
||||
|
||||
public static readonly IconData Keyword = new() |
||||
{ |
||||
FAIcon = "fa-solid fa-key", |
||||
Foreground = ThemeColors.CompletionForegroundBrush, |
||||
}; |
||||
|
||||
public static IconData? GetIconForTagType(TagType tagType) |
||||
{ |
||||
return tagType switch |
||||
{ |
||||
TagType.General => General, |
||||
TagType.Artist => Artist, |
||||
TagType.Character => Character, |
||||
TagType.Species => Species, |
||||
TagType.Invalid => Invalid, |
||||
TagType.Copyright => Copyright, |
||||
_ => null |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,480 @@
|
||||
// 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.CodeAnalysis; |
||||
using System.Linq; |
||||
using Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Primitives; |
||||
using Avalonia.Input; |
||||
using Avalonia.Interactivity; |
||||
using Avalonia.Markup.Xaml.Templates; |
||||
using AvaloniaEdit.Utils; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
|
||||
/// <summary> |
||||
/// The listbox used inside the CompletionWindow, contains CompletionListBox. |
||||
/// </summary> |
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] |
||||
public class CompletionList : TemplatedControl |
||||
{ |
||||
public CompletionList() |
||||
{ |
||||
DoubleTapped += OnDoubleTapped; |
||||
|
||||
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. |
||||
/// </summary> |
||||
public bool IsFiltering { get; set; } = true; |
||||
|
||||
/// <summary> |
||||
/// Dependency property for <see cref="EmptyTemplate" />. |
||||
/// </summary> |
||||
public static readonly StyledProperty<ControlTemplate> EmptyTemplateProperty = |
||||
AvaloniaProperty.Register<CompletionList, ControlTemplate>(nameof(EmptyTemplate)); |
||||
|
||||
/// <summary> |
||||
/// Content of EmptyTemplate will be shown when CompletionList contains no items. |
||||
/// If EmptyTemplate is null, nothing will be shown. |
||||
/// </summary> |
||||
public ControlTemplate EmptyTemplate |
||||
{ |
||||
get => GetValue(EmptyTemplateProperty); |
||||
set => SetValue(EmptyTemplateProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Dependency property for <see cref="FooterText" />. |
||||
/// </summary> |
||||
public static readonly StyledProperty<string?> FooterTextProperty = AvaloniaProperty.Register<CompletionList, string?>( |
||||
"FooterText", "Press Enter to insert, Tab to replace"); |
||||
|
||||
/// <summary> |
||||
/// Gets/Sets the text displayed in the footer of the completion list. |
||||
/// </summary> |
||||
public string? FooterText |
||||
{ |
||||
get => GetValue(FooterTextProperty); |
||||
set => SetValue(FooterTextProperty, value); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Is raised when the completion list indicates that the user has chosen |
||||
/// an entry to be completed. |
||||
/// </summary> |
||||
public event EventHandler? InsertionRequested; |
||||
|
||||
/// <summary> |
||||
/// Raises the InsertionRequested event. |
||||
/// </summary> |
||||
public void RequestInsertion(EventArgs e) |
||||
{ |
||||
InsertionRequested?.Invoke(this, e); |
||||
} |
||||
|
||||
private CompletionListBox? _listBox; |
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
||||
{ |
||||
base.OnApplyTemplate(e); |
||||
|
||||
_listBox = e.NameScope.Find("PART_ListBox") as CompletionListBox; |
||||
if (_listBox is not null) |
||||
{ |
||||
_listBox.ItemsSource = _completionData; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets the list box. |
||||
/// </summary> |
||||
public CompletionListBox? ListBox |
||||
{ |
||||
get |
||||
{ |
||||
if (_listBox == null) |
||||
ApplyTemplate(); |
||||
return _listBox; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the array of keys that are supposed to request insertation of the completion |
||||
/// </summary> |
||||
public Key[] CompletionAcceptKeys { get; set; } |
||||
|
||||
/// <summary> |
||||
/// Gets the scroll viewer used in this list box. |
||||
/// </summary> |
||||
public ScrollViewer? ScrollViewer => _listBox?.ScrollViewer; |
||||
|
||||
private readonly ObservableCollection<ICompletionData> _completionData = new(); |
||||
|
||||
/// <summary> |
||||
/// Gets the list to which completion data can be added. |
||||
/// </summary> |
||||
public IList<ICompletionData> CompletionData => _completionData; |
||||
|
||||
/// <inheritdoc/> |
||||
protected override void OnKeyDown(KeyEventArgs e) |
||||
{ |
||||
base.OnKeyDown(e); |
||||
if (!e.Handled) |
||||
{ |
||||
HandleKey(e); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Handles a key press. Used to let the completion list handle key presses while the |
||||
/// focus is still on the text editor. |
||||
/// </summary> |
||||
public void HandleKey(KeyEventArgs e) |
||||
{ |
||||
if (_listBox == null) |
||||
return; |
||||
|
||||
// We have to do some key handling manually, because the default doesn't work with |
||||
// our simulated events. |
||||
// Also, the default PageUp/PageDown implementation changes the focus, so we avoid it. |
||||
switch (e.Key) |
||||
{ |
||||
case Key.Down: |
||||
e.Handled = true; |
||||
_listBox.SelectIndex(_listBox.SelectedIndex + 1); |
||||
break; |
||||
case Key.Up: |
||||
e.Handled = true; |
||||
_listBox.SelectIndex(_listBox.SelectedIndex - 1); |
||||
break; |
||||
case Key.PageDown: |
||||
e.Handled = true; |
||||
_listBox.SelectIndex(_listBox.SelectedIndex + _listBox.VisibleItemCount); |
||||
break; |
||||
case Key.PageUp: |
||||
e.Handled = true; |
||||
_listBox.SelectIndex(_listBox.SelectedIndex - _listBox.VisibleItemCount); |
||||
break; |
||||
case Key.Home: |
||||
e.Handled = true; |
||||
_listBox.SelectIndex(0); |
||||
break; |
||||
case Key.End: |
||||
e.Handled = true; |
||||
_listBox.SelectIndex(_listBox.ItemCount - 1); |
||||
break; |
||||
default: |
||||
if (CompletionAcceptKeys.Contains(e.Key) && CurrentList.Count > 0) |
||||
{ |
||||
e.Handled = true; |
||||
RequestInsertion(e); |
||||
} |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
protected void OnDoubleTapped(object? sender, RoutedEventArgs e) |
||||
{ |
||||
//TODO TEST |
||||
if ( |
||||
((AvaloniaObject?) e.Source) |
||||
.VisualAncestorsAndSelf() |
||||
.TakeWhile(obj => obj != this) |
||||
.Any(obj => obj is ListBoxItem) |
||||
) |
||||
{ |
||||
e.Handled = true; |
||||
RequestInsertion(e); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets/Sets the selected item. |
||||
/// </summary> |
||||
/// <remarks> |
||||
/// The setter of this property does not scroll to the selected item. |
||||
/// You might want to also call <see cref="ScrollIntoView"/>. |
||||
/// </remarks> |
||||
public ICompletionData? SelectedItem |
||||
{ |
||||
get => _listBox?.SelectedItem as ICompletionData; |
||||
set |
||||
{ |
||||
if (_listBox == null && value != null) |
||||
ApplyTemplate(); |
||||
if (_listBox != null) // may still be null if ApplyTemplate fails, or if listBox and value both are null |
||||
_listBox.SelectedItem = value; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Scrolls the specified item into view. |
||||
/// </summary> |
||||
public void ScrollIntoView(ICompletionData item) |
||||
{ |
||||
if (_listBox == null) |
||||
ApplyTemplate(); |
||||
_listBox?.ScrollIntoView(item); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Occurs when the SelectedItem property changes. |
||||
/// </summary> |
||||
public event EventHandler<SelectionChangedEventArgs> SelectionChanged |
||||
{ |
||||
add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value); |
||||
remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value); |
||||
} |
||||
|
||||
// SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once |
||||
private string _currentText; |
||||
|
||||
private ObservableCollection<ICompletionData>? _currentList; |
||||
|
||||
public List<ICompletionData>? CurrentList => ListBox?.Items.Cast<ICompletionData>().ToList(); |
||||
|
||||
/// <summary> |
||||
/// Selects the best match, and filter the items if turned on using <see cref="IsFiltering" />. |
||||
/// </summary> |
||||
public void SelectItem(string text) |
||||
{ |
||||
if (text == _currentText) |
||||
return; |
||||
if (_listBox == null) |
||||
ApplyTemplate(); |
||||
|
||||
if (IsFiltering) |
||||
{ |
||||
SelectItemFiltering(text); |
||||
} |
||||
else |
||||
{ |
||||
SelectItemWithStart(text); |
||||
} |
||||
_currentText = text; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Filters CompletionList items to show only those matching given query, and selects the best match. |
||||
/// </summary> |
||||
private void SelectItemFiltering(string query) |
||||
{ |
||||
// if the user just typed one more character, don't filter all data but just filter what we are already displaying |
||||
var listToFilter = |
||||
_currentList != null |
||||
&& !string.IsNullOrEmpty(_currentText) |
||||
&& !string.IsNullOrEmpty(query) |
||||
&& query.StartsWith(_currentText, StringComparison.Ordinal) |
||||
? _currentList |
||||
: _completionData; |
||||
|
||||
var matchingItems = |
||||
from item in listToFilter |
||||
let quality = GetMatchQuality(item.Text, query) |
||||
where quality > 0 |
||||
select new { Item = item, Quality = quality }; |
||||
|
||||
// e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)" |
||||
var suggestedItem = |
||||
_listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem : null; |
||||
|
||||
var listBoxItems = new ObservableCollection<ICompletionData>(); |
||||
var bestIndex = -1; |
||||
var bestQuality = -1; |
||||
double bestPriority = 0; |
||||
var i = 0; |
||||
foreach (var matchingItem in matchingItems) |
||||
{ |
||||
var priority = |
||||
matchingItem.Item == suggestedItem |
||||
? double.PositiveInfinity |
||||
: matchingItem.Item.Priority; |
||||
var quality = matchingItem.Quality; |
||||
if (quality > bestQuality || quality == bestQuality && priority > bestPriority) |
||||
{ |
||||
bestIndex = i; |
||||
bestPriority = priority; |
||||
bestQuality = quality; |
||||
} |
||||
// Add to listbox |
||||
listBoxItems.Add(matchingItem.Item); |
||||
// Update the character highlighting |
||||
matchingItem.Item.UpdateCharHighlighting(query); |
||||
i++; |
||||
} |
||||
|
||||
_currentList = listBoxItems; |
||||
//_listBox.Items = null; Makes no sense? Tooltip disappeared because of this |
||||
_listBox.ItemsSource = listBoxItems; |
||||
SelectIndexCentered(bestIndex); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Selects the item that starts with the specified query. |
||||
/// </summary> |
||||
private void SelectItemWithStart(string query) |
||||
{ |
||||
if (string.IsNullOrEmpty(query)) |
||||
return; |
||||
|
||||
var suggestedIndex = _listBox.SelectedIndex; |
||||
|
||||
var bestIndex = -1; |
||||
var bestQuality = -1; |
||||
double bestPriority = 0; |
||||
for (var i = 0; i < _completionData.Count; ++i) |
||||
{ |
||||
var quality = GetMatchQuality(_completionData[i].Text, query); |
||||
if (quality < 0) |
||||
continue; |
||||
|
||||
var priority = _completionData[i].Priority; |
||||
bool useThisItem; |
||||
if (bestQuality < quality) |
||||
{ |
||||
useThisItem = true; |
||||
} |
||||
else |
||||
{ |
||||
if (bestIndex == suggestedIndex) |
||||
{ |
||||
useThisItem = false; |
||||
} |
||||
else if (i == suggestedIndex) |
||||
{ |
||||
// prefer recommendedItem, regardless of its priority |
||||
useThisItem = bestQuality == quality; |
||||
} |
||||
else |
||||
{ |
||||
useThisItem = bestQuality == quality && bestPriority < priority; |
||||
} |
||||
} |
||||
if (useThisItem) |
||||
{ |
||||
bestIndex = i; |
||||
bestPriority = priority; |
||||
bestQuality = quality; |
||||
} |
||||
} |
||||
SelectIndexCentered(bestIndex); |
||||
} |
||||
|
||||
private void SelectIndexCentered(int bestIndex) |
||||
{ |
||||
if (bestIndex < 0) |
||||
{ |
||||
_listBox.ClearSelection(); |
||||
} |
||||
else |
||||
{ |
||||
var firstItem = _listBox.FirstVisibleItem; |
||||
if (bestIndex < firstItem || firstItem + _listBox.VisibleItemCount <= bestIndex) |
||||
{ |
||||
// CenterViewOn does nothing as CompletionListBox.ScrollViewer is null |
||||
_listBox.CenterViewOn(bestIndex); |
||||
_listBox.SelectIndex(bestIndex); |
||||
} |
||||
else |
||||
{ |
||||
_listBox.SelectIndex(bestIndex); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private int GetMatchQuality(string itemText, string query) |
||||
{ |
||||
if (itemText == null) |
||||
throw new ArgumentNullException(nameof(itemText), "ICompletionData.Text returned null"); |
||||
|
||||
// Qualities: |
||||
// 8 = full match case sensitive |
||||
// 7 = full match |
||||
// 6 = match start case sensitive |
||||
// 5 = match start |
||||
// 4 = match CamelCase when length of query is 1 or 2 characters |
||||
// 3 = match substring case sensitive |
||||
// 2 = match substring |
||||
// 1 = match CamelCase |
||||
// -1 = no match |
||||
if (query == itemText) |
||||
return 8; |
||||
if (string.Equals(itemText, query, StringComparison.CurrentCultureIgnoreCase)) |
||||
return 7; |
||||
|
||||
if (itemText.StartsWith(query, StringComparison.CurrentCulture)) |
||||
return 6; |
||||
if (itemText.StartsWith(query, StringComparison.CurrentCultureIgnoreCase)) |
||||
return 5; |
||||
|
||||
bool? camelCaseMatch = null; |
||||
if (query.Length <= 2) |
||||
{ |
||||
camelCaseMatch = CamelCaseMatch(itemText, query); |
||||
if (camelCaseMatch == true) |
||||
return 4; |
||||
} |
||||
|
||||
// search by substring, if filtering (i.e. new behavior) turned on |
||||
if (IsFiltering) |
||||
{ |
||||
if (itemText.IndexOf(query, StringComparison.CurrentCulture) >= 0) |
||||
return 3; |
||||
if (itemText.IndexOf(query, StringComparison.CurrentCultureIgnoreCase) >= 0) |
||||
return 2; |
||||
} |
||||
|
||||
if (!camelCaseMatch.HasValue) |
||||
camelCaseMatch = CamelCaseMatch(itemText, query); |
||||
if (camelCaseMatch == true) |
||||
return 1; |
||||
|
||||
return -1; |
||||
} |
||||
|
||||
private static bool CamelCaseMatch(string text, string query) |
||||
{ |
||||
// We take the first letter of the text regardless of whether or not it's upper case so we match |
||||
// against camelCase text as well as PascalCase text ("cct" matches "camelCaseText") |
||||
var theFirstLetterOfEachWord = text.AsEnumerable() |
||||
.Take(1) |
||||
.Concat(text.AsEnumerable().Skip(1).Where(char.IsUpper)); |
||||
|
||||
var i = 0; |
||||
foreach (var letter in theFirstLetterOfEachWord) |
||||
{ |
||||
if (i > query.Length - 1) |
||||
return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis") |
||||
if (char.ToUpperInvariant(query[i]) != char.ToUpperInvariant(letter)) |
||||
return false; |
||||
i++; |
||||
} |
||||
if (i >= query.Length) |
||||
return true; |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,109 @@
|
||||
// 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 Avalonia.Controls; |
||||
using Avalonia.Controls.Primitives; |
||||
using AvaloniaEdit.Utils; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
|
||||
/// <summary> |
||||
/// The list box used inside the CompletionList. |
||||
/// </summary> |
||||
public class CompletionListBox : ListBox |
||||
{ |
||||
internal ScrollViewer ScrollViewer; |
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
||||
{ |
||||
base.OnApplyTemplate(e); |
||||
ScrollViewer = e.NameScope.Find("PART_ScrollViewer") as ScrollViewer; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets the number of the first visible item. |
||||
/// </summary> |
||||
public int FirstVisibleItem |
||||
{ |
||||
get |
||||
{ |
||||
if (ScrollViewer == null || ScrollViewer.Extent.Height == 0) |
||||
{ |
||||
return 0; |
||||
} |
||||
|
||||
return (int)(ItemCount * ScrollViewer.Offset.Y / ScrollViewer.Extent.Height); |
||||
} |
||||
set |
||||
{ |
||||
value = value.CoerceValue(0, ItemCount - VisibleItemCount); |
||||
if (ScrollViewer != null) |
||||
{ |
||||
ScrollViewer.Offset = ScrollViewer.Offset.WithY((double)value / ItemCount * ScrollViewer.Extent.Height); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets the number of visible items. |
||||
/// </summary> |
||||
public int VisibleItemCount |
||||
{ |
||||
get |
||||
{ |
||||
if (ScrollViewer == null || ScrollViewer.Extent.Height == 0) |
||||
{ |
||||
return 10; |
||||
} |
||||
return Math.Max( |
||||
3, |
||||
(int)Math.Ceiling(ItemCount * ScrollViewer.Viewport.Height |
||||
/ ScrollViewer.Extent.Height)); |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Removes the selection. |
||||
/// </summary> |
||||
public void ClearSelection() |
||||
{ |
||||
SelectedIndex = -1; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Selects the item with the specified index and scrolls it into view. |
||||
/// </summary> |
||||
public void SelectIndex(int index) |
||||
{ |
||||
if (index >= ItemCount) |
||||
index = ItemCount - 1; |
||||
if (index < 0) |
||||
index = 0; |
||||
SelectedIndex = index; |
||||
ScrollIntoView(SelectedItem); |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Centers the view on the item with the specified index. |
||||
/// </summary> |
||||
public void CenterViewOn(int index) |
||||
{ |
||||
FirstVisibleItem = index - VisibleItemCount / 2; |
||||
} |
||||
} |
@ -0,0 +1,223 @@
|
||||
<ResourceDictionary |
||||
xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:cc="clr-namespace:StabilityMatrix.Avalonia.Controls.CodeCompletion" |
||||
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" |
||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> |
||||
|
||||
<Design.PreviewWith> |
||||
<Panel Width="400" Height="600"> |
||||
<Panel Width="350" Height="200"> |
||||
<ContentPresenter Content="{x:Static mocks:DesignData.SampleCompletionList}" /> |
||||
</Panel> |
||||
</Panel> |
||||
|
||||
</Design.PreviewWith> |
||||
|
||||
<!-- ReSharper disable once Xaml.StaticResourceNotResolved --> |
||||
<ControlTheme |
||||
x:Key="{x:Type cc:CompletionListBox}" |
||||
BasedOn="{StaticResource {x:Type ListBox}}" |
||||
TargetType="cc:CompletionListBox"> |
||||
<Setter Property="Margin" Value="0,2" /> |
||||
<Setter Property="Padding" Value="0" /> |
||||
<Setter Property="WrapSelection" Value="True" /> |
||||
<Setter Property="ItemContainerTheme"> |
||||
<!-- ReSharper disable once Xaml.StaticResourceNotResolved --> |
||||
<ControlTheme TargetType="ListBoxItem"> |
||||
<!-- |
||||
Modified from https://github.com/amwx/FluentAvalonia/blob/main/src/ |
||||
FluentAvalonia/Styling/ControlThemes/BasicControls/ListBoxStyles.axaml |
||||
--> |
||||
|
||||
<Setter Property="Padding" Value="8,0,12,0" /> |
||||
|
||||
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" /> |
||||
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" /> |
||||
<Setter Property="Background" Value="{DynamicResource ListViewItemBackground}" /> |
||||
<Setter Property="CornerRadius" Value="7" /> |
||||
<Setter Property="Foreground" Value="{DynamicResource ListViewItemForeground}" /> |
||||
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> |
||||
<Setter Property="VerticalContentAlignment" Value="Center" /> |
||||
<Setter Property="MinWidth" Value="{DynamicResource ListViewItemMinWidth}" /> |
||||
<Setter Property="Height" Value="26" /> |
||||
|
||||
<Setter Property="Template"> |
||||
<ControlTemplate> |
||||
<Panel> |
||||
<ContentPresenter |
||||
Name="PART_ContentPresenter" |
||||
Margin="2,0" |
||||
Padding="{TemplateBinding Padding}" |
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" |
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" |
||||
Background="{TemplateBinding Background}" |
||||
BorderBrush="{TemplateBinding BorderBrush}" |
||||
BorderThickness="{TemplateBinding BorderThickness}" |
||||
Content="{TemplateBinding Content}" |
||||
ContentTemplate="{TemplateBinding ContentTemplate}" |
||||
CornerRadius="{TemplateBinding CornerRadius}" /> |
||||
</Panel> |
||||
</ControlTemplate> |
||||
</Setter> |
||||
|
||||
<!--<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource ListViewItemBackgroundPointerOver}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ListViewItemForegroundPointerOver}" /> |
||||
</Style> |
||||
</Style>--> |
||||
|
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource ListViewItemBackgroundPressed}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ListViewItemForegroundPressed}" /> |
||||
</Style> |
||||
|
||||
|
||||
<Style Selector="^:selected"> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource CompletionSelectionBackgroundBrush}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ListViewItemForegroundSelected}" /> |
||||
</Style> |
||||
<!-- Disable selection indicator from fluent theme --> |
||||
<!--<Style Selector="^ /template/ Rectangle#SelectionIndicator"> |
||||
<Setter Property="IsVisible" Value="False" /> |
||||
</Style>--> |
||||
<!--<Style Selector="^:not(:focus) /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource ListViewItemBackgroundSelected}" /> |
||||
</Style> |
||||
<Style Selector="^:not(:focus) /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ListViewItemForegroundSelected}" /> |
||||
</Style>--> |
||||
|
||||
<!--<Style Selector="^:pointerover"> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource ListViewItemBackgroundSelectedPointerOver}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ListViewItemForegroundSelectedPointerOver}" /> |
||||
</Style> |
||||
</Style>--> |
||||
|
||||
<Style Selector="^:pressed"> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource ListViewItemBackgroundSelectedPressed}" /> |
||||
</Style> |
||||
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter"> |
||||
<Setter Property="Foreground" Value="{DynamicResource ListViewItemForegroundSelectedPressed}" /> |
||||
</Style> |
||||
</Style> |
||||
|
||||
<!--<Style Selector="^:disabled /template/ Rectangle#SelectionIndicator"> |
||||
<Setter Property="Fill" Value="{DynamicResource ListViewItemSelectionIndicatorDisabledBrush}" /> |
||||
</Style>--> |
||||
</Style> |
||||
|
||||
</ControlTheme> |
||||
</Setter> |
||||
</ControlTheme> |
||||
|
||||
<ControlTheme x:Key="{x:Type cc:CompletionList}" TargetType="cc:CompletionList"> |
||||
|
||||
<Setter Property="Background" Value="{DynamicResource CompletionBackgroundBrush}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource CompletionBorderBrush}" /> |
||||
<Setter Property="BorderThickness" Value="1.5" /> |
||||
<Setter Property="Padding" Value="0" /> |
||||
<Setter Property="MinWidth" Value="{DynamicResource FlyoutThemeMinWidth}" /> |
||||
<Setter Property="MaxWidth" Value="{DynamicResource FlyoutThemeMaxWidth}" /> |
||||
<Setter Property="MinHeight" Value="{DynamicResource FlyoutThemeMinHeight}" /> |
||||
<Setter Property="MaxHeight" Value="{DynamicResource FlyoutThemeMaxHeight}" /> |
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" /> |
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" /> |
||||
<Setter Property="CornerRadius" Value="6" /> |
||||
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" /> |
||||
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" /> |
||||
|
||||
<Setter Property="Template"> |
||||
<ControlTemplate> |
||||
<!-- BoxShadow="0 0 10 2 #BF000000" --> |
||||
<ui:FABorder |
||||
Padding="{DynamicResource FlyoutBorderThemePadding}" |
||||
Background="{TemplateBinding Background}" |
||||
BorderBrush="{TemplateBinding BorderBrush}" |
||||
BorderThickness="{TemplateBinding BorderThickness}" |
||||
CornerRadius="{TemplateBinding CornerRadius}"> |
||||
|
||||
<Grid RowDefinitions="*,Auto"> |
||||
<!-- ReSharper disable once InconsistentNaming --> |
||||
<cc:CompletionListBox Name="PART_ListBox"> |
||||
<cc:CompletionListBox.ItemTemplate> |
||||
<DataTemplate x:DataType="cc:ICompletionData"> |
||||
<Grid ColumnDefinitions="20,Auto,*"> |
||||
<Image |
||||
Width="16" |
||||
Height="16" |
||||
Margin="0,0,2,0" |
||||
IsVisible="{Binding Image, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
Source="{Binding Image}" /> |
||||
|
||||
<!-- Icon --> |
||||
<Panel Grid.Column="0" |
||||
IsVisible="{Binding Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
DataContext="{Binding Icon}"> |
||||
<icons:Icon |
||||
Margin="0,0,4,0" |
||||
FontSize="{Binding FontSize, TargetNullValue=12}" |
||||
Foreground="{Binding Foreground, TargetNullValue=Red, FallbackValue=Red}" |
||||
IsVisible="{Binding FAIcon, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
Value="{Binding FAIcon}" /> |
||||
</Panel> |
||||
|
||||
<!-- Main text --> |
||||
<TextBlock |
||||
Grid.Column="1" |
||||
VerticalAlignment="Center" |
||||
FontFamily="Cascadia Code,Consolas,Menlo,Monospace" |
||||
FontSize="13" |
||||
Inlines="{Binding TextInlines}" /> |
||||
|
||||
<!-- Description Text --> |
||||
<TextBlock |
||||
Grid.Column="2" |
||||
VerticalAlignment="Center" |
||||
FontFamily="Cascadia Code,Consolas,Menlo,Monospace" |
||||
FontSize="13" |
||||
Foreground="{DynamicResource CompletionSecondaryForegroundBrush}" |
||||
Text="{Binding Description}" |
||||
TextAlignment="Right" /> |
||||
<!--<ContentPresenter Content="{Binding Content}" />--> |
||||
</Grid> |
||||
</DataTemplate> |
||||
</cc:CompletionListBox.ItemTemplate> |
||||
</cc:CompletionListBox> |
||||
|
||||
<!-- Keybinding hints panel --> |
||||
<Panel |
||||
Grid.Row="1" |
||||
MinHeight="20" |
||||
Background="{DynamicResource CompletionSecondaryBackgroundBrush}"> |
||||
<TextBlock |
||||
Margin="10,3" |
||||
FontSize="12" |
||||
Foreground="{DynamicResource CompletionSecondaryForegroundBrush}" |
||||
Text="{TemplateBinding FooterText}" /> |
||||
</Panel> |
||||
</Grid> |
||||
|
||||
|
||||
</ui:FABorder> |
||||
|
||||
</ControlTemplate> |
||||
</Setter> |
||||
</ControlTheme> |
||||
|
||||
</ResourceDictionary> |
@ -0,0 +1,55 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:controls="using:StabilityMatrix.Avalonia.Controls.CodeCompletion" |
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> |
||||
|
||||
<!-- Style used for tooltip next to completion list box. --> |
||||
<!--<Style Selector="ContentControl.ToolTip"> |
||||
<Setter Property="BorderThickness" Value="{DynamicResource CompletionToolTipBorderThickness}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource CompletionToolTipBorderBrush}" /> |
||||
<Setter Property="Background" Value="{DynamicResource CompletionToolTipBackground}" /> |
||||
<Setter Property="Foreground" Value="{DynamicResource CompletionToolTipForeground}" /> |
||||
<Setter Property="Padding" Value="4,2" /> |
||||
</Style>--> |
||||
|
||||
|
||||
<!--<Style Selector="controls|CompletionWindow"> |
||||
~1~<Style Selector="^ /template/ ContentControl.ToolTip"> |
||||
<Setter Property="BorderThickness" Value="{DynamicResource CompletionToolTipBorderThickness}" /> |
||||
<Setter Property="BorderBrush" Value="{DynamicResource CompletionToolTipBorderBrush}" /> |
||||
<Setter Property="Background" Value="{DynamicResource CompletionToolTipBackground}" /> |
||||
<Setter Property="Foreground" Value="{DynamicResource CompletionToolTipForeground}" /> |
||||
<Setter Property="Padding" Value="4,2" /> |
||||
|
||||
</Style>@1@ |
||||
~1~<Setter Property="Template"> |
||||
<ControlTemplate> |
||||
<TextBlock Text="Templated Control" /> |
||||
</ControlTemplate> |
||||
</Setter>@1@ |
||||
</Style>--> |
||||
|
||||
<!--<Style Selector="controls|CompletionWindow"> |
||||
<Setter Property="TransparencyLevelHint" Value="Transparent" /> |
||||
<Setter Property="Background" Value="{x:Null}" /> |
||||
<Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" /> |
||||
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" /> |
||||
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" /> |
||||
<Setter Property="Template"> |
||||
<ControlTemplate> |
||||
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}"> |
||||
<Panel> |
||||
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" /> |
||||
<VisualLayerManager IsPopup="True"> |
||||
<ContentPresenter Name="PART_ContentPresenter" |
||||
Background="{TemplateBinding Background}" |
||||
ContentTemplate="{TemplateBinding ContentTemplate}" |
||||
Content="{TemplateBinding Content}" |
||||
Padding="{TemplateBinding Padding}" /> |
||||
</VisualLayerManager> |
||||
</Panel> |
||||
</LayoutTransformControl> |
||||
</ControlTemplate> |
||||
</Setter> |
||||
</Style>--> |
||||
</Styles> |
@ -0,0 +1,258 @@
|
||||
// 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 Avalonia; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Primitives; |
||||
using Avalonia.Input; |
||||
using Avalonia.Media; |
||||
using AvaloniaEdit.Document; |
||||
using AvaloniaEdit.Editing; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
|
||||
/// <summary> |
||||
/// The code completion window. |
||||
/// </summary> |
||||
public class CompletionWindow : CompletionWindowBase |
||||
{ |
||||
private PopupWithCustomPosition _toolTip; |
||||
private ContentControl _toolTipContent; |
||||
|
||||
|
||||
/// <summary> |
||||
/// Gets the completion list used in this completion window. |
||||
/// </summary> |
||||
public CompletionList CompletionList { get; } |
||||
|
||||
/// <summary> |
||||
/// Creates a new code completion window. |
||||
/// </summary> |
||||
public CompletionWindow(TextArea textArea) : base(textArea) |
||||
{ |
||||
CompletionList = new CompletionList(); |
||||
// keep height automatic |
||||
CloseAutomatically = true; |
||||
MaxHeight = 225; |
||||
// Width = 175; |
||||
Width = 350; |
||||
Child = CompletionList; |
||||
// prevent user from resizing window to 0x0 |
||||
MinHeight = 15; |
||||
MinWidth = 30; |
||||
|
||||
_toolTipContent = new ContentControl(); |
||||
_toolTipContent.Classes.Add("ToolTip"); |
||||
|
||||
_toolTip = new PopupWithCustomPosition |
||||
{ |
||||
IsLightDismissEnabled = true, |
||||
PlacementTarget = this, |
||||
// Placement = PlacementMode.RightEdgeAlignedTop, |
||||
Placement = PlacementMode.LeftEdgeAlignedBottom, |
||||
Child = _toolTipContent, |
||||
}; |
||||
|
||||
LogicalChildren.Add(_toolTip); |
||||
|
||||
//_toolTip.Closed += (o, e) => ((Popup)o).Child = null; |
||||
|
||||
AttachEvents(); |
||||
} |
||||
|
||||
protected override void OnClosed() |
||||
{ |
||||
base.OnClosed(); |
||||
|
||||
if (_toolTip != null) |
||||
{ |
||||
_toolTip.IsOpen = false; |
||||
_toolTip = null; |
||||
_toolTipContent = null; |
||||
} |
||||
} |
||||
|
||||
#region ToolTip handling |
||||
|
||||
private void CompletionList_SelectionChanged(object sender, SelectionChangedEventArgs e) |
||||
{ |
||||
if (_toolTipContent == null) return; |
||||
|
||||
var item = CompletionList.SelectedItem; |
||||
var description = item?.Description; |
||||
if (description != null) |
||||
{ |
||||
if (description is string descriptionText) |
||||
{ |
||||
_toolTipContent.Content = new TextBlock |
||||
{ |
||||
Text = descriptionText, |
||||
TextWrapping = TextWrapping.Wrap |
||||
}; |
||||
} |
||||
else |
||||
{ |
||||
_toolTipContent.Content = description; |
||||
} |
||||
|
||||
_toolTip.IsOpen = false; //Popup needs to be closed to change position |
||||
|
||||
// Calculate offset for tooltip |
||||
var popupRoot = Host as PopupRoot; |
||||
if (CompletionList.CurrentList != null) |
||||
{ |
||||
double yOffset = 0; |
||||
var itemContainer = CompletionList.ListBox.ContainerFromItem(item); |
||||
if (popupRoot != null && itemContainer != null) |
||||
{ |
||||
var position = itemContainer.TranslatePoint(new Point(0, 0), popupRoot); |
||||
if (position.HasValue) |
||||
yOffset = position.Value.Y; |
||||
} |
||||
|
||||
_toolTip.Offset = new Point(2, yOffset); |
||||
} |
||||
|
||||
_toolTip.PlacementTarget = popupRoot; |
||||
_toolTip.IsOpen = true; |
||||
} |
||||
else |
||||
{ |
||||
_toolTip.IsOpen = false; |
||||
} |
||||
} |
||||
|
||||
#endregion |
||||
|
||||
private void CompletionList_InsertionRequested(object sender, EventArgs e) |
||||
{ |
||||
Hide(); |
||||
// The window must close before Complete() is called. |
||||
// If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes. |
||||
var item = CompletionList.SelectedItem; |
||||
item?.Complete(TextArea, new AnchorSegment(TextArea.Document, StartOffset, EndOffset - StartOffset), e); |
||||
} |
||||
|
||||
private void AttachEvents() |
||||
{ |
||||
CompletionList.InsertionRequested += CompletionList_InsertionRequested; |
||||
CompletionList.SelectionChanged += CompletionList_SelectionChanged; |
||||
TextArea.Caret.PositionChanged += CaretPositionChanged; |
||||
TextArea.PointerWheelChanged += TextArea_MouseWheel; |
||||
TextArea.TextInput += TextArea_PreviewTextInput; |
||||
} |
||||
|
||||
/// <inheritdoc/> |
||||
protected override void DetachEvents() |
||||
{ |
||||
CompletionList.InsertionRequested -= CompletionList_InsertionRequested; |
||||
CompletionList.SelectionChanged -= CompletionList_SelectionChanged; |
||||
TextArea.Caret.PositionChanged -= CaretPositionChanged; |
||||
TextArea.PointerWheelChanged -= TextArea_MouseWheel; |
||||
TextArea.TextInput -= TextArea_PreviewTextInput; |
||||
base.DetachEvents(); |
||||
} |
||||
|
||||
/// <inheritdoc/> |
||||
protected override void OnKeyDown(KeyEventArgs e) |
||||
{ |
||||
base.OnKeyDown(e); |
||||
if (!e.Handled) |
||||
{ |
||||
CompletionList.HandleKey(e); |
||||
} |
||||
} |
||||
|
||||
private void TextArea_PreviewTextInput(object sender, TextInputEventArgs e) |
||||
{ |
||||
e.Handled = RaiseEventPair(this, null, TextInputEvent, |
||||
new TextInputEventArgs { Text = e.Text }); |
||||
} |
||||
|
||||
private void TextArea_MouseWheel(object sender, PointerWheelEventArgs e) |
||||
{ |
||||
e.Handled = RaiseEventPair(GetScrollEventTarget(), |
||||
null, PointerWheelChangedEvent, e); |
||||
} |
||||
|
||||
private Control GetScrollEventTarget() |
||||
{ |
||||
if (CompletionList == null) |
||||
return this; |
||||
return CompletionList.ScrollViewer ?? CompletionList.ListBox ?? (Control)CompletionList; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Gets/Sets whether the completion window should close automatically. |
||||
/// The default value is true. |
||||
/// </summary> |
||||
public bool CloseAutomatically { get; set; } |
||||
|
||||
/// <inheritdoc/> |
||||
protected override bool CloseOnFocusLost => CloseAutomatically; |
||||
|
||||
/// <summary> |
||||
/// When this flag is set, code completion closes if the caret moves to the |
||||
/// beginning of the allowed range. This is useful in Ctrl+Space and "complete when typing", |
||||
/// but not in dot-completion. |
||||
/// Has no effect if CloseAutomatically is false. |
||||
/// </summary> |
||||
public bool CloseWhenCaretAtBeginning { get; set; } |
||||
|
||||
private void CaretPositionChanged(object sender, EventArgs e) |
||||
{ |
||||
Debug.WriteLine($"Caret Position changed: {e}"); |
||||
var offset = TextArea.Caret.Offset; |
||||
if (offset == StartOffset) |
||||
{ |
||||
if (CloseAutomatically && CloseWhenCaretAtBeginning) |
||||
{ |
||||
Hide(); |
||||
} |
||||
else |
||||
{ |
||||
CompletionList.SelectItem(string.Empty); |
||||
|
||||
if (CompletionList.ListBox.ItemCount == 0) IsVisible = false; |
||||
else IsVisible = true; |
||||
} |
||||
return; |
||||
} |
||||
if (offset < StartOffset || offset > EndOffset) |
||||
{ |
||||
if (CloseAutomatically) |
||||
{ |
||||
Hide(); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
var document = TextArea.Document; |
||||
if (document != null) |
||||
{ |
||||
var newText = document.GetText(StartOffset, offset - StartOffset); |
||||
Debug.WriteLine("CaretPositionChanged newText: " + newText); |
||||
CompletionList.SelectItem(newText); |
||||
|
||||
IsVisible = CompletionList.ListBox.ItemCount != 0; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,421 @@
|
||||
// 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> |
||||
public class CompletionWindowBase : Popup |
||||
{ |
||||
static CompletionWindowBase() |
||||
{ |
||||
//BackgroundProperty.OverrideDefaultValue(typeof(CompletionWindowBase), Brushes.White); |
||||
} |
||||
|
||||
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) : base() |
||||
{ |
||||
TextArea = textArea ?? throw new ArgumentNullException(nameof(textArea)); |
||||
_parentWindow = textArea.GetVisualRoot() as Window; |
||||
|
||||
|
||||
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 += (sender, args) => 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(); |
||||
} |
||||
|
||||
/// <inheritdoc/> |
||||
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> |
||||
[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")] |
||||
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 |
||||
///// <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; } |
||||
|
||||
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); |
||||
} |
||||
} |
@ -0,0 +1,95 @@
|
||||
// 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 Avalonia.Controls.Documents; |
||||
using Avalonia.Media; |
||||
using AvaloniaEdit.Document; |
||||
using AvaloniaEdit.Editing; |
||||
using FluentAvalonia.UI.Controls; |
||||
using StabilityMatrix.Avalonia.Models; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
|
||||
/// <summary> |
||||
/// Describes an entry in the <see cref="AvaloniaEdit.CodeCompletion.CompletionList"/>. |
||||
/// </summary> |
||||
/// <remarks> |
||||
/// Note that the CompletionList uses data binding against the properties in this interface. |
||||
/// Thus, your implementation of the interface must use public properties; not explicit interface implementation. |
||||
/// </remarks> |
||||
public interface ICompletionData |
||||
{ |
||||
/// <summary> |
||||
/// Gets the text. This property is used to filter the list of visible elements. |
||||
/// </summary> |
||||
string Text { get; } |
||||
|
||||
/// <summary> |
||||
/// Gets the description. |
||||
/// </summary> |
||||
public string? Description { get; } |
||||
|
||||
/// <summary> |
||||
/// Gets the image. |
||||
/// </summary> |
||||
IImage? Image { get; } |
||||
|
||||
/// <summary> |
||||
/// Gets the icon shown on the left. |
||||
/// </summary> |
||||
IconData? Icon { get; } |
||||
|
||||
/// <summary> |
||||
/// The displayed content. This can be the same as 'Text', or a control if |
||||
/// you want to display rich content. |
||||
/// </summary> |
||||
object Content { get; } |
||||
|
||||
/// <summary> |
||||
/// Gets inline text fragments. |
||||
/// </summary> |
||||
InlineCollection TextInlines { get; } |
||||
|
||||
/// <summary> |
||||
/// Gets the priority. This property is used in the selection logic. You can use it to prefer selecting those items |
||||
/// which the user is accessing most frequently. |
||||
/// </summary> |
||||
double Priority { get; } |
||||
|
||||
/// <summary> |
||||
/// Perform the completion. |
||||
/// </summary> |
||||
/// <param name="textArea">The text area on which completion is performed.</param> |
||||
/// <param name="completionSegment">The text segment that was used by the completion window if |
||||
/// the user types (segment between CompletionWindow.StartOffset and CompletionWindow.EndOffset).</param> |
||||
/// <param name="insertionRequestEventArgs">The EventArgs used for the insertion request. |
||||
/// These can be TextCompositionEventArgs, KeyEventArgs, MouseEventArgs, depending on how |
||||
/// the insertion was triggered.</param> |
||||
void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs); |
||||
|
||||
/// <summary> |
||||
/// Update the text character highlighting |
||||
/// </summary> |
||||
void UpdateCharHighlighting(string searchText); |
||||
|
||||
/// <summary> |
||||
/// Reset the text character highlighting |
||||
/// </summary> |
||||
void ResetCharHighlighting(); |
||||
} |
@ -0,0 +1,19 @@
|
||||
using Avalonia; |
||||
using Avalonia.Controls.Primitives; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
|
||||
internal class PopupWithCustomPosition : Popup |
||||
{ |
||||
public Point Offset |
||||
{ |
||||
get => new(HorizontalOffset, VerticalOffset); |
||||
set |
||||
{ |
||||
HorizontalOffset = value.X; |
||||
VerticalOffset = value.Y; |
||||
|
||||
// this.Revalidate(VerticalOffsetProperty); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,12 @@
|
||||
using Avalonia.Media; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models; |
||||
|
||||
public record IconData |
||||
{ |
||||
public string? FAIcon { get; init; } |
||||
|
||||
public int? FontSize { get; init; } |
||||
|
||||
public SolidColorBrush? Foreground { get; init; } |
||||
} |
@ -0,0 +1,17 @@
|
||||
using StabilityMatrix.Avalonia.Controls.CodeCompletion; |
||||
using StabilityMatrix.Core.Extensions; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models.TagCompletion; |
||||
|
||||
public class TagCompletionData : CompletionData |
||||
{ |
||||
protected TagType TagType { get; } |
||||
|
||||
/// <inheritdoc /> |
||||
public TagCompletionData(string text, TagType tagType) : base(text) |
||||
{ |
||||
TagType = tagType; |
||||
Icon = CompletionIcons.GetIconForTagType(tagType) ?? CompletionIcons.Invalid; |
||||
Description = tagType.GetStringValue(); |
||||
} |
||||
} |
@ -0,0 +1,38 @@
|
||||
using System.Text.Json.Serialization; |
||||
using StabilityMatrix.Core.Converters.Json; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Models.TagCompletion; |
||||
|
||||
[JsonConverter(typeof(DefaultUnknownEnumConverter<TagType>))] |
||||
public enum TagType |
||||
{ |
||||
Unknown, |
||||
Invalid, |
||||
General, |
||||
Artist, |
||||
Copyright, |
||||
Character, |
||||
Species, |
||||
Meta, |
||||
Lore |
||||
} |
||||
|
||||
public static class TagTypeExtensions |
||||
{ |
||||
public static TagType FromE621(int tag) |
||||
{ |
||||
return tag switch |
||||
{ |
||||
-1 => TagType.Invalid, |
||||
0 => TagType.General, |
||||
1 => TagType.Artist, |
||||
3 => TagType.Copyright, |
||||
4 => TagType.Character, |
||||
5 => TagType.Species, |
||||
6 => TagType.Invalid, |
||||
7 => TagType.Meta, |
||||
8 => TagType.Lore, |
||||
_ => TagType.Unknown |
||||
}; |
||||
} |
||||
} |
Loading…
Reference in new issue