diff --git a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs index 7335f22e..70f8d30e 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs @@ -1,17 +1,6 @@ using System.Windows; -using System.Windows.Controls; -using System.Windows.Controls.Primitives; -using System.Windows.Data; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Media.Effects; -using System.Windows.Shapes; using System.Xml.Linq; -using Wpf.Ui.Markup; - -using Bloxstrap.UI.Elements.Controls; - namespace Bloxstrap.UI.Elements.Bootstrapper { public partial class CustomDialog @@ -59,894 +48,6 @@ private class DummyFrameworkElement : FrameworkElement { } ["Rectangle"] = HandleXmlElement_Rectangle }; - - #region Utilities - 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 Exception($"Element {element.Name} is missing the {attributeName} attribute"); - } - - return attribute.Value.ToString(); - } - - private static T ParseXmlAttribute(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 Exception($"Element {element.Name} is missing the {attributeName} attribute"); - } - - T? parsed = ConvertValue(attribute.Value); - if (parsed == null) - throw new Exception($"{element.Name} {attributeName} is not a valid {typeof(T).Name}"); - - return (T)parsed; - } - - /// - /// ParseXmlAttribute but the default value is always null - /// - private static T? ParseXmlAttributeNullable(XElement element, string attributeName) where T : struct - { - var attribute = element.Attribute(attributeName); - - if (attribute == null) - return null; - - T? parsed = ConvertValue(attribute.Value); - if (parsed == null) - throw new Exception($"{element.Name} {attributeName} is not a valid {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 Exception($"{elementName} {attributeName} must be larger than {min}"); - if (max != null && value > max) - throw new Exception($"{elementName} {attributeName} must be smaller than {max}"); - } - - private static void ValidateXmlElement(string elementName, string attributeName, double value, double? min = null, double? max = null) - { - if (min != null && value < min) - throw new Exception($"{elementName} {attributeName} must be larger than {min}"); - if (max != null && value > max) - throw new Exception($"{elementName} {attributeName} must be smaller than {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(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 Exception($"{element.Name} Unknown 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 Exception($"{element.Name} Unknown 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 Exception($"{element.Name} Unknown 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]; - return Strings.ResourceManager.GetStringSafe(resourceName); - } - - private static string? GetFullPath(CustomDialog dialog, string? sourcePath) - { - if (sourcePath == null) - return null; - - return sourcePath.Replace("theme://", $"{dialog.ThemeDir}\\"); - } - - struct GetImageSourceDataResult - { - public bool IsIcon = false; - public Uri? Uri = null; - - public GetImageSourceDataResult() - { - } - } - - 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 Exception($"{xmlElement.Name} failed to parse {name} as Uri"); - - if (result == null) - throw new Exception($"{xmlElement.Name} {name} Uri is null"); - - if (result.Scheme != "file") - throw new Exception($"{xmlElement.Name} {name} uses blacklisted scheme {result.Scheme}"); - - return new GetImageSourceDataResult { Uri = result }; - } - #endregion - - #region Transformation Elements - private static Transform HandleXmlElement_ScaleTransform(CustomDialog dialog, XElement xmlElement) - { - var st = new ScaleTransform(); - - st.ScaleX = ParseXmlAttribute(xmlElement, "ScaleX", 1); - st.ScaleY = ParseXmlAttribute(xmlElement, "ScaleY", 1); - st.CenterX = ParseXmlAttribute(xmlElement, "CenterX", 0); - st.CenterY = ParseXmlAttribute(xmlElement, "CenterY", 0); - - return st; - } - - private static Transform HandleXmlElement_SkewTransform(CustomDialog dialog, XElement xmlElement) - { - var st = new SkewTransform(); - - st.AngleX = ParseXmlAttribute(xmlElement, "AngleX", 0); - st.AngleY = ParseXmlAttribute(xmlElement, "AngleY", 0); - st.CenterX = ParseXmlAttribute(xmlElement, "CenterX", 0); - st.CenterY = ParseXmlAttribute(xmlElement, "CenterY", 0); - - return st; - } - - private static Transform HandleXmlElement_RotateTransform(CustomDialog dialog, XElement xmlElement) - { - var rt = new RotateTransform(); - - rt.Angle = ParseXmlAttribute(xmlElement, "Angle", 0); - rt.CenterX = ParseXmlAttribute(xmlElement, "CenterX", 0); - rt.CenterY = ParseXmlAttribute(xmlElement, "CenterY", 0); - - return rt; - } - - private static Transform HandleXmlElement_TranslateTransform(CustomDialog dialog, XElement xmlElement) - { - var tt = new TranslateTransform(); - - tt.X = ParseXmlAttribute(xmlElement, "X", 0); - tt.Y = ParseXmlAttribute(xmlElement, "Y", 0); - - return tt; - } - - 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(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); - } - #endregion - - #region Effects - private static BlurEffect HandleXmlElement_BlurEffect(CustomDialog dialog, XElement xmlElement) - { - var effect = new BlurEffect(); - - effect.KernelType = ParseXmlAttribute(xmlElement, "KernelType", KernelType.Gaussian); - effect.Radius = ParseXmlAttribute(xmlElement, "Radius", 5); - effect.RenderingBias = ParseXmlAttribute(xmlElement, "RenderingBias", RenderingBias.Performance); - - return effect; - } - - private static DropShadowEffect HandleXmlElement_DropShadowEffect(CustomDialog dialog, XElement xmlElement) - { - var effect = new DropShadowEffect(); - - effect.BlurRadius = ParseXmlAttribute(xmlElement, "BlurRadius", 5); - effect.Direction = ParseXmlAttribute(xmlElement, "Direction", 315); - effect.Opacity = ParseXmlAttribute(xmlElement, "Opacity", 1); - effect.ShadowDepth = ParseXmlAttribute(xmlElement, "ShadowDepth", 5); - effect.RenderingBias = ParseXmlAttribute(xmlElement, "RenderingBias", RenderingBias.Performance); - - var color = GetColorFromXElement(xmlElement, "Color"); - if (color is Color) - effect.Color = (Color)color; - - return effect; - } - - - 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 Exception($"{xmlElement.Name}.Effect can only have one child"); - - var child = children.FirstOrDefault(); - if (child == null) - return; - - Effect effect = HandleXml(dialog, child); - uiElement.Effect = effect; - } - #endregion - - #region Brushes - private static void HandleXml_Brush(Brush brush, XElement xmlElement) - { - brush.Opacity = ParseXmlAttribute(xmlElement, "Opacity", 1.0); - } - - private static Brush HandleXmlElement_SolidColorBrush(CustomDialog dialog, XElement xmlElement) - { - var brush = new SolidColorBrush(); - HandleXml_Brush(brush, xmlElement); - - object? color = GetColorFromXElement(xmlElement, "Color"); - if (color is Color) - brush.Color = (Color)color; - - return brush; - } - - private static Brush HandleXmlElement_ImageBrush(CustomDialog dialog, XElement xmlElement) - { - var imageBrush = new ImageBrush(); - HandleXml_Brush(imageBrush, xmlElement); - - imageBrush.AlignmentX = ParseXmlAttribute(xmlElement, "AlignmentX", AlignmentX.Center); - imageBrush.AlignmentY = ParseXmlAttribute(xmlElement, "AlignmentY", AlignmentY.Center); - - imageBrush.Stretch = ParseXmlAttribute(xmlElement, "Stretch", Stretch.Fill); - imageBrush.TileMode = ParseXmlAttribute(xmlElement, "TileMode", TileMode.None); - - imageBrush.ViewboxUnits = ParseXmlAttribute(xmlElement, "ViewboxUnits", BrushMappingMode.RelativeToBoundingBox); - imageBrush.ViewportUnits = ParseXmlAttribute(xmlElement, "ViewportUnits", BrushMappingMode.RelativeToBoundingBox); - - var viewbox = GetRectFromXElement(xmlElement, "Viewbox"); - if (viewbox is Rect) - imageBrush.Viewbox = (Rect)viewbox; - - var viewport = GetRectFromXElement(xmlElement, "Viewport"); - if (viewport is Rect) - imageBrush.Viewport = (Rect)viewport; - - var sourceData = GetImageSourceData(dialog, "ImageSource", xmlElement); - - if (sourceData.IsIcon) - { - // bind the icon property - Binding binding = new Binding("Icon") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(imageBrush, ImageBrush.ImageSourceProperty, binding); - } - else - { - BitmapImage bitmapImage; - try - { - bitmapImage = new BitmapImage(sourceData.Uri!); - } - catch (Exception ex) - { - throw new Exception($"ImageBrush Failed to create BitmapImage: {ex.Message}", ex); - } - - imageBrush.ImageSource = bitmapImage; - } - - return imageBrush; - } - - private static GradientStop HandleXmlElement_GradientStop(CustomDialog dialog, XElement xmlElement) - { - var gs = new GradientStop(); - - object? color = GetColorFromXElement(xmlElement, "Color"); - if (color is Color) - gs.Color = (Color)color; - - gs.Offset = ParseXmlAttribute(xmlElement, "Offset", 0.0); - - return gs; - } - - private static Brush HandleXmlElement_LinearGradientBrush(CustomDialog dialog, XElement xmlElement) - { - var brush = new LinearGradientBrush(); - HandleXml_Brush(brush, xmlElement); - - object? startPoint = GetPointFromXElement(xmlElement, "StartPoint"); - if (startPoint is Point) - brush.StartPoint = (Point)startPoint; - - object? endPoint = GetPointFromXElement(xmlElement, "EndPoint"); - if (endPoint is Point) - brush.EndPoint = (Point)endPoint; - - brush.ColorInterpolationMode = ParseXmlAttribute(xmlElement, "ColorInterpolationMode", ColorInterpolationMode.SRgbLinearInterpolation); - brush.MappingMode = ParseXmlAttribute(xmlElement, "MappingMode", BrushMappingMode.RelativeToBoundingBox); - brush.SpreadMethod = ParseXmlAttribute(xmlElement, "SpreadMethod", GradientSpreadMethod.Pad); - - foreach (var child in xmlElement.Elements()) - brush.GradientStops.Add(HandleXml(dialog, child)); - - return brush; - } - - private static void ApplyBrush_UIElement(CustomDialog dialog, FrameworkElement uiElement, string name, DependencyProperty dependencyProperty, XElement xmlElement) - { - // check if attribute exists - object? brushAttr = GetBrushFromXElement(xmlElement, name); - if (brushAttr is Brush) - { - uiElement.SetValue(dependencyProperty, brushAttr); - return; - } - else if (brushAttr is string) - { - uiElement.SetResourceReference(dependencyProperty, brushAttr); - return; - } - - // check if element exists - var brushElement = xmlElement.Element($"{xmlElement.Name}.{name}"); - if (brushElement == null) - return; - - var first = brushElement.FirstNode as XElement; - if (first == null) - throw new Exception($"{xmlElement.Name} {name} is missing the brush"); - - var brush = HandleXml(dialog, first); - uiElement.SetValue(dependencyProperty, brush); - } - #endregion - - #region Shapes - private static void HandleXmlElement_Shape(CustomDialog dialog, Shape shape, XElement xmlElement) - { - HandleXmlElement_FrameworkElement(dialog, shape, xmlElement); - - ApplyBrush_UIElement(dialog, shape, "Fill", Shape.FillProperty, xmlElement); - ApplyBrush_UIElement(dialog, shape, "Stroke", Shape.StrokeProperty, xmlElement); - - shape.Stretch = ParseXmlAttribute(xmlElement, "Stretch", Stretch.Fill); - - shape.StrokeDashCap = ParseXmlAttribute(xmlElement, "StrokeDashCap", PenLineCap.Flat); - shape.StrokeDashOffset = ParseXmlAttribute(xmlElement, "StrokeDashOffset", 0); - shape.StrokeEndLineCap = ParseXmlAttribute(xmlElement, "StrokeEndLineCap", PenLineCap.Flat); - shape.StrokeLineJoin = ParseXmlAttribute(xmlElement, "StrokeLineJoin", PenLineJoin.Miter); - shape.StrokeMiterLimit = ParseXmlAttribute(xmlElement, "StrokeMiterLimit", 10); - shape.StrokeStartLineCap = ParseXmlAttribute(xmlElement, "StrokeStartLineCap", PenLineCap.Flat); - shape.StrokeThickness = ParseXmlAttribute(xmlElement, "StrokeThickness", 1); - } - - private static Ellipse HandleXmlElement_Ellipse(CustomDialog dialog, XElement xmlElement) - { - var ellipse = new Ellipse(); - HandleXmlElement_Shape(dialog, ellipse, xmlElement); - - return ellipse; - } - - private static Line HandleXmlElement_Line(CustomDialog dialog, XElement xmlElement) - { - var line = new Line(); - HandleXmlElement_Shape(dialog, line, xmlElement); - - line.X1 = ParseXmlAttribute(xmlElement, "X1", 0); - line.X2 = ParseXmlAttribute(xmlElement, "X2", 0); - line.Y1 = ParseXmlAttribute(xmlElement, "Y1", 0); - line.Y2 = ParseXmlAttribute(xmlElement, "Y2", 0); - - return line; - } - - private static Rectangle HandleXmlElement_Rectangle(CustomDialog dialog, XElement xmlElement) - { - var rectangle = new Rectangle(); - HandleXmlElement_Shape(dialog, rectangle, xmlElement); - - rectangle.RadiusX = ParseXmlAttribute(xmlElement, "RadiusX", 0); - rectangle.RadiusY = ParseXmlAttribute(xmlElement, "RadiusY", 0); - - return rectangle; - } - - #endregion - - #region Elements - private static void HandleXmlElement_FrameworkElement(CustomDialog dialog, FrameworkElement uiElement, XElement xmlElement) - { - // prevent two elements from having the same name - string? name = xmlElement.Attribute("Name")?.Value?.ToString(); - if (name != null) - { - if (dialog.UsedNames.Contains(name)) - throw new Exception($"{xmlElement.Name} has duplicate name {name}"); - - dialog.UsedNames.Add(name); - } - - uiElement.Name = name; - - uiElement.Visibility = ParseXmlAttribute(xmlElement, "Visibility", Visibility.Visible); - uiElement.IsEnabled = ParseXmlAttribute(xmlElement, "IsEnabled", true); - - object? margin = GetThicknessFromXElement(xmlElement, "Margin"); - if (margin != null) - uiElement.Margin = (Thickness)margin; - - uiElement.Height = ParseXmlAttribute(xmlElement, "Height", double.NaN); - uiElement.Width = ParseXmlAttribute(xmlElement, "Width", double.NaN); - - // default values of these were originally Stretch but that was no good - uiElement.HorizontalAlignment = ParseXmlAttribute(xmlElement, "HorizontalAlignment", HorizontalAlignment.Left); - uiElement.VerticalAlignment = ParseXmlAttribute(xmlElement, "VerticalAlignment", VerticalAlignment.Top); - - uiElement.Opacity = ParseXmlAttribute(xmlElement, "Opacity", 1); - ApplyBrush_UIElement(dialog, uiElement, "OpacityMask", FrameworkElement.OpacityMaskProperty, xmlElement); - - object? renderTransformOrigin = GetPointFromXElement(xmlElement, "RenderTransformOrigin"); - if (renderTransformOrigin is Point) - uiElement.RenderTransformOrigin = (Point)renderTransformOrigin; - - int zIndex = ParseXmlAttributeClamped(xmlElement, "ZIndex", defaultValue: 0, min: 0, max: 1000); - Panel.SetZIndex(uiElement, zIndex); - - ApplyTransformations_UIElement(dialog, uiElement, xmlElement); - ApplyEffects_UIElement(dialog, uiElement, xmlElement); - } - - private static void HandleXmlElement_Control(CustomDialog dialog, Control uiElement, XElement xmlElement) - { - HandleXmlElement_FrameworkElement(dialog, uiElement, xmlElement); - - object? padding = GetThicknessFromXElement(xmlElement, "Padding"); - if (padding != null) - uiElement.Padding = (Thickness)padding; - - object? borderThickness = GetThicknessFromXElement(xmlElement, "BorderThickness"); - if (borderThickness != null) - uiElement.BorderThickness = (Thickness)borderThickness; - - ApplyBrush_UIElement(dialog, uiElement, "Foreground", Control.ForegroundProperty, xmlElement); - - ApplyBrush_UIElement(dialog, uiElement, "Background", Control.BackgroundProperty, xmlElement); - - ApplyBrush_UIElement(dialog, uiElement, "BorderBrush", Control.BorderBrushProperty, xmlElement); - - var fontSize = ParseXmlAttributeNullable(xmlElement, "FontSize"); - if (fontSize is double) - uiElement.FontSize = (double)fontSize; - uiElement.FontWeight = GetFontWeightFromXElement(xmlElement); - uiElement.FontStyle = GetFontStyleFromXElement(xmlElement); - - // NOTE: font family can both be the name of the font or a uri - string? fontFamily = GetFullPath(dialog, xmlElement.Attribute("FontFamily")?.Value); - if (fontFamily != null) - uiElement.FontFamily = new System.Windows.Media.FontFamily(fontFamily); - } - - private static UIElement HandleXmlElement_BloxstrapCustomBootstrapper(CustomDialog dialog, XElement xmlElement) - { - xmlElement.SetAttributeValue("Visibility", "Collapsed"); // don't show the bootstrapper yet!!! - xmlElement.SetAttributeValue("IsEnabled", "True"); - HandleXmlElement_Control(dialog, dialog, xmlElement); - - dialog.Opacity = 1; - - // transfer effect to element grid - dialog.ElementGrid.RenderTransform = dialog.RenderTransform; - dialog.RenderTransform = null; - dialog.ElementGrid.LayoutTransform = dialog.LayoutTransform; - dialog.LayoutTransform = null; - - dialog.ElementGrid.Effect = dialog.Effect; - dialog.Effect = null; - - var theme = ParseXmlAttribute(xmlElement, "Theme", Theme.Default); - if (theme == Theme.Default) - theme = App.Settings.Prop.Theme; - - var wpfUiTheme = theme.GetFinal() == Theme.Dark ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light; - - dialog.Resources.MergedDictionaries.Clear(); - dialog.Resources.MergedDictionaries.Add(new ThemesDictionary() { Theme = wpfUiTheme }); - dialog.DefaultBorderThemeOverwrite = wpfUiTheme; - - // disable default window border if border is modified - if (xmlElement.Attribute("BorderBrush") != null || xmlElement.Attribute("BorderThickness") != null) - dialog.DefaultBorderEnabled = false; - - // set the margin & padding on the element grid - dialog.ElementGrid.Margin = dialog.Margin; - // TODO: put elementgrid inside a border? - - dialog.Margin = new Thickness(0, 0, 0, 0); - dialog.Padding = new Thickness(0, 0, 0, 0); - - string? title = xmlElement.Attribute("Title")?.Value?.ToString() ?? "Bloxstrap"; - dialog.Title = title; - - return new DummyFrameworkElement(); - } - - private static UIElement HandleXmlElement_BloxstrapCustomBootstrapper_Fake(CustomDialog dialog, XElement xmlElement) - { - // this only exists to error out the theme if someone tries to use two BloxstrapCustomBootstrappers - throw new Exception($"{xmlElement.Parent!.Name} cannot have a child of {xmlElement.Name}"); - } - - private static DummyFrameworkElement HandleXmlElement_TitleBar(CustomDialog dialog, XElement xmlElement) - { - xmlElement.SetAttributeValue("Name", "TitleBar"); // prevent two titlebars from existing - xmlElement.SetAttributeValue("IsEnabled", "True"); - HandleXmlElement_Control(dialog, dialog.RootTitleBar, xmlElement); - - // get rid of all effects - dialog.RootTitleBar.RenderTransform = null; - dialog.RootTitleBar.LayoutTransform = null; - - dialog.RootTitleBar.Effect = null; - - Panel.SetZIndex(dialog.RootTitleBar, 1001); // always show above others - - // properties we dont want modifiable - dialog.RootTitleBar.Height = double.NaN; - dialog.RootTitleBar.Width = double.NaN; - dialog.RootTitleBar.HorizontalAlignment = HorizontalAlignment.Stretch; - dialog.RootTitleBar.Margin = new Thickness(0, 0, 0, 0); - - dialog.RootTitleBar.ShowMinimize = ParseXmlAttribute(xmlElement, "ShowMinimize", true); - dialog.RootTitleBar.ShowClose = ParseXmlAttribute(xmlElement, "ShowClose", true); - - string? title = xmlElement.Attribute("Title")?.Value?.ToString() ?? "Bloxstrap"; - dialog.RootTitleBar.Title = title; - - return new DummyFrameworkElement(); // dont add anything - } - - 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 Exception($"{xmlElement.Name} can only have one Content defined"); - - if (contentAttr != null) - return GetTranslatedText(contentAttr.Value); - - if (contentElement == null) - return null; - - var children = contentElement.Elements(); - if (children.Count() > 1) - throw new Exception($"{xmlElement.Name}.Content can only have one child"); - - var first = contentElement.FirstNode as XElement; - if (first == null) - throw new Exception($"{xmlElement.Name} Content is missing the content"); - - var uiElement = HandleXml(dialog, first); - return uiElement; - } - - private static UIElement HandleXmlElement_Button(CustomDialog dialog, XElement xmlElement) - { - var button = new Button(); - HandleXmlElement_Control(dialog, button, xmlElement); - - button.Content = GetContentFromXElement(dialog, xmlElement); - - if (xmlElement.Attribute("Name")?.Value == "CancelButton") - { - Binding cancelEnabledBinding = new Binding("CancelEnabled") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(button, Button.IsEnabledProperty, cancelEnabledBinding); - - Binding cancelCommandBinding = new Binding("CancelInstallCommand"); - BindingOperations.SetBinding(button, Button.CommandProperty, cancelCommandBinding); - } - - return button; - } - - private static void HandleXmlElement_RangeBase(CustomDialog dialog, RangeBase rangeBase, XElement xmlElement) - { - HandleXmlElement_Control(dialog, rangeBase, xmlElement); - - rangeBase.Value = ParseXmlAttribute(xmlElement, "Value", 0); - rangeBase.Maximum = ParseXmlAttribute(xmlElement, "Maximum", 100); - } - - private static UIElement HandleXmlElement_ProgressBar(CustomDialog dialog, XElement xmlElement) - { - var progressBar = new Wpf.Ui.Controls.ProgressBar(); - HandleXmlElement_RangeBase(dialog, progressBar, xmlElement); - - progressBar.IsIndeterminate = ParseXmlAttribute(xmlElement, "IsIndeterminate", false); - - object? cornerRadius = GetCornerRadiusFromXElement(xmlElement, "CornerRadius"); - if (cornerRadius != null) - progressBar.CornerRadius = (CornerRadius)cornerRadius; - - object? indicatorCornerRadius = GetCornerRadiusFromXElement(xmlElement, "IndicatorCornerRadius"); - if (indicatorCornerRadius != null) - progressBar.IndicatorCornerRadius = (CornerRadius)indicatorCornerRadius; - - if (xmlElement.Attribute("Name")?.Value == "PrimaryProgressBar") - { - Binding isIndeterminateBinding = new Binding("ProgressIndeterminate") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(progressBar, ProgressBar.IsIndeterminateProperty, isIndeterminateBinding); - - Binding maximumBinding = new Binding("ProgressMaximum") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(progressBar, ProgressBar.MaximumProperty, maximumBinding); - - Binding valueBinding = new Binding("ProgressValue") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(progressBar, ProgressBar.ValueProperty, valueBinding); - } - - return progressBar; - } - - private static UIElement HandleXmlElement_ProgressRing(CustomDialog dialog, XElement xmlElement) - { - var progressBar = new Wpf.Ui.Controls.ProgressRing(); - HandleXmlElement_RangeBase(dialog, progressBar, xmlElement); - - progressBar.IsIndeterminate = ParseXmlAttribute(xmlElement, "IsIndeterminate", false); - - if (xmlElement.Attribute("Name")?.Value == "PrimaryProgressRing") - { - Binding isIndeterminateBinding = new Binding("ProgressIndeterminate") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.IsIndeterminateProperty, isIndeterminateBinding); - - Binding maximumBinding = new Binding("ProgressMaximum") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.MaximumProperty, maximumBinding); - - Binding valueBinding = new Binding("ProgressValue") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.ValueProperty, valueBinding); - } - - return progressBar; - } - - private static void HandleXmlElement_TextBlock_Base(CustomDialog dialog, TextBlock textBlock, XElement xmlElement) - { - HandleXmlElement_FrameworkElement(dialog, textBlock, xmlElement); - - ApplyBrush_UIElement(dialog, textBlock, "Foreground", TextBlock.ForegroundProperty, xmlElement); - - ApplyBrush_UIElement(dialog, textBlock, "Background", TextBlock.BackgroundProperty, xmlElement); - - var fontSize = ParseXmlAttributeNullable(xmlElement, "FontSize"); - if (fontSize is double) - textBlock.FontSize = (double)fontSize; - textBlock.FontWeight = GetFontWeightFromXElement(xmlElement); - textBlock.FontStyle = GetFontStyleFromXElement(xmlElement); - - textBlock.LineHeight = ParseXmlAttribute(xmlElement, "LineHeight", double.NaN); - textBlock.LineStackingStrategy = ParseXmlAttribute(xmlElement, "LineStackingStrategy", LineStackingStrategy.MaxHeight); - - textBlock.TextAlignment = ParseXmlAttribute(xmlElement, "TextAlignment", TextAlignment.Center); - textBlock.TextTrimming = ParseXmlAttribute(xmlElement, "TextTrimming", TextTrimming.None); - textBlock.TextWrapping = ParseXmlAttribute(xmlElement, "TextWrapping", TextWrapping.NoWrap); - textBlock.TextDecorations = GetTextDecorationsFromXElement(xmlElement); - - textBlock.IsHyphenationEnabled = ParseXmlAttribute(xmlElement, "IsHyphenationEnabled", false); - textBlock.BaselineOffset = ParseXmlAttribute(xmlElement, "BaselineOffset", double.NaN); - - // NOTE: font family can both be the name of the font or a uri - string? fontFamily = GetFullPath(dialog, xmlElement.Attribute("FontFamily")?.Value); - if (fontFamily != null) - textBlock.FontFamily = new System.Windows.Media.FontFamily(fontFamily); - - object? padding = GetThicknessFromXElement(xmlElement, "Padding"); - if (padding != null) - textBlock.Padding = (Thickness)padding; - - if (xmlElement.Attribute("Name")?.Value == "StatusText") - { - Binding textBinding = new Binding("Message") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, textBinding); - } - } - - private static UIElement HandleXmlElement_TextBlock(CustomDialog dialog, XElement xmlElement) - { - var textBlock = new TextBlock(); - HandleXmlElement_TextBlock_Base(dialog, textBlock, xmlElement); - - textBlock.Text = GetTranslatedText(xmlElement.Attribute("Text")?.Value); - - return textBlock; - } - - private static UIElement HandleXmlElement_MarkdownTextBlock(CustomDialog dialog, XElement xmlElement) - { - var textBlock = new MarkdownTextBlock(); - HandleXmlElement_TextBlock_Base(dialog, textBlock, xmlElement); - - string? text = GetTranslatedText(xmlElement.Attribute("Text")?.Value); - if (text != null) - textBlock.MarkdownText = text; - - return textBlock; - } - - private static UIElement HandleXmlElement_Image(CustomDialog dialog, XElement xmlElement) - { - var image = new Image(); - HandleXmlElement_FrameworkElement(dialog, image, xmlElement); - - image.Stretch = ParseXmlAttribute(xmlElement, "Stretch", Stretch.Uniform); - image.StretchDirection = ParseXmlAttribute(xmlElement, "StretchDirection", StretchDirection.Both); - - RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality); // should this be modifiable by the user? - - var sourceData = GetImageSourceData(dialog, "Source", xmlElement); - - if (sourceData.IsIcon) - { - // bind the icon property - Binding binding = new Binding("Icon") { Mode = BindingMode.OneWay }; - BindingOperations.SetBinding(image, Image.SourceProperty, binding); - } - else - { - bool isAnimated = ParseXmlAttribute(xmlElement, "IsAnimated", false); - if (!isAnimated) - { - BitmapImage bitmapImage; - try - { - bitmapImage = new BitmapImage(sourceData.Uri!); - } - catch (Exception ex) - { - throw new Exception($"Image Failed to create BitmapImage: {ex.Message}", ex); - } - - image.Source = bitmapImage; - } - else - { - XamlAnimatedGif.AnimationBehavior.SetSourceUri(image, sourceData.Uri!); - } - } - - return image; - } - private static T HandleXml(CustomDialog dialog, XElement xmlElement) where T : class { if (!_elementHandlerMap.ContainsKey(xmlElement.Name.ToString())) @@ -992,7 +93,6 @@ private void HandleXmlBase(XElement xml) foreach (var child in xml.Elements()) AddXml(this, child); } - #endregion #region Public APIs public void ApplyCustomTheme(string name, string contents) diff --git a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs new file mode 100644 index 00000000..3d39e9e4 --- /dev/null +++ b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs @@ -0,0 +1,619 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Media.Effects; +using System.Windows.Shapes; +using System.Xml.Linq; + +using Wpf.Ui.Markup; + +using Bloxstrap.UI.Elements.Controls; + +namespace Bloxstrap.UI.Elements.Bootstrapper +{ + public partial class CustomDialog + { + #region Transformation + private static Transform HandleXmlElement_ScaleTransform(CustomDialog dialog, XElement xmlElement) + { + var st = new ScaleTransform(); + + st.ScaleX = ParseXmlAttribute(xmlElement, "ScaleX", 1); + st.ScaleY = ParseXmlAttribute(xmlElement, "ScaleY", 1); + st.CenterX = ParseXmlAttribute(xmlElement, "CenterX", 0); + st.CenterY = ParseXmlAttribute(xmlElement, "CenterY", 0); + + return st; + } + + private static Transform HandleXmlElement_SkewTransform(CustomDialog dialog, XElement xmlElement) + { + var st = new SkewTransform(); + + st.AngleX = ParseXmlAttribute(xmlElement, "AngleX", 0); + st.AngleY = ParseXmlAttribute(xmlElement, "AngleY", 0); + st.CenterX = ParseXmlAttribute(xmlElement, "CenterX", 0); + st.CenterY = ParseXmlAttribute(xmlElement, "CenterY", 0); + + return st; + } + + private static Transform HandleXmlElement_RotateTransform(CustomDialog dialog, XElement xmlElement) + { + var rt = new RotateTransform(); + + rt.Angle = ParseXmlAttribute(xmlElement, "Angle", 0); + rt.CenterX = ParseXmlAttribute(xmlElement, "CenterX", 0); + rt.CenterY = ParseXmlAttribute(xmlElement, "CenterY", 0); + + return rt; + } + + private static Transform HandleXmlElement_TranslateTransform(CustomDialog dialog, XElement xmlElement) + { + var tt = new TranslateTransform(); + + tt.X = ParseXmlAttribute(xmlElement, "X", 0); + tt.Y = ParseXmlAttribute(xmlElement, "Y", 0); + + return tt; + } + #endregion + + #region Effects + private static BlurEffect HandleXmlElement_BlurEffect(CustomDialog dialog, XElement xmlElement) + { + var effect = new BlurEffect(); + + effect.KernelType = ParseXmlAttribute(xmlElement, "KernelType", KernelType.Gaussian); + effect.Radius = ParseXmlAttribute(xmlElement, "Radius", 5); + effect.RenderingBias = ParseXmlAttribute(xmlElement, "RenderingBias", RenderingBias.Performance); + + return effect; + } + + private static DropShadowEffect HandleXmlElement_DropShadowEffect(CustomDialog dialog, XElement xmlElement) + { + var effect = new DropShadowEffect(); + + effect.BlurRadius = ParseXmlAttribute(xmlElement, "BlurRadius", 5); + effect.Direction = ParseXmlAttribute(xmlElement, "Direction", 315); + effect.Opacity = ParseXmlAttribute(xmlElement, "Opacity", 1); + effect.ShadowDepth = ParseXmlAttribute(xmlElement, "ShadowDepth", 5); + effect.RenderingBias = ParseXmlAttribute(xmlElement, "RenderingBias", RenderingBias.Performance); + + var color = GetColorFromXElement(xmlElement, "Color"); + if (color is Color) + effect.Color = (Color)color; + + return effect; + } + #endregion + + #region Brushes + private static void HandleXml_Brush(Brush brush, XElement xmlElement) + { + brush.Opacity = ParseXmlAttribute(xmlElement, "Opacity", 1.0); + } + + private static Brush HandleXmlElement_SolidColorBrush(CustomDialog dialog, XElement xmlElement) + { + var brush = new SolidColorBrush(); + HandleXml_Brush(brush, xmlElement); + + object? color = GetColorFromXElement(xmlElement, "Color"); + if (color is Color) + brush.Color = (Color)color; + + return brush; + } + + private static Brush HandleXmlElement_ImageBrush(CustomDialog dialog, XElement xmlElement) + { + var imageBrush = new ImageBrush(); + HandleXml_Brush(imageBrush, xmlElement); + + imageBrush.AlignmentX = ParseXmlAttribute(xmlElement, "AlignmentX", AlignmentX.Center); + imageBrush.AlignmentY = ParseXmlAttribute(xmlElement, "AlignmentY", AlignmentY.Center); + + imageBrush.Stretch = ParseXmlAttribute(xmlElement, "Stretch", Stretch.Fill); + imageBrush.TileMode = ParseXmlAttribute(xmlElement, "TileMode", TileMode.None); + + imageBrush.ViewboxUnits = ParseXmlAttribute(xmlElement, "ViewboxUnits", BrushMappingMode.RelativeToBoundingBox); + imageBrush.ViewportUnits = ParseXmlAttribute(xmlElement, "ViewportUnits", BrushMappingMode.RelativeToBoundingBox); + + var viewbox = GetRectFromXElement(xmlElement, "Viewbox"); + if (viewbox is Rect) + imageBrush.Viewbox = (Rect)viewbox; + + var viewport = GetRectFromXElement(xmlElement, "Viewport"); + if (viewport is Rect) + imageBrush.Viewport = (Rect)viewport; + + var sourceData = GetImageSourceData(dialog, "ImageSource", xmlElement); + + if (sourceData.IsIcon) + { + // bind the icon property + Binding binding = new Binding("Icon") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(imageBrush, ImageBrush.ImageSourceProperty, binding); + } + else + { + BitmapImage bitmapImage; + try + { + bitmapImage = new BitmapImage(sourceData.Uri!); + } + catch (Exception ex) + { + throw new Exception($"ImageBrush Failed to create BitmapImage: {ex.Message}", ex); + } + + imageBrush.ImageSource = bitmapImage; + } + + return imageBrush; + } + + private static GradientStop HandleXmlElement_GradientStop(CustomDialog dialog, XElement xmlElement) + { + var gs = new GradientStop(); + + object? color = GetColorFromXElement(xmlElement, "Color"); + if (color is Color) + gs.Color = (Color)color; + + gs.Offset = ParseXmlAttribute(xmlElement, "Offset", 0.0); + + return gs; + } + + private static Brush HandleXmlElement_LinearGradientBrush(CustomDialog dialog, XElement xmlElement) + { + var brush = new LinearGradientBrush(); + HandleXml_Brush(brush, xmlElement); + + object? startPoint = GetPointFromXElement(xmlElement, "StartPoint"); + if (startPoint is Point) + brush.StartPoint = (Point)startPoint; + + object? endPoint = GetPointFromXElement(xmlElement, "EndPoint"); + if (endPoint is Point) + brush.EndPoint = (Point)endPoint; + + brush.ColorInterpolationMode = ParseXmlAttribute(xmlElement, "ColorInterpolationMode", ColorInterpolationMode.SRgbLinearInterpolation); + brush.MappingMode = ParseXmlAttribute(xmlElement, "MappingMode", BrushMappingMode.RelativeToBoundingBox); + brush.SpreadMethod = ParseXmlAttribute(xmlElement, "SpreadMethod", GradientSpreadMethod.Pad); + + foreach (var child in xmlElement.Elements()) + brush.GradientStops.Add(HandleXml(dialog, child)); + + return brush; + } + + private static void ApplyBrush_UIElement(CustomDialog dialog, FrameworkElement uiElement, string name, DependencyProperty dependencyProperty, XElement xmlElement) + { + // check if attribute exists + object? brushAttr = GetBrushFromXElement(xmlElement, name); + if (brushAttr is Brush) + { + uiElement.SetValue(dependencyProperty, brushAttr); + return; + } + else if (brushAttr is string) + { + uiElement.SetResourceReference(dependencyProperty, brushAttr); + return; + } + + // check if element exists + var brushElement = xmlElement.Element($"{xmlElement.Name}.{name}"); + if (brushElement == null) + return; + + var first = brushElement.FirstNode as XElement; + if (first == null) + throw new Exception($"{xmlElement.Name} {name} is missing the brush"); + + var brush = HandleXml(dialog, first); + uiElement.SetValue(dependencyProperty, brush); + } + #endregion + + #region Shapes + private static void HandleXmlElement_Shape(CustomDialog dialog, Shape shape, XElement xmlElement) + { + HandleXmlElement_FrameworkElement(dialog, shape, xmlElement); + + ApplyBrush_UIElement(dialog, shape, "Fill", Shape.FillProperty, xmlElement); + ApplyBrush_UIElement(dialog, shape, "Stroke", Shape.StrokeProperty, xmlElement); + + shape.Stretch = ParseXmlAttribute(xmlElement, "Stretch", Stretch.Fill); + + shape.StrokeDashCap = ParseXmlAttribute(xmlElement, "StrokeDashCap", PenLineCap.Flat); + shape.StrokeDashOffset = ParseXmlAttribute(xmlElement, "StrokeDashOffset", 0); + shape.StrokeEndLineCap = ParseXmlAttribute(xmlElement, "StrokeEndLineCap", PenLineCap.Flat); + shape.StrokeLineJoin = ParseXmlAttribute(xmlElement, "StrokeLineJoin", PenLineJoin.Miter); + shape.StrokeMiterLimit = ParseXmlAttribute(xmlElement, "StrokeMiterLimit", 10); + shape.StrokeStartLineCap = ParseXmlAttribute(xmlElement, "StrokeStartLineCap", PenLineCap.Flat); + shape.StrokeThickness = ParseXmlAttribute(xmlElement, "StrokeThickness", 1); + } + + private static Ellipse HandleXmlElement_Ellipse(CustomDialog dialog, XElement xmlElement) + { + var ellipse = new Ellipse(); + HandleXmlElement_Shape(dialog, ellipse, xmlElement); + + return ellipse; + } + + private static Line HandleXmlElement_Line(CustomDialog dialog, XElement xmlElement) + { + var line = new Line(); + HandleXmlElement_Shape(dialog, line, xmlElement); + + line.X1 = ParseXmlAttribute(xmlElement, "X1", 0); + line.X2 = ParseXmlAttribute(xmlElement, "X2", 0); + line.Y1 = ParseXmlAttribute(xmlElement, "Y1", 0); + line.Y2 = ParseXmlAttribute(xmlElement, "Y2", 0); + + return line; + } + + private static Rectangle HandleXmlElement_Rectangle(CustomDialog dialog, XElement xmlElement) + { + var rectangle = new Rectangle(); + HandleXmlElement_Shape(dialog, rectangle, xmlElement); + + rectangle.RadiusX = ParseXmlAttribute(xmlElement, "RadiusX", 0); + rectangle.RadiusY = ParseXmlAttribute(xmlElement, "RadiusY", 0); + + return rectangle; + } + + #endregion + + #region Elements + private static void HandleXmlElement_FrameworkElement(CustomDialog dialog, FrameworkElement uiElement, XElement xmlElement) + { + // prevent two elements from having the same name + string? name = xmlElement.Attribute("Name")?.Value?.ToString(); + if (name != null) + { + if (dialog.UsedNames.Contains(name)) + throw new Exception($"{xmlElement.Name} has duplicate name {name}"); + + dialog.UsedNames.Add(name); + } + + uiElement.Name = name; + + uiElement.Visibility = ParseXmlAttribute(xmlElement, "Visibility", Visibility.Visible); + uiElement.IsEnabled = ParseXmlAttribute(xmlElement, "IsEnabled", true); + + object? margin = GetThicknessFromXElement(xmlElement, "Margin"); + if (margin != null) + uiElement.Margin = (Thickness)margin; + + uiElement.Height = ParseXmlAttribute(xmlElement, "Height", double.NaN); + uiElement.Width = ParseXmlAttribute(xmlElement, "Width", double.NaN); + + // default values of these were originally Stretch but that was no good + uiElement.HorizontalAlignment = ParseXmlAttribute(xmlElement, "HorizontalAlignment", HorizontalAlignment.Left); + uiElement.VerticalAlignment = ParseXmlAttribute(xmlElement, "VerticalAlignment", VerticalAlignment.Top); + + uiElement.Opacity = ParseXmlAttribute(xmlElement, "Opacity", 1); + ApplyBrush_UIElement(dialog, uiElement, "OpacityMask", FrameworkElement.OpacityMaskProperty, xmlElement); + + object? renderTransformOrigin = GetPointFromXElement(xmlElement, "RenderTransformOrigin"); + if (renderTransformOrigin is Point) + uiElement.RenderTransformOrigin = (Point)renderTransformOrigin; + + int zIndex = ParseXmlAttributeClamped(xmlElement, "ZIndex", defaultValue: 0, min: 0, max: 1000); + Panel.SetZIndex(uiElement, zIndex); + + ApplyTransformations_UIElement(dialog, uiElement, xmlElement); + ApplyEffects_UIElement(dialog, uiElement, xmlElement); + } + + private static void HandleXmlElement_Control(CustomDialog dialog, Control uiElement, XElement xmlElement) + { + HandleXmlElement_FrameworkElement(dialog, uiElement, xmlElement); + + object? padding = GetThicknessFromXElement(xmlElement, "Padding"); + if (padding != null) + uiElement.Padding = (Thickness)padding; + + object? borderThickness = GetThicknessFromXElement(xmlElement, "BorderThickness"); + if (borderThickness != null) + uiElement.BorderThickness = (Thickness)borderThickness; + + ApplyBrush_UIElement(dialog, uiElement, "Foreground", Control.ForegroundProperty, xmlElement); + + ApplyBrush_UIElement(dialog, uiElement, "Background", Control.BackgroundProperty, xmlElement); + + ApplyBrush_UIElement(dialog, uiElement, "BorderBrush", Control.BorderBrushProperty, xmlElement); + + var fontSize = ParseXmlAttributeNullable(xmlElement, "FontSize"); + if (fontSize is double) + uiElement.FontSize = (double)fontSize; + uiElement.FontWeight = GetFontWeightFromXElement(xmlElement); + uiElement.FontStyle = GetFontStyleFromXElement(xmlElement); + + // NOTE: font family can both be the name of the font or a uri + string? fontFamily = GetFullPath(dialog, xmlElement.Attribute("FontFamily")?.Value); + if (fontFamily != null) + uiElement.FontFamily = new System.Windows.Media.FontFamily(fontFamily); + } + + private static UIElement HandleXmlElement_BloxstrapCustomBootstrapper(CustomDialog dialog, XElement xmlElement) + { + xmlElement.SetAttributeValue("Visibility", "Collapsed"); // don't show the bootstrapper yet!!! + xmlElement.SetAttributeValue("IsEnabled", "True"); + HandleXmlElement_Control(dialog, dialog, xmlElement); + + dialog.Opacity = 1; + + // transfer effect to element grid + dialog.ElementGrid.RenderTransform = dialog.RenderTransform; + dialog.RenderTransform = null; + dialog.ElementGrid.LayoutTransform = dialog.LayoutTransform; + dialog.LayoutTransform = null; + + dialog.ElementGrid.Effect = dialog.Effect; + dialog.Effect = null; + + var theme = ParseXmlAttribute(xmlElement, "Theme", Theme.Default); + if (theme == Theme.Default) + theme = App.Settings.Prop.Theme; + + var wpfUiTheme = theme.GetFinal() == Theme.Dark ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light; + + dialog.Resources.MergedDictionaries.Clear(); + dialog.Resources.MergedDictionaries.Add(new ThemesDictionary() { Theme = wpfUiTheme }); + dialog.DefaultBorderThemeOverwrite = wpfUiTheme; + + // disable default window border if border is modified + if (xmlElement.Attribute("BorderBrush") != null || xmlElement.Attribute("BorderThickness") != null) + dialog.DefaultBorderEnabled = false; + + // set the margin & padding on the element grid + dialog.ElementGrid.Margin = dialog.Margin; + // TODO: put elementgrid inside a border? + + dialog.Margin = new Thickness(0, 0, 0, 0); + dialog.Padding = new Thickness(0, 0, 0, 0); + + string? title = xmlElement.Attribute("Title")?.Value?.ToString() ?? "Bloxstrap"; + dialog.Title = title; + + return new DummyFrameworkElement(); + } + + private static UIElement HandleXmlElement_BloxstrapCustomBootstrapper_Fake(CustomDialog dialog, XElement xmlElement) + { + // this only exists to error out the theme if someone tries to use two BloxstrapCustomBootstrappers + throw new Exception($"{xmlElement.Parent!.Name} cannot have a child of {xmlElement.Name}"); + } + + private static DummyFrameworkElement HandleXmlElement_TitleBar(CustomDialog dialog, XElement xmlElement) + { + xmlElement.SetAttributeValue("Name", "TitleBar"); // prevent two titlebars from existing + xmlElement.SetAttributeValue("IsEnabled", "True"); + HandleXmlElement_Control(dialog, dialog.RootTitleBar, xmlElement); + + // get rid of all effects + dialog.RootTitleBar.RenderTransform = null; + dialog.RootTitleBar.LayoutTransform = null; + + dialog.RootTitleBar.Effect = null; + + Panel.SetZIndex(dialog.RootTitleBar, 1001); // always show above others + + // properties we dont want modifiable + dialog.RootTitleBar.Height = double.NaN; + dialog.RootTitleBar.Width = double.NaN; + dialog.RootTitleBar.HorizontalAlignment = HorizontalAlignment.Stretch; + dialog.RootTitleBar.Margin = new Thickness(0, 0, 0, 0); + + dialog.RootTitleBar.ShowMinimize = ParseXmlAttribute(xmlElement, "ShowMinimize", true); + dialog.RootTitleBar.ShowClose = ParseXmlAttribute(xmlElement, "ShowClose", true); + + string? title = xmlElement.Attribute("Title")?.Value?.ToString() ?? "Bloxstrap"; + dialog.RootTitleBar.Title = title; + + return new DummyFrameworkElement(); // dont add anything + } + + private static UIElement HandleXmlElement_Button(CustomDialog dialog, XElement xmlElement) + { + var button = new Button(); + HandleXmlElement_Control(dialog, button, xmlElement); + + button.Content = GetContentFromXElement(dialog, xmlElement); + + if (xmlElement.Attribute("Name")?.Value == "CancelButton") + { + Binding cancelEnabledBinding = new Binding("CancelEnabled") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(button, Button.IsEnabledProperty, cancelEnabledBinding); + + Binding cancelCommandBinding = new Binding("CancelInstallCommand"); + BindingOperations.SetBinding(button, Button.CommandProperty, cancelCommandBinding); + } + + return button; + } + + private static void HandleXmlElement_RangeBase(CustomDialog dialog, RangeBase rangeBase, XElement xmlElement) + { + HandleXmlElement_Control(dialog, rangeBase, xmlElement); + + rangeBase.Value = ParseXmlAttribute(xmlElement, "Value", 0); + rangeBase.Maximum = ParseXmlAttribute(xmlElement, "Maximum", 100); + } + + private static UIElement HandleXmlElement_ProgressBar(CustomDialog dialog, XElement xmlElement) + { + var progressBar = new Wpf.Ui.Controls.ProgressBar(); + HandleXmlElement_RangeBase(dialog, progressBar, xmlElement); + + progressBar.IsIndeterminate = ParseXmlAttribute(xmlElement, "IsIndeterminate", false); + + object? cornerRadius = GetCornerRadiusFromXElement(xmlElement, "CornerRadius"); + if (cornerRadius != null) + progressBar.CornerRadius = (CornerRadius)cornerRadius; + + object? indicatorCornerRadius = GetCornerRadiusFromXElement(xmlElement, "IndicatorCornerRadius"); + if (indicatorCornerRadius != null) + progressBar.IndicatorCornerRadius = (CornerRadius)indicatorCornerRadius; + + if (xmlElement.Attribute("Name")?.Value == "PrimaryProgressBar") + { + Binding isIndeterminateBinding = new Binding("ProgressIndeterminate") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(progressBar, ProgressBar.IsIndeterminateProperty, isIndeterminateBinding); + + Binding maximumBinding = new Binding("ProgressMaximum") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(progressBar, ProgressBar.MaximumProperty, maximumBinding); + + Binding valueBinding = new Binding("ProgressValue") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(progressBar, ProgressBar.ValueProperty, valueBinding); + } + + return progressBar; + } + + private static UIElement HandleXmlElement_ProgressRing(CustomDialog dialog, XElement xmlElement) + { + var progressBar = new Wpf.Ui.Controls.ProgressRing(); + HandleXmlElement_RangeBase(dialog, progressBar, xmlElement); + + progressBar.IsIndeterminate = ParseXmlAttribute(xmlElement, "IsIndeterminate", false); + + if (xmlElement.Attribute("Name")?.Value == "PrimaryProgressRing") + { + Binding isIndeterminateBinding = new Binding("ProgressIndeterminate") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.IsIndeterminateProperty, isIndeterminateBinding); + + Binding maximumBinding = new Binding("ProgressMaximum") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.MaximumProperty, maximumBinding); + + Binding valueBinding = new Binding("ProgressValue") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(progressBar, Wpf.Ui.Controls.ProgressRing.ValueProperty, valueBinding); + } + + return progressBar; + } + + private static void HandleXmlElement_TextBlock_Base(CustomDialog dialog, TextBlock textBlock, XElement xmlElement) + { + HandleXmlElement_FrameworkElement(dialog, textBlock, xmlElement); + + ApplyBrush_UIElement(dialog, textBlock, "Foreground", TextBlock.ForegroundProperty, xmlElement); + + ApplyBrush_UIElement(dialog, textBlock, "Background", TextBlock.BackgroundProperty, xmlElement); + + var fontSize = ParseXmlAttributeNullable(xmlElement, "FontSize"); + if (fontSize is double) + textBlock.FontSize = (double)fontSize; + textBlock.FontWeight = GetFontWeightFromXElement(xmlElement); + textBlock.FontStyle = GetFontStyleFromXElement(xmlElement); + + textBlock.LineHeight = ParseXmlAttribute(xmlElement, "LineHeight", double.NaN); + textBlock.LineStackingStrategy = ParseXmlAttribute(xmlElement, "LineStackingStrategy", LineStackingStrategy.MaxHeight); + + textBlock.TextAlignment = ParseXmlAttribute(xmlElement, "TextAlignment", TextAlignment.Center); + textBlock.TextTrimming = ParseXmlAttribute(xmlElement, "TextTrimming", TextTrimming.None); + textBlock.TextWrapping = ParseXmlAttribute(xmlElement, "TextWrapping", TextWrapping.NoWrap); + textBlock.TextDecorations = GetTextDecorationsFromXElement(xmlElement); + + textBlock.IsHyphenationEnabled = ParseXmlAttribute(xmlElement, "IsHyphenationEnabled", false); + textBlock.BaselineOffset = ParseXmlAttribute(xmlElement, "BaselineOffset", double.NaN); + + // NOTE: font family can both be the name of the font or a uri + string? fontFamily = GetFullPath(dialog, xmlElement.Attribute("FontFamily")?.Value); + if (fontFamily != null) + textBlock.FontFamily = new System.Windows.Media.FontFamily(fontFamily); + + object? padding = GetThicknessFromXElement(xmlElement, "Padding"); + if (padding != null) + textBlock.Padding = (Thickness)padding; + + if (xmlElement.Attribute("Name")?.Value == "StatusText") + { + Binding textBinding = new Binding("Message") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, textBinding); + } + } + + private static UIElement HandleXmlElement_TextBlock(CustomDialog dialog, XElement xmlElement) + { + var textBlock = new TextBlock(); + HandleXmlElement_TextBlock_Base(dialog, textBlock, xmlElement); + + textBlock.Text = GetTranslatedText(xmlElement.Attribute("Text")?.Value); + + return textBlock; + } + + private static UIElement HandleXmlElement_MarkdownTextBlock(CustomDialog dialog, XElement xmlElement) + { + var textBlock = new MarkdownTextBlock(); + HandleXmlElement_TextBlock_Base(dialog, textBlock, xmlElement); + + string? text = GetTranslatedText(xmlElement.Attribute("Text")?.Value); + if (text != null) + textBlock.MarkdownText = text; + + return textBlock; + } + + private static UIElement HandleXmlElement_Image(CustomDialog dialog, XElement xmlElement) + { + var image = new Image(); + HandleXmlElement_FrameworkElement(dialog, image, xmlElement); + + image.Stretch = ParseXmlAttribute(xmlElement, "Stretch", Stretch.Uniform); + image.StretchDirection = ParseXmlAttribute(xmlElement, "StretchDirection", StretchDirection.Both); + + RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality); // should this be modifiable by the user? + + var sourceData = GetImageSourceData(dialog, "Source", xmlElement); + + if (sourceData.IsIcon) + { + // bind the icon property + Binding binding = new Binding("Icon") { Mode = BindingMode.OneWay }; + BindingOperations.SetBinding(image, Image.SourceProperty, binding); + } + else + { + bool isAnimated = ParseXmlAttribute(xmlElement, "IsAnimated", false); + if (!isAnimated) + { + BitmapImage bitmapImage; + try + { + bitmapImage = new BitmapImage(sourceData.Uri!); + } + catch (Exception ex) + { + throw new Exception($"Image Failed to create BitmapImage: {ex.Message}", ex); + } + + image.Source = bitmapImage; + } + else + { + XamlAnimatedGif.AnimationBehavior.SetSourceUri(image, sourceData.Uri!); + } + } + + return image; + } + #endregion + } +} diff --git a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs new file mode 100644 index 00000000..948b4603 --- /dev/null +++ b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs @@ -0,0 +1,295 @@ +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 Exception($"Element {element.Name} is missing the {attributeName} attribute"); + } + + return attribute.Value.ToString(); + } + + private static T ParseXmlAttribute(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 Exception($"Element {element.Name} is missing the {attributeName} attribute"); + } + + T? parsed = ConvertValue(attribute.Value); + if (parsed == null) + throw new Exception($"{element.Name} {attributeName} is not a valid {typeof(T).Name}"); + + return (T)parsed; + } + + /// + /// ParseXmlAttribute but the default value is always null + /// + private static T? ParseXmlAttributeNullable(XElement element, string attributeName) where T : struct + { + var attribute = element.Attribute(attributeName); + + if (attribute == null) + return null; + + T? parsed = ConvertValue(attribute.Value); + if (parsed == null) + throw new Exception($"{element.Name} {attributeName} is not a valid {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 Exception($"{elementName} {attributeName} must be larger than {min}"); + if (max != null && value > max) + throw new Exception($"{elementName} {attributeName} must be smaller than {max}"); + } + + private static void ValidateXmlElement(string elementName, string attributeName, double value, double? min = null, double? max = null) + { + if (min != null && value < min) + throw new Exception($"{elementName} {attributeName} must be larger than {min}"); + if (max != null && value > max) + throw new Exception($"{elementName} {attributeName} must be smaller than {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(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 Exception($"{element.Name} Unknown 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 Exception($"{element.Name} Unknown 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 Exception($"{element.Name} Unknown 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]; + return Strings.ResourceManager.GetStringSafe(resourceName); + } + + private static string? GetFullPath(CustomDialog dialog, string? sourcePath) + { + if (sourcePath == null) + return null; + + 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 Exception($"{xmlElement.Name} failed to parse {name} as Uri"); + + if (result == null) + throw new Exception($"{xmlElement.Name} {name} Uri is null"); + + if (result.Scheme != "file") + throw new Exception($"{xmlElement.Name} {name} uses blacklisted scheme {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 Exception($"{xmlElement.Name} can only have one Content defined"); + + if (contentAttr != null) + return GetTranslatedText(contentAttr.Value); + + if (contentElement == null) + return null; + + var children = contentElement.Elements(); + if (children.Count() > 1) + throw new Exception($"{xmlElement.Name}.Content can only have one child"); + + var first = contentElement.FirstNode as XElement; + if (first == null) + throw new Exception($"{xmlElement.Name} Content is missing the content"); + + var uiElement = HandleXml(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 Exception($"{xmlElement.Name}.Effect can only have one child"); + + var child = children.FirstOrDefault(); + if (child == null) + return; + + Effect effect = HandleXml(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(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); + } + } +}