add auto complete

This commit is contained in:
bluepilledgreat 2024-10-20 15:51:21 +01:00
parent 23e1b2d737
commit 3a923e2614
4 changed files with 641 additions and 1 deletions

View File

@ -28,6 +28,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Resources\CustomBootstrapperSchema.json" />
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate.xml" /> <EmbeddedResource Include="Resources\CustomBootstrapperTemplate.xml" />
<EmbeddedResource Include="Resources\Icon2008.ico" /> <EmbeddedResource Include="Resources\Icon2008.ico" />
<EmbeddedResource Include="Resources\Icon2011.ico" /> <EmbeddedResource Include="Resources\Icon2011.ico" />

View File

@ -21,5 +21,10 @@ namespace Bloxstrap
await stream.CopyToAsync(memoryStream); await stream.CopyToAsync(memoryStream);
return memoryStream.ToArray(); return memoryStream.ToArray();
} }
public static async Task<string> GetString(string name)
{
return Encoding.UTF8.GetString(await Get(name));
}
} }
} }

View File

@ -0,0 +1,251 @@
{
"Elements": {
"FrameworkElement": {
"IsCreatable": false,
"Attributes": {
"Name": "string",
"Visibility": "Visibility",
"IsEnabled": "bool",
"Margin": "Thickness",
"Height": "double",
"Width": "double",
"HorizontalAlignment": "HorizontalAlignment",
"VerticalAlignment": "VerticalAlignment",
"ZIndex": "int"
}
},
"Control": {
"SuperClass": "FrameworkElement",
"IsCreatable": false,
"Attributes": {
"Padding": "Thickness",
"BorderThickness": "Thickness",
"Foreground": "Brush",
"Background": "Brush",
"BorderBrush": "Brush"
}
},
"BloxstrapCustomBootstrapper": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"Theme": "Theme"
}
},
"TitleBar": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"ShowMinimize": "bool",
"ShowMaximize": "bool",
"Title": "string"
}
},
"Button": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"Content": "Content"
}
},
"ProgressBar": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"IsIndeterminate": "bool",
"Value": "double",
"Maximum": "double"
}
},
"TextBlock": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Text": "string",
"Foreground": "Brush",
"Background": "Brush",
"FontSize": "double",
"FontWeight": "FontWeight",
"FontStyle": "FontStyle",
"LineHeight": "double",
"LineStackingStrategy": "LineStackingStrategy",
"TextAlignment": "TextAlignment",
"TextTrimming": "TextTrimming",
"TextWrapping": "TextWrapping",
"TextDecorations": "TextDecorations",
"IsHyphenationEnabled": "bool",
"BaselineOffset": "double",
"Padding": "Thickness"
}
},
"MarkdownTextBlock": {
"SuperClass": "TextBlock",
"IsCreatable": true,
"Attributes": {}
},
"Image": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Stretch": "Stretch",
"StretchDirection": "StretchDirection",
"Source": "string",
"IsAnimated": "bool"
}
},
"ScaleTransform": {
"IsCreatable": true,
"Attributes": {
"ScaleX": "double",
"ScaleY": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"SkewTransform": {
"IsCreatable": true,
"Attributes": {
"AngleX": "double",
"AngleY": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"RotateTransform": {
"IsCreatable": true,
"Attributes": {
"Angle": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"TranslateTransform": {
"IsCreatable": true,
"Attributes": {
"X": "double",
"Y": "double"
}
},
"RenderTransform": {
"IsCreatable": true,
"Attributes": {}
},
"Content": {
"IsCreatable": true,
"Attributes": {}
}
},
"Types": {
"string": {},
"bool": {
"Values": [
"True",
"False"
]
},
"int": {},
"double": {},
"Thickness": {},
"Brush": {},
"Content": {},
"Visibility": {
"Values": [
"Visible",
"Hidden",
"Collapsed"
]
},
"HorizontalAlignment": {
"Values": [
"Left",
"Center",
"Right",
"Stretch"
]
},
"VerticalAlignment": {
"Values": [
"Top",
"Center",
"Bottom",
"Stretch"
]
},
"Theme": {
"Values": [
"Default",
"Dark",
"Light"
]
},
"FontWeight": {
"Values": [
"Thin",
"ExtraLight",
"UltraLight",
"Medium",
"Normal",
"Regular",
"DemiBold",
"SemiBold",
"Bold",
"ExtraBold",
"UltraBold",
"Black",
"Heavy",
"ExtraBlack",
"ExtraHeavy"
]
},
"FontStyle": {
"Values": [
"Normal",
"Italic",
"Oblique"
]
},
"LineStackingStrategy": {
"Values": [
"BlockLineHeight",
"MaxHeight"
]
},
"TextTrimming": {
"Values": [
"None",
"CharacterEllipsis",
"WordEllipsis"
]
},
"TextWrapping": {
"Values": [
"WrapWithOverflow",
"NoWrap",
"Wrap"
]
},
"TextDecorations": {
"Values": [
"Baseline",
"OverLine",
"Strikethrough",
"Underline"
]
},
"Stretch": {
"Values": [
"None",
"Fill",
"Uniform",
"UniformToFill"
]
},
"StretchDirection": {
"Values": [
"UpOnly",
"DownOnly",
"Both"
]
}
}
}

View File

@ -1,4 +1,10 @@
using Bloxstrap.UI.Elements.Base; using System.Windows.Input;
using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using Bloxstrap.UI.Elements.Base;
using Bloxstrap.UI.ViewModels.Editor; using Bloxstrap.UI.ViewModels.Editor;
namespace Bloxstrap.UI.Elements.Editor namespace Bloxstrap.UI.Elements.Editor
@ -8,8 +14,98 @@ namespace Bloxstrap.UI.Elements.Editor
/// </summary> /// </summary>
public partial class BootstrapperEditorWindow : WpfUiWindow public partial class BootstrapperEditorWindow : WpfUiWindow
{ {
private static class CustomBootstrapperSchema
{
private class Schema
{
public Dictionary<string, Element> Elements { get; set; } = new Dictionary<string, Element>();
public Dictionary<string, Type> Types { get; set; } = new Dictionary<string, Type>();
}
private class Element
{
public string? SuperClass { get; set; } = null;
public bool IsCreatable { get; set; } = false;
// [AttributeName] = [TypeName]
public Dictionary<string, string> Attributes { get; set; } = new Dictionary<string, string>();
}
public class Type
{
public List<string>? Values { get; set; } = null;
}
private static Schema? _schema;
/// <summary>
/// Elements and their attributes
/// </summary>
public static Dictionary<string, Dictionary<string, string>> ElementInfo { get; set; } = new();
/// <summary>
/// All type info
/// </summary>
public static Dictionary<string, Type> Types { get; set; } = new();
public static void ParseSchema()
{
if (_schema != null)
return;
_schema = JsonSerializer.Deserialize<Schema>(Resource.GetString("CustomBootstrapperSchema.json").Result);
if (_schema == null)
throw new Exception("Deserialised CustomBootstrapperSchema is null");
Types = _schema.Types;
PopulateElementInfo();
}
private static Dictionary<string, string> GetElementAttributes(string name, Element element)
{
if (ElementInfo.ContainsKey(name))
return ElementInfo[name];
Dictionary<string, string> attributes = new();
foreach (var attribute in element.Attributes)
attributes.Add(attribute.Key, attribute.Value);
if (element.SuperClass != null)
{
foreach (var attribute in GetElementAttributes(element.SuperClass, _schema!.Elements[element.SuperClass]))
attributes.Add(attribute.Key, attribute.Value);
}
return attributes;
}
private static void PopulateElementInfo()
{
List<string> toRemove = new List<string>();
foreach (var element in _schema!.Elements)
{
ElementInfo[element.Key] = GetElementAttributes(element.Key, element.Value);
if (!element.Value.IsCreatable)
toRemove.Add(element.Key);
}
// remove non-creatable from list now that everything is done
foreach (var name in toRemove)
{
ElementInfo.Remove(name);
}
}
}
CompletionWindow? _completionWindow = null;
public BootstrapperEditorWindow(string name) public BootstrapperEditorWindow(string name)
{ {
CustomBootstrapperSchema.ParseSchema();
var viewModel = new BootstrapperEditorWindowViewModel(); var viewModel = new BootstrapperEditorWindowViewModel();
viewModel.Name = name; viewModel.Name = name;
viewModel.Title = $"Editing \"{name}\""; viewModel.Title = $"Editing \"{name}\"";
@ -19,6 +115,7 @@ namespace Bloxstrap.UI.Elements.Editor
InitializeComponent(); InitializeComponent();
UIXML.Text = viewModel.Code; UIXML.Text = viewModel.Code;
UIXML.TextArea.TextEntered += OnTextAreaTextEntered;
} }
private void OnCodeChanged(object sender, EventArgs e) private void OnCodeChanged(object sender, EventArgs e)
@ -27,5 +124,291 @@ namespace Bloxstrap.UI.Elements.Editor
viewModel.Code = UIXML.Text; viewModel.Code = UIXML.Text;
viewModel.OnPropertyChanged(nameof(viewModel.Code)); viewModel.OnPropertyChanged(nameof(viewModel.Code));
} }
private void OnTextAreaTextEntered(object sender, TextCompositionEventArgs e)
{
switch (e.Text)
{
case "<":
OpenElementAutoComplete();
break;
case " ":
OpenAttributeAutoComplete();
break;
case "/":
CloseCompletionWindow();
break;
case ">":
CloseCompletionWindow();
break;
}
}
private (string, int) GetLineAndPosAtCaretPosition()
{
// this assumes the file was saved as CSLF (\r\n newlines)
int lineStartIdx = UIXML.Text.LastIndexOf('\n', UIXML.CaretOffset);
int lineEndIdx = UIXML.Text.IndexOf('\n', UIXML.CaretOffset);
string line;
int pos;
if (lineStartIdx == -1 && lineEndIdx == -1)
{
line = UIXML.Text;
pos = UIXML.CaretOffset;
}
else if (lineStartIdx == -1)
{
line = UIXML.Text[..(lineEndIdx - 1)];
pos = UIXML.CaretOffset;
}
else if (lineEndIdx == -1)
{
line = UIXML.Text[(lineStartIdx + 1)..];
pos = UIXML.CaretOffset - lineStartIdx - 2;
}
else
{
line = UIXML.Text[(lineStartIdx + 1)..(lineEndIdx - 1)];
pos = UIXML.CaretOffset - lineStartIdx - 2;
}
return (line, pos);
}
/// <summary>
/// Source: https://xsemmel.codeplex.com
/// </summary>
/// <param name="xml"></param>
/// <param name="offset"></param>
/// <returns></returns>
public static string? GetElementAtCursor(string xml, int offset)
{
if (offset == xml.Length)
{
offset--;
}
int startIdx = xml.LastIndexOf('<', offset);
if (startIdx < 0) return null;
if (startIdx < xml.Length && xml[startIdx + 1] == '/')
{
startIdx = startIdx + 1;
}
int endIdx1 = xml.IndexOf(' ', startIdx);
if (endIdx1 == -1 /*|| endIdx1 > offset*/) endIdx1 = int.MaxValue;
int endIdx2 = xml.IndexOf('>', startIdx);
if (endIdx2 == -1 /*|| endIdx2 > offset*/)
{
endIdx2 = int.MaxValue;
}
else
{
if (endIdx2 < offset)
return null; // we dont want attribute auto complete to show outside of elements
if (endIdx2 < xml.Length && xml[endIdx2 - 1] == '/')
{
endIdx2 = endIdx2 - 1;
}
}
int endIdx = Math.Min(endIdx1, endIdx2);
if (endIdx2 > 0 && endIdx2 < int.MaxValue && endIdx > startIdx)
{
return xml.Substring(startIdx + 1, endIdx - startIdx - 1);
}
else
{
return null;
}
}
/// <summary>
/// Returns null if not eligible to auto complete there.
/// Returns the name of the element to show the attributes for
/// </summary>
/// <returns></returns>
private string? ShowAttributesForElementName()
{
(string line, int pos) = GetLineAndPosAtCaretPosition();
// check if theres an even number of speech marks on the line
int numSpeech = line.Count(x => x == '"');
if (numSpeech % 2 == 0)
{
// we have an equal number, let's check if pos is in between the speech marks
int count = -1;
int idx = pos;
while (idx != -1)
{
count++;
idx = line.IndexOf('"', idx + 1);
}
if (count % 2 != 0)
{
// odd number of speech marks means we're inside a string right now
// we dont want to display attribute auto complete while we're inside a string
return null;
}
}
return GetElementAtCursor(UIXML.Text, UIXML.CaretOffset);
}
private void OpenElementAutoComplete()
{
var data = new List<ICompletionData>();
foreach (var element in CustomBootstrapperSchema.ElementInfo.Keys)
data.Add(new ElementCompletionData(element));
ShowCompletionWindow(data);
}
private void OpenAttributeAutoComplete()
{
string? element = ShowAttributesForElementName();
if (element == null)
{
CloseCompletionWindow();
return;
}
if (!CustomBootstrapperSchema.ElementInfo.ContainsKey(element))
{
CloseCompletionWindow();
return;
}
var attributes = CustomBootstrapperSchema.ElementInfo[element];
var data = new List<ICompletionData>();
foreach (var attribute in attributes)
data.Add(new AttributeCompletionData(attribute.Key, () => OpenTypeValueAutoComplete(attribute.Value)));
ShowCompletionWindow(data);
}
private void OpenTypeValueAutoComplete(string typeName)
{
var typeValues = CustomBootstrapperSchema.Types[typeName].Values;
if (typeValues == null)
return;
var data = new List<ICompletionData>();
foreach (var value in typeValues)
data.Add(new TypeValueCompletionData(value));
ShowCompletionWindow(data);
}
private void CloseCompletionWindow()
{
if (_completionWindow != null)
{
_completionWindow.Close();
_completionWindow = null;
}
}
private void ShowCompletionWindow(List<ICompletionData> completionData)
{
CloseCompletionWindow();
_completionWindow = new CompletionWindow(UIXML.TextArea);
IList<ICompletionData> data = _completionWindow.CompletionList.CompletionData;
foreach (var c in completionData)
data.Add(c);
_completionWindow.Show();
_completionWindow.Closed += (_, _) => _completionWindow = null;
}
}
public class ElementCompletionData : ICompletionData
{
public ElementCompletionData(string text)
{
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text);
}
}
public class AttributeCompletionData : ICompletionData
{
private Action _openValueAutoCompleteAction;
public AttributeCompletionData(string text, Action openValueAutoCompleteAction)
{
_openValueAutoCompleteAction = openValueAutoCompleteAction;
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text + "=\"\"");
textArea.Caret.Offset = textArea.Caret.Offset - 1;
_openValueAutoCompleteAction();
}
}
public class TypeValueCompletionData : ICompletionData
{
public TypeValueCompletionData(string text)
{
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text);
}
} }
} }