Browse Source

Add Code completion namespaces

pull/165/head
Ionite 1 year ago
parent
commit
f9abadfd1b
No known key found for this signature in database
  1. 162
      StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs
  2. 128
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs
  3. 69
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs
  4. 480
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs
  5. 109
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs
  6. 223
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml
  7. 55
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml
  8. 258
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs
  9. 421
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs
  10. 95
      StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs
  11. 19
      StabilityMatrix.Avalonia/Controls/CodeCompletion/PopupWithCustomPosition.cs
  12. 18
      StabilityMatrix.Avalonia/Controls/PromptCard.axaml
  13. 23
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  14. 12
      StabilityMatrix.Avalonia/Models/IconData.cs
  15. 17
      StabilityMatrix.Avalonia/Models/TagCompletion/TagCompletionData.cs
  16. 38
      StabilityMatrix.Avalonia/Models/TagCompletion/TagType.cs
  17. 39
      StabilityMatrix.Avalonia/Styles/ThemeColors.axaml
  18. 16
      StabilityMatrix.Avalonia/Styles/ThemeColors.cs
  19. 7
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs

162
StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs

@ -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;
}
}

128
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs

@ -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;
}
}
}

69
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs

@ -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
};
}
}

480
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs

@ -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;
}
}

109
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs

@ -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;
}
}

223
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml

@ -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>

55
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml

@ -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>

258
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs

@ -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;
}
}
}
}

421
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs

@ -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);
}
}

95
StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs

@ -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();
}

19
StabilityMatrix.Avalonia/Controls/CodeCompletion/PopupWithCustomPosition.cs

@ -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);
}
}
}

18
StabilityMatrix.Avalonia/Controls/PromptCard.axaml

@ -4,6 +4,8 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
xmlns:behaviors="clr-namespace:StabilityMatrix.Avalonia.Behaviors"
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
x:DataType="vmInference:PromptCardViewModel"> x:DataType="vmInference:PromptCardViewModel">
<Design.PreviewWith> <Design.PreviewWith>
@ -64,8 +66,12 @@
<avaloniaEdit:TextEditor <avaloniaEdit:TextEditor
x:Name="PromptEditor" x:Name="PromptEditor"
Document="{Binding PromptDocument}" Document="{Binding PromptDocument}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"/> FontFamily="Cascadia Code,Consolas,Menlo,Monospace">
<i:Interaction.Behaviors>
<behaviors:TextEditorCompletionBehavior/>
</i:Interaction.Behaviors>
</avaloniaEdit:TextEditor>
</ExperimentalAcrylicBorder> </ExperimentalAcrylicBorder>
</Grid> </Grid>
@ -93,8 +99,12 @@
<avaloniaEdit:TextEditor <avaloniaEdit:TextEditor
x:Name="NegativePromptEditor" x:Name="NegativePromptEditor"
Document="{Binding NegativePromptDocument}" Document="{Binding NegativePromptDocument}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"/> FontFamily="Cascadia Code,Consolas,Menlo,Monospace">
<i:Interaction.Behaviors>
<behaviors:TextEditorCompletionBehavior/>
</i:Interaction.Behaviors>
</avaloniaEdit:TextEditor>
</ExperimentalAcrylicBorder> </ExperimentalAcrylicBorder>
</Grid> </Grid>
</Grid> </Grid>

23
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -5,8 +5,11 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using AvaloniaEdit.Utils;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -430,6 +433,26 @@ public static class DesignData
public static BatchSizeCardViewModel BatchSizeCardViewModel => public static BatchSizeCardViewModel BatchSizeCardViewModel =>
DialogFactory.Get<BatchSizeCardViewModel>(); DialogFactory.Get<BatchSizeCardViewModel>();
public static IList<ICompletionData> SampleCompletionData => new List<ICompletionData>
{
new TagCompletionData("test1", TagType.General),
new TagCompletionData("test2", TagType.Artist),
new TagCompletionData("test3", TagType.Character),
new TagCompletionData("test4", TagType.Copyright),
new TagCompletionData("test5", TagType.Species),
new TagCompletionData("test_unknown", TagType.Invalid),
};
public static CompletionList SampleCompletionList
{
get
{
var list = new CompletionList();
list.CompletionData.AddRange(SampleCompletionData);
return list;
}
}
public static Indexer Types => new(); public static Indexer Types => new();
public class Indexer public class Indexer

12
StabilityMatrix.Avalonia/Models/IconData.cs

@ -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; }
}

17
StabilityMatrix.Avalonia/Models/TagCompletion/TagCompletionData.cs

@ -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();
}
}

38
StabilityMatrix.Avalonia/Models/TagCompletion/TagType.cs

@ -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
};
}
}

39
StabilityMatrix.Avalonia/Styles/ThemeColors.axaml

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<!-- Add Resources Here --> xmlns:sty="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia">
<!-- Static colors -->
<Color x:Key="ThemePrimaryColor">#333333</Color> <Color x:Key="ThemePrimaryColor">#333333</Color>
<Color x:Key="ThemeDarkDarkRedColor">#952923</Color> <Color x:Key="ThemeDarkDarkRedColor">#952923</Color>
<Color x:Key="ThemeDarkRedColor">#C2362E</Color> <Color x:Key="ThemeDarkRedColor">#C2362E</Color>
@ -30,4 +31,38 @@
<Color x:Key="ThemeBrownColor">#795548</Color> <Color x:Key="ThemeBrownColor">#795548</Color>
<Color x:Key="ThemeGreyColor">#9E9E9E</Color> <Color x:Key="ThemeGreyColor">#9E9E9E</Color>
<Color x:Key="ThemeBlueGreyColor">#607D8B</Color> <Color x:Key="ThemeBlueGreyColor">#607D8B</Color>
<!-- Dark / light dynamic colors -->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush x:Key="CompletionBorderBrush" Color="#43454A"/>
<SolidColorBrush x:Key="CompletionBackgroundBrush" Color="#2B2D30"/>
<SolidColorBrush x:Key="CompletionSecondaryBackgroundBrush" Color="#393B40"/>
<SolidColorBrush x:Key="CompletionForegroundBrush" Color="#B4B8BF"/>
<SolidColorBrush x:Key="CompletionSecondaryForegroundBrush" Color="#878B8D"/>
<SolidColorBrush x:Key="CompletionSelectionBackgroundBrush" Color="#2E436E"/>
<SolidColorBrush x:Key="CompletionSelectionForegroundBrush" Color="#5389F4"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="CompletionBorderBrush" Color="#43454A"/>
<SolidColorBrush x:Key="CompletionBackgroundBrush" Color="#2B2D30"/>
<SolidColorBrush x:Key="CompletionSecondaryBackgroundBrush" Color="#393B40"/>
<SolidColorBrush x:Key="CompletionForegroundBrush" Color="#B4B8BF"/>
<SolidColorBrush x:Key="CompletionSecondaryForegroundBrush" Color="#878B8D"/>
<SolidColorBrush x:Key="CompletionSelectionBackgroundBrush" Color="#2E436E"/>
<SolidColorBrush x:Key="CompletionSelectionForegroundBrush" Color="#5389F4"/>
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static sty:FluentAvaloniaTheme.HighContrastTheme}">
<SolidColorBrush x:Key="CompletionBorderBrush" Color="#43454A"/>
<SolidColorBrush x:Key="CompletionBackgroundBrush" Color="#2B2D30"/>
<SolidColorBrush x:Key="CompletionSecondaryBackgroundBrush" Color="#393B40"/>
<SolidColorBrush x:Key="CompletionForegroundBrush" Color="#B4B8BF"/>
<SolidColorBrush x:Key="CompletionSecondaryForegroundBrush" Color="#878B8D"/>
<SolidColorBrush x:Key="CompletionSelectionBackgroundBrush" Color="#2E436E"/>
<SolidColorBrush x:Key="CompletionSelectionForegroundBrush" Color="#5389F4"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary> </ResourceDictionary>

16
StabilityMatrix.Avalonia/Styles/ThemeColors.cs

@ -1,4 +1,5 @@
using Avalonia.Media; using Avalonia;
using Avalonia.Media;
namespace StabilityMatrix.Avalonia.Styles; namespace StabilityMatrix.Avalonia.Styles;
@ -7,4 +8,17 @@ public static class ThemeColors
public static readonly SolidColorBrush ThemeGreen = SolidColorBrush.Parse("#4caf50"); public static readonly SolidColorBrush ThemeGreen = SolidColorBrush.Parse("#4caf50");
public static readonly SolidColorBrush ThemeRed = SolidColorBrush.Parse("#f44336"); public static readonly SolidColorBrush ThemeRed = SolidColorBrush.Parse("#f44336");
public static readonly SolidColorBrush ThemeYellow = SolidColorBrush.Parse("#ffeb3b"); public static readonly SolidColorBrush ThemeYellow = SolidColorBrush.Parse("#ffeb3b");
public static readonly SolidColorBrush AmericanYellow = SolidColorBrush.Parse("#f2ac08");
public static readonly SolidColorBrush HalloweenOrange = SolidColorBrush.Parse("#ed5D1f");
public static readonly SolidColorBrush LightSteelBlue = SolidColorBrush.Parse("#b4c7d9");
public static readonly SolidColorBrush DeepMagenta = SolidColorBrush.Parse("#dd00dd");
public static readonly SolidColorBrush LuminousGreen = SolidColorBrush.Parse("#00aa00");
public static readonly SolidColorBrush CompletionSelectionBackgroundBrush =
SolidColorBrush.Parse("#2E436E");
public static readonly SolidColorBrush CompletionSelectionForegroundBrush =
SolidColorBrush.Parse("#5389F4");
public static readonly SolidColorBrush CompletionForegroundBrush =
SolidColorBrush.Parse("#B4B8BF");
} }

7
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs

@ -286,7 +286,7 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
// Connect progress handler // Connect progress handler
// client.ProgressUpdateReceived += OnProgressUpdateReceived; // client.ProgressUpdateReceived += OnProgressUpdateReceived;
client.PreviewImageReceived += OnPreviewImageReceived; client.PreviewImageReceived += OnPreviewImageReceived;
ComfyTask? promptTask = null; ComfyTask? promptTask = null;
try try
{ {
@ -356,12 +356,11 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
await using var fileStream = gridPath.Info.OpenWrite(); await using var fileStream = gridPath.Info.OpenWrite();
await fileStream.WriteAsync(grid.Encode().ToArray(), cancellationToken); await fileStream.WriteAsync(grid.Encode().ToArray(), cancellationToken);
// Insert to start of gallery // Insert to start of images
ImageGalleryCardViewModel.ImageSources.Add(new ImageSource(gridPath)); ImageGalleryCardViewModel.ImageSources.Add(new ImageSource(gridPath));
// var bitmaps = (await outputImages.SelectAsync(async i => await i.GetBitmapAsync())).ToImmutableArray();
} }
// Insert rest of images // Add rest of images
ImageGalleryCardViewModel.ImageSources.AddRange(outputImages); ImageGalleryCardViewModel.ImageSources.AddRange(outputImages);
} }
finally finally

Loading…
Cancel
Save