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