mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-23 02:51:26 -07:00
* add custom bootstrappers * add avalonedit to licenses page * add gif support * add stretch & stretchdirection to images * dont create a bitmapimage for gifs * remove maxheight and maxwidth sets * remove comment * add isenabled * add more textblock properties * add markdowntextblocks * update how transform elements are stored * overhaul textbox content * dont set fontsize if not set * fix warnings * add foreground property to control * add background property to textblock * count descendants and increase element cap * add auto complete * dont display completion window if there is no data * sort schema elements and types * make ! close the completion window * add end tag auto complete * fix pos being wrong * dont treat comments as elements * add imagebrushes * follow same conventions as brushes * fix exception messages * fix them again * update schema * fix crash * now it works * wrong attribute name * add solidcolorbrush * move converters into a separate file * add lineargradientbrushes * unify handlers * update schema * add fake BloxstrapCustomBootstrapper * stop adding an extra end character * add property element auto-complete * add title attribute to custombloxstrapbootstrapper * add shapes * add string translation support * use default wpf size instead of 100x100 * update min height of window * fix verticalalignment not working * uncap height and width * add effects * move transformation handler inside frameworkelement * fix title bar effect & transformation removal * add more frameworkelement properties * add layout transform * add font properties to control * improve window border stuff * make sure file contents are in CRLF * add cornerradius to progress bar * add progressring * Update wpfui * update schema * update function names * add children check to content * make sure only one content is defined * add fontfamily * update schema * only allow file uris for images * disable backdrop * move text setter to textblock handler from base * split up creator into multiple files * turn version into a constant * add grids * cleanup converters * add IgnoreTitleBarInset * add Version to schema * reveal custom bootstrapper stuff on selection * increase listbox height * only set statustext binding in textblock * update ui * rename ZIndex to Panel.ZIndex * add stackpanel * add border * fix being unable to apply transforms on grids * rearrange and add new editor button * use snackbars for saving * add close confirmation message * use viewmodel variable * remove pointless onpropertychanged call * add version string format * start editor window in the centre * update licenses page also resized the about window so everything could fit nicely * fix border not inheriting frameworkelement * add WindowCornerPreference * add the import dialog * add an export theme button * update version number * localise CustomDialog exceptions * localise custom theme editor * localise custom theme add dialog * localise frontend * localise appearance menu page * change customtheme error strings namespace * change icons on appearance page * update button margin on appearance page
581 lines
19 KiB
C#
581 lines
19 KiB
C#
using System.Windows.Input;
|
|
using System.Xml;
|
|
|
|
using ICSharpCode.AvalonEdit.CodeCompletion;
|
|
using ICSharpCode.AvalonEdit.Document;
|
|
using ICSharpCode.AvalonEdit.Editing;
|
|
using ICSharpCode.AvalonEdit.Highlighting.Xshd;
|
|
using ICSharpCode.AvalonEdit.Highlighting;
|
|
|
|
using Bloxstrap.UI.Elements.Base;
|
|
using Bloxstrap.UI.ViewModels.Editor;
|
|
using System.Windows;
|
|
|
|
namespace Bloxstrap.UI.Elements.Editor
|
|
{
|
|
/// <summary>
|
|
/// Interaction logic for BootstrapperEditorWindow.xaml
|
|
/// </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 bool CanHaveElement { get; set; } = false;
|
|
public List<string>? Values { get; set; } = null;
|
|
}
|
|
|
|
private static Schema? _schema;
|
|
|
|
/// <summary>
|
|
/// Elements and their attributes
|
|
/// </summary>
|
|
public static SortedDictionary<string, SortedDictionary<string, string>> ElementInfo { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Attributes of elements that can have property elements
|
|
/// </summary>
|
|
public static Dictionary<string, List<string>> PropertyElements { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// All type info
|
|
/// </summary>
|
|
public static SortedDictionary<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");
|
|
|
|
foreach (var type in _schema.Types)
|
|
Types.Add(type.Key, type.Value);
|
|
|
|
PopulateElementInfo();
|
|
}
|
|
|
|
private static (SortedDictionary<string, string>, List<string>) GetElementAttributes(string name, Element element)
|
|
{
|
|
if (ElementInfo.ContainsKey(name))
|
|
return (ElementInfo[name], PropertyElements[name]);
|
|
|
|
List<string> properties = new List<string>();
|
|
SortedDictionary<string, string> attributes = new();
|
|
|
|
foreach (var attribute in element.Attributes)
|
|
{
|
|
attributes.Add(attribute.Key, attribute.Value);
|
|
|
|
if (!Types.ContainsKey(attribute.Value))
|
|
throw new Exception($"Schema for type {attribute.Value} is missing. Blame Matt!");
|
|
|
|
Type type = Types[attribute.Value];
|
|
if (type.CanHaveElement)
|
|
properties.Add(attribute.Key);
|
|
}
|
|
|
|
if (element.SuperClass != null)
|
|
{
|
|
(SortedDictionary<string, string> superAttributes, List<string> superProperties) = GetElementAttributes(element.SuperClass, _schema!.Elements[element.SuperClass]);
|
|
foreach (var attribute in superAttributes)
|
|
attributes.Add(attribute.Key, attribute.Value);
|
|
|
|
foreach (var property in superProperties)
|
|
properties.Add(property);
|
|
}
|
|
|
|
properties.Sort();
|
|
|
|
ElementInfo[name] = attributes;
|
|
PropertyElements[name] = properties;
|
|
|
|
return (attributes, properties);
|
|
}
|
|
|
|
private static void PopulateElementInfo()
|
|
{
|
|
List<string> toRemove = new List<string>();
|
|
|
|
foreach (var element in _schema!.Elements)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private BootstrapperEditorWindowViewModel _viewModel;
|
|
private CompletionWindow? _completionWindow = null;
|
|
|
|
public BootstrapperEditorWindow(string name)
|
|
{
|
|
CustomBootstrapperSchema.ParseSchema();
|
|
|
|
string directory = Path.Combine(Paths.CustomThemes, name);
|
|
|
|
string themeContents = File.ReadAllText(Path.Combine(directory, "Theme.xml"));
|
|
themeContents = ToCRLF(themeContents); // make sure the theme is in CRLF. a function expects CRLF.
|
|
|
|
_viewModel = new BootstrapperEditorWindowViewModel();
|
|
_viewModel.ThemeSavedCallback = ThemeSavedCallback;
|
|
_viewModel.Directory = directory;
|
|
_viewModel.Name = name;
|
|
_viewModel.Title = string.Format(Strings.CustomTheme_Editor_Title, name);
|
|
_viewModel.Code = themeContents;
|
|
|
|
DataContext = _viewModel;
|
|
InitializeComponent();
|
|
|
|
UIXML.Text = _viewModel.Code;
|
|
UIXML.TextChanged += OnCodeChanged;
|
|
UIXML.TextArea.TextEntered += OnTextAreaTextEntered;
|
|
|
|
LoadHighlightingTheme();
|
|
}
|
|
|
|
private void LoadHighlightingTheme()
|
|
{
|
|
string name = $"Editor-Theme-{App.Settings.Prop.Theme.GetFinal()}.xshd";
|
|
using Stream xmlStream = Resource.GetStream(name);
|
|
using XmlReader reader = XmlReader.Create(xmlStream);
|
|
UIXML.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance);
|
|
|
|
UIXML.TextArea.TextView.SetResourceReference(ICSharpCode.AvalonEdit.Rendering.TextView.LinkTextForegroundBrushProperty, "NewTextEditorLink");
|
|
}
|
|
|
|
private void ThemeSavedCallback(bool success, string message)
|
|
{
|
|
if (success)
|
|
Snackbar.Show(Strings.CustomTheme_Editor_Save_Success, message, Wpf.Ui.Common.SymbolRegular.CheckmarkCircle32, Wpf.Ui.Common.ControlAppearance.Success);
|
|
else
|
|
Snackbar.Show(Strings.CustomTheme_Editor_Save_Error, message, Wpf.Ui.Common.SymbolRegular.ErrorCircle24, Wpf.Ui.Common.ControlAppearance.Danger);
|
|
}
|
|
|
|
private static string ToCRLF(string text)
|
|
{
|
|
return text.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
|
|
}
|
|
|
|
private void OnCodeChanged(object? sender, EventArgs e)
|
|
{
|
|
_viewModel.Code = UIXML.Text;
|
|
_viewModel.CodeChanged = true;
|
|
}
|
|
|
|
private void OnClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
|
{
|
|
if (!_viewModel.CodeChanged)
|
|
return;
|
|
|
|
var result = Frontend.ShowMessageBox(string.Format(Strings.CustomTheme_Editor_ConfirmSave, _viewModel.Name), MessageBoxImage.Information, MessageBoxButton.YesNoCancel);
|
|
if (result == MessageBoxResult.Cancel)
|
|
{
|
|
e.Cancel = true;
|
|
}
|
|
else if (result == MessageBoxResult.Yes)
|
|
{
|
|
_viewModel.SaveCommand.Execute(null);
|
|
}
|
|
}
|
|
|
|
private void OnTextAreaTextEntered(object sender, TextCompositionEventArgs e)
|
|
{
|
|
switch (e.Text)
|
|
{
|
|
case "<":
|
|
OpenElementAutoComplete();
|
|
break;
|
|
case " ":
|
|
OpenAttributeAutoComplete();
|
|
break;
|
|
case ".":
|
|
OpenPropertyElementAutoComplete();
|
|
break;
|
|
case "/":
|
|
AddEndTag();
|
|
break;
|
|
case ">":
|
|
CloseCompletionWindow();
|
|
break;
|
|
case "!":
|
|
CloseCompletionWindow();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private (string, int) GetLineAndPosAtCaretPosition()
|
|
{
|
|
// this assumes the file was saved as CSLF (\r\n newlines)
|
|
int offset = UIXML.CaretOffset - 1;
|
|
int lineStartIdx = UIXML.Text.LastIndexOf('\n', offset);
|
|
int lineEndIdx = UIXML.Text.IndexOf('\n', offset);
|
|
|
|
string line;
|
|
int pos;
|
|
if (lineStartIdx == -1 && lineEndIdx == -1)
|
|
{
|
|
line = UIXML.Text;
|
|
pos = offset;
|
|
}
|
|
else if (lineStartIdx == -1)
|
|
{
|
|
line = UIXML.Text[..(lineEndIdx - 1)];
|
|
pos = offset;
|
|
}
|
|
else if (lineEndIdx == -1)
|
|
{
|
|
line = UIXML.Text[(lineStartIdx + 1)..];
|
|
pos = offset - lineStartIdx - 2;
|
|
}
|
|
else
|
|
{
|
|
line = UIXML.Text[(lineStartIdx + 1)..(lineEndIdx - 1)];
|
|
pos = offset - 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, bool onlyAllowInside = false)
|
|
{
|
|
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 (onlyAllowInside && 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)
|
|
{
|
|
string element = xml.Substring(startIdx + 1, endIdx - startIdx - 1);
|
|
return element == "!--" ? null : element; // dont treat comments as elements
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A space between the cursor and the element will completely cancel this function
|
|
/// </summary>
|
|
private string? GetElementAtCursorNoSpaces(string xml, int offset)
|
|
{
|
|
(string line, int pos) = GetLineAndPosAtCaretPosition();
|
|
|
|
string curr = "";
|
|
while (pos != -1)
|
|
{
|
|
char c = line[pos];
|
|
if (c == ' ' || c == '\t')
|
|
return null;
|
|
if (c == '<')
|
|
return curr;
|
|
curr = c + curr;
|
|
pos--;
|
|
}
|
|
|
|
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;
|
|
int size = line.Length - 1;
|
|
while (idx != -1)
|
|
{
|
|
count++;
|
|
|
|
if (size > idx + 1)
|
|
idx = line.IndexOf('"', idx + 1);
|
|
else
|
|
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, true);
|
|
}
|
|
|
|
private void AddEndTag()
|
|
{
|
|
CloseCompletionWindow();
|
|
|
|
if (UIXML.Text.Length > 2 && UIXML.Text[UIXML.CaretOffset - 2] == '<')
|
|
{
|
|
var elementName = GetElementAtCursor(UIXML.Text, UIXML.CaretOffset - 3);
|
|
if (elementName == null)
|
|
return;
|
|
|
|
UIXML.TextArea.Document.Insert(UIXML.CaretOffset, $"{elementName}>");
|
|
}
|
|
else
|
|
{
|
|
if (UIXML.Text.Length > UIXML.CaretOffset && UIXML.Text[UIXML.CaretOffset] == '>')
|
|
return;
|
|
|
|
var elementName = ShowAttributesForElementName(); // re-using functions :)
|
|
if (elementName != null)
|
|
UIXML.TextArea.Document.Insert(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 OpenPropertyElementAutoComplete()
|
|
{
|
|
string? element = GetElementAtCursorNoSpaces(UIXML.Text, UIXML.CaretOffset);
|
|
if (element == null)
|
|
{
|
|
CloseCompletionWindow();
|
|
return;
|
|
}
|
|
|
|
if (!CustomBootstrapperSchema.PropertyElements.ContainsKey(element))
|
|
{
|
|
CloseCompletionWindow();
|
|
return;
|
|
}
|
|
|
|
var properties = CustomBootstrapperSchema.PropertyElements[element];
|
|
|
|
var data = new List<ICompletionData>();
|
|
|
|
foreach (var property in properties)
|
|
data.Add(new TypeValueCompletionData(property));
|
|
|
|
ShowCompletionWindow(data);
|
|
}
|
|
|
|
private void CloseCompletionWindow()
|
|
{
|
|
if (_completionWindow != null)
|
|
{
|
|
_completionWindow.Close();
|
|
_completionWindow = null;
|
|
}
|
|
}
|
|
|
|
private void ShowCompletionWindow(List<ICompletionData> completionData)
|
|
{
|
|
CloseCompletionWindow();
|
|
|
|
if (!completionData.Any())
|
|
return;
|
|
|
|
_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);
|
|
}
|
|
}
|
|
}
|