bloxstrap/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs
Matt 9d356b0b71
Some checks are pending
CI (Debug) / build (push) Waiting to run
CI (Release) / build (push) Waiting to run
CI (Release) / release (push) Blocked by required conditions
CI (Release) / release-test (push) Blocked by required conditions
Custom bootstrapper themes (#4380)
* 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
2025-03-11 19:18:54 +00:00

301 lines
11 KiB
C#

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Xml.Linq;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
public partial class CustomDialog
{
struct GetImageSourceDataResult
{
public bool IsIcon = false;
public Uri? Uri = null;
public GetImageSourceDataResult()
{
}
}
private static string GetXmlAttribute(XElement element, string attributeName, string? defaultValue = null)
{
var attribute = element.Attribute(attributeName);
if (attribute == null)
{
if (defaultValue != null)
return defaultValue;
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissing", element.Name, attributeName);
}
return attribute.Value.ToString();
}
private static T ParseXmlAttribute<T>(XElement element, string attributeName, T? defaultValue = null) where T : struct
{
var attribute = element.Attribute(attributeName);
if (attribute == null)
{
if (defaultValue != null)
return (T)defaultValue;
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissing", element.Name, attributeName);
}
T? parsed = ConvertValue<T>(attribute.Value);
if (parsed == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeInvalidType", element.Name, attributeName, typeof(T).Name);
return (T)parsed;
}
/// <summary>
/// ParseXmlAttribute but the default value is always null
/// </summary>
private static T? ParseXmlAttributeNullable<T>(XElement element, string attributeName) where T : struct
{
var attribute = element.Attribute(attributeName);
if (attribute == null)
return null;
T? parsed = ConvertValue<T>(attribute.Value);
if (parsed == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeInvalidType", element.Name, attributeName, typeof(T).Name);
return (T)parsed;
}
private static void ValidateXmlElement(string elementName, string attributeName, int value, int? min = null, int? max = null)
{
if (min != null && value < min)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeLargerThanMin", elementName, attributeName, min);
if (max != null && value > max)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax", elementName, attributeName, max);
}
private static void ValidateXmlElement(string elementName, string attributeName, double value, double? min = null, double? max = null)
{
if (min != null && value < min)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeLargerThanMin", elementName, attributeName, min);
if (max != null && value > max)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax", elementName, attributeName, max);
}
// You can't do numeric only generics in .NET 6. The feature is exclusive to .NET 7+.
private static int ParseXmlAttributeClamped(XElement element, string attributeName, int? defaultValue = null, int? min = null, int? max = null)
{
int value = ParseXmlAttribute<int>(element, attributeName, defaultValue);
ValidateXmlElement(element.Name.ToString(), attributeName, value, min, max);
return value;
}
private static FontWeight GetFontWeightFromXElement(XElement element)
{
string? value = element.Attribute("FontWeight")?.Value?.ToString();
if (string.IsNullOrEmpty(value))
value = "Normal";
// bruh
// https://learn.microsoft.com/en-us/dotnet/api/system.windows.fontweights?view=windowsdesktop-6.0
switch (value)
{
case "Thin":
return FontWeights.Thin;
case "ExtraLight":
case "UltraLight":
return FontWeights.ExtraLight;
case "Medium":
return FontWeights.Medium;
case "Normal":
case "Regular":
return FontWeights.Normal;
case "DemiBold":
case "SemiBold":
return FontWeights.DemiBold;
case "Bold":
return FontWeights.Bold;
case "ExtraBold":
case "UltraBold":
return FontWeights.ExtraBold;
case "Black":
case "Heavy":
return FontWeights.Black;
case "ExtraBlack":
case "UltraBlack":
return FontWeights.UltraBlack;
default:
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "FontWeight", value);
}
}
private static FontStyle GetFontStyleFromXElement(XElement element)
{
string? value = element.Attribute("FontStyle")?.Value?.ToString();
if (string.IsNullOrEmpty(value))
value = "Normal";
switch (value)
{
case "Normal":
return FontStyles.Normal;
case "Italic":
return FontStyles.Italic;
case "Oblique":
return FontStyles.Oblique;
default:
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "FontStyle", value);
}
}
private static TextDecorationCollection? GetTextDecorationsFromXElement(XElement element)
{
string? value = element.Attribute("TextDecorations")?.Value?.ToString();
if (string.IsNullOrEmpty(value))
return null;
switch (value)
{
case "Baseline":
return TextDecorations.Baseline;
case "OverLine":
return TextDecorations.OverLine;
case "Strikethrough":
return TextDecorations.Strikethrough;
case "Underline":
return TextDecorations.Underline;
default:
throw new CustomThemeException("CustomTheme.Errors.UnknownEnumValue", element.Name, "TextDecorations", value);
}
}
private static string? GetTranslatedText(string? text)
{
if (text == null || !text.StartsWith('{') || !text.EndsWith('}'))
return text; // can't be translated (not in the correct format)
string resourceName = text[1..^1];
if (resourceName == "Version")
return App.Version;
return Strings.ResourceManager.GetStringSafe(resourceName);
}
private static string? GetFullPath(CustomDialog dialog, string? sourcePath)
{
if (sourcePath == null)
return null;
// TODO: this is bad :(
return sourcePath.Replace("theme://", $"{dialog.ThemeDir}\\");
}
private static GetImageSourceDataResult GetImageSourceData(CustomDialog dialog, string name, XElement xmlElement)
{
string path = GetXmlAttribute(xmlElement, name);
if (path == "{Icon}")
return new GetImageSourceDataResult { IsIcon = true };
path = GetFullPath(dialog, path)!;
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out Uri? result))
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeParseError", xmlElement.Name, name, "Uri");
if (result == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeParseErrorNull", xmlElement.Name, name, "Uri");
if (result.Scheme != "file")
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeBlacklistedUriScheme", xmlElement.Name, name, result.Scheme);
return new GetImageSourceDataResult { Uri = result };
}
private static object? GetContentFromXElement(CustomDialog dialog, XElement xmlElement)
{
var contentAttr = xmlElement.Attribute("Content");
var contentElement = xmlElement.Element($"{xmlElement.Name}.Content");
if (contentAttr != null && contentElement != null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", xmlElement.Name, "Content");
if (contentAttr != null)
return GetTranslatedText(contentAttr.Value);
if (contentElement == null)
return null;
var children = contentElement.Elements();
if (children.Count() > 1)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleChildren", xmlElement.Name, "Content");
var first = contentElement.FirstNode as XElement;
if (first == null)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMissingChild", xmlElement.Name, "Content");
var uiElement = HandleXml<UIElement>(dialog, first);
return uiElement;
}
private static void ApplyEffects_UIElement(CustomDialog dialog, UIElement uiElement, XElement xmlElement)
{
var effectElement = xmlElement.Element($"{xmlElement.Name}.Effect");
if (effectElement == null)
return;
var children = effectElement.Elements();
if (children.Count() > 1)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleChildren", xmlElement.Name, "Effect");
var child = children.FirstOrDefault();
if (child == null)
return;
Effect effect = HandleXml<Effect>(dialog, child);
uiElement.Effect = effect;
}
private static void ApplyTransformation_UIElement(CustomDialog dialog, string name, DependencyProperty property, UIElement uiElement, XElement xmlElement)
{
var transformElement = xmlElement.Element($"{xmlElement.Name}.{name}");
if (transformElement == null)
return;
var tg = new TransformGroup();
foreach (var child in transformElement.Elements())
{
Transform element = HandleXml<Transform>(dialog, child);
tg.Children.Add(element);
}
uiElement.SetValue(property, tg);
}
private static void ApplyTransformations_UIElement(CustomDialog dialog, UIElement uiElement, XElement xmlElement)
{
ApplyTransformation_UIElement(dialog, "RenderTransform", FrameworkElement.RenderTransformProperty, uiElement, xmlElement);
ApplyTransformation_UIElement(dialog, "LayoutTransform", FrameworkElement.LayoutTransformProperty, uiElement, xmlElement);
}
}
}