mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 10:01:27 -07:00
add auto complete
This commit is contained in:
parent
23e1b2d737
commit
3a923e2614
@ -28,6 +28,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\CustomBootstrapperSchema.json" />
|
||||
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate.xml" />
|
||||
<EmbeddedResource Include="Resources\Icon2008.ico" />
|
||||
<EmbeddedResource Include="Resources\Icon2011.ico" />
|
||||
|
@ -21,5 +21,10 @@ namespace Bloxstrap
|
||||
await stream.CopyToAsync(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
public static async Task<string> GetString(string name)
|
||||
{
|
||||
return Encoding.UTF8.GetString(await Get(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
251
Bloxstrap/Resources/CustomBootstrapperSchema.json
Normal file
251
Bloxstrap/Resources/CustomBootstrapperSchema.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
namespace Bloxstrap.UI.Elements.Editor
|
||||
@ -8,8 +14,98 @@ namespace Bloxstrap.UI.Elements.Editor
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
CustomBootstrapperSchema.ParseSchema();
|
||||
|
||||
var viewModel = new BootstrapperEditorWindowViewModel();
|
||||
viewModel.Name = name;
|
||||
viewModel.Title = $"Editing \"{name}\"";
|
||||
@ -19,6 +115,7 @@ namespace Bloxstrap.UI.Elements.Editor
|
||||
InitializeComponent();
|
||||
|
||||
UIXML.Text = viewModel.Code;
|
||||
UIXML.TextArea.TextEntered += OnTextAreaTextEntered;
|
||||
}
|
||||
|
||||
private void OnCodeChanged(object sender, EventArgs e)
|
||||
@ -27,5 +124,291 @@ namespace Bloxstrap.UI.Elements.Editor
|
||||
viewModel.Code = UIXML.Text;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user