diff --git a/Bloxstrap/Exceptions/CustomThemeException.cs b/Bloxstrap/Exceptions/CustomThemeException.cs new file mode 100644 index 0000000..68ad0a1 --- /dev/null +++ b/Bloxstrap/Exceptions/CustomThemeException.cs @@ -0,0 +1,60 @@ +using Bloxstrap.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bloxstrap.Exceptions +{ + internal class CustomThemeException : Exception + { + /// + /// The exception message in English (for logging) + /// + public string EnglishMessage { get; } = null!; + + public CustomThemeException(string translationString) + : base(Strings.ResourceManager.GetStringSafe(translationString)) + { + EnglishMessage = Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")); + } + + public CustomThemeException(Exception innerException, string translationString) + : base(Strings.ResourceManager.GetStringSafe(translationString), innerException) + { + EnglishMessage = Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")); + } + + public CustomThemeException(string translationString, params object?[] args) + : base(string.Format(Strings.ResourceManager.GetStringSafe(translationString), args)) + { + EnglishMessage = string.Format(Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")), args); + } + + public CustomThemeException(Exception innerException, string translationString, params object?[] args) + : base(string.Format(Strings.ResourceManager.GetStringSafe(translationString), args), innerException) + { + EnglishMessage = string.Format(Strings.ResourceManager.GetStringSafe(translationString, new CultureInfo("en-GB")), args); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(GetType().ToString()); + + if (!string.IsNullOrEmpty(Message)) + sb.Append($": {Message}"); + + if (!string.IsNullOrEmpty(EnglishMessage) && Message != EnglishMessage) + sb.Append($" ({EnglishMessage})"); + + if (InnerException != null) + sb.Append($"\r\n ---> {InnerException}\r\n "); + + if (StackTrace != null) + sb.Append($"\r\n{StackTrace}"); + + return sb.ToString(); + } + } +} diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 8b4247b..5e08cc0 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -847,6 +847,222 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Custom dialog has already been initialised. + /// + public static string CustomTheme_DialogAlreadyInitialised { + get { + return ResourceManager.GetString("CustomTheme.DialogAlreadyInitialised", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}.{1} uses blacklisted scheme {2}. + /// + public static string CustomTheme_ElementAttributeBlacklistedUriScheme { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeBlacklistedUriScheme", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} has invalid {1}: {2}. + /// + public static string CustomTheme_ElementAttributeConversionError { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeConversionError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1} is not a valid {2}. + /// + public static string CustomTheme_ElementAttributeInvalidType { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeInvalidType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Element {0} is missing the {1} attribute. + /// + public static string CustomTheme_ElementAttributeMissing { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}.{1} is missing it's child. + /// + public static string CustomTheme_ElementAttributeMissingChild { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeMissingChild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}.{1} can only have one child. + /// + public static string CustomTheme_ElementAttributeMultipleChildren { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeMultipleChildren", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} can only have one {1} defined. + /// + public static string CustomTheme_ElementAttributeMultipleDefinitions { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeMultipleDefinitions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1} must be larger than {2}. + /// + public static string CustomTheme_ElementAttributeMustBeLargerThanMin { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeMustBeLargerThanMin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1} must be smaller than {2}. + /// + public static string CustomTheme_ElementAttributeMustBeSmallerThanMax { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeMustBeSmallerThanMax", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}.{1} could not be parsed into a {2}. + /// + public static string CustomTheme_ElementAttributeParseError { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeParseError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}.{1} {2} is null. + /// + public static string CustomTheme_ElementAttributeParseErrorNull { + get { + return ResourceManager.GetString("CustomTheme.ElementAttributeParseErrorNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} cannot have a child of {1}. + /// + public static string CustomTheme_ElementInvalidChild { + get { + return ResourceManager.GetString("CustomTheme.ElementInvalidChild", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} can only have one child. + /// + public static string CustomTheme_ElementMultipleChildren { + get { + return ResourceManager.GetString("CustomTheme.ElementMultipleChildren", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} failed to create {1}: {2}. + /// + public static string CustomTheme_ElementTypeCreationFailed { + get { + return ResourceManager.GetString("CustomTheme.ElementTypeCreationFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Theme XML root is not {0}. + /// + public static string CustomTheme_InvalidRoot { + get { + return ResourceManager.GetString("CustomTheme.InvalidRoot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Custom bootstrappers can only have a maximum of {0} elements, got {1}.. + /// + public static string CustomTheme_TooManyElements { + get { + return ResourceManager.GetString("CustomTheme.TooManyElements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown element {0}. + /// + public static string CustomTheme_UnknownElement { + get { + return ResourceManager.GetString("CustomTheme.UnknownElement", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} Unknown {1} {2}. + /// + public static string CustomTheme_UnknownEnumValue { + get { + return ResourceManager.GetString("CustomTheme.UnknownEnumValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} version is not a number. + /// + public static string CustomTheme_VersionNotNumber { + get { + return ResourceManager.GetString("CustomTheme.VersionNotNumber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} version {1} is not recognised. + /// + public static string CustomTheme_VersionNotRecognised { + get { + return ResourceManager.GetString("CustomTheme.VersionNotRecognised", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} version is not set. + /// + public static string CustomTheme_VersionNotSet { + get { + return ResourceManager.GetString("CustomTheme.VersionNotSet", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} version {1} is no longer supported. + /// + public static string CustomTheme_VersionNotSupported { + get { + return ResourceManager.GetString("CustomTheme.VersionNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to parse the theme file: {0}. + /// + public static string CustomTheme_XMLParseFailed { + get { + return ResourceManager.GetString("CustomTheme.XMLParseFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add Fast Flag. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 2b1fb0f..cd38f6e 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1294,4 +1294,76 @@ Please close any applications that may be using Roblox's files, and relaunch. Simple + + Theme XML root is not {0} + + + Custom dialog has already been initialised + + + Custom bootstrappers can only have a maximum of {0} elements, got {1}. + + + {0} version is not set + + + {0} version is not a number + + + {0} version {1} is no longer supported + + + {0} version {1} is not recognised + + + {0} cannot have a child of {1} + + + Unknown element {0} + + + Failed to parse the theme file: {0} + + + {0} has invalid {1}: {2} + + + Element {0} is missing the {1} attribute + + + {0} {1} is not a valid {2} + + + {0} {1} must be larger than {2} + + + {0} {1} must be smaller than {2} + + + {0} Unknown {1} {2} + + + {0} can only have one {1} defined + + + {0}.{1} can only have one child + + + {0} can only have one child + + + {0}.{1} is missing it's child + + + {0}.{1} could not be parsed into a {2} + + + {0}.{1} {2} is null + + + {0}.{1} uses blacklisted scheme {2} + + + {0} failed to create {1}: {2} + \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Converters.cs b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Converters.cs index a4b7eb7..d329540 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Converters.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Converters.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Windows; using System.Windows.Media; +using System.Xml; using System.Xml.Linq; namespace Bloxstrap.UI.Elements.Bootstrapper @@ -37,7 +38,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper } catch (Exception ex) { - throw new Exception($"{xmlElement.Name} has invalid {attributeName}: {ex.Message}", ex); + throw new CustomThemeException(ex, "CustomTheme.ElementAttributeConversionError", xmlElement.Name, attributeName, ex.Message); } } @@ -82,7 +83,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper } catch (Exception ex) { - throw new Exception($"{element.Name} has invalid {attributeName}: {ex.Message}", ex); + throw new CustomThemeException(ex, "CustomTheme.ElementAttributeConversionError", element.Name, attributeName, ex.Message); } } } diff --git a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs index 1642eb8..02877a3 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs @@ -59,11 +59,11 @@ namespace Bloxstrap.UI.Elements.Bootstrapper private static T HandleXml(CustomDialog dialog, XElement xmlElement) where T : class { if (!_elementHandlerMap.ContainsKey(xmlElement.Name.ToString())) - throw new Exception($"Unknown element {xmlElement.Name}"); + throw new CustomThemeException("CustomTheme.UnknownElement", xmlElement.Name); var element = _elementHandlerMap[xmlElement.Name.ToString()](dialog, xmlElement); if (element is not T) - throw new Exception($"{xmlElement.Parent!.Name} cannot have a child of {xmlElement.Name}"); + throw new CustomThemeException("CustomTheme.ElementInvalidChild", xmlElement.Parent!.Name, xmlElement.Name); return (T)element; } @@ -81,34 +81,34 @@ namespace Bloxstrap.UI.Elements.Bootstrapper private static void AssertThemeVersion(string? versionStr) { if (string.IsNullOrEmpty(versionStr)) - throw new Exception("BloxstrapCustomBootstrapper version is not set"); + throw new CustomThemeException("CustomTheme.VersionNotSet", "BloxstrapCustomBootstrapper"); if (!uint.TryParse(versionStr, out uint version)) - throw new Exception("BloxstrapCustomBootstrapper version is not a number"); + throw new CustomThemeException("CustomTheme.VersionNotNumber", "BloxstrapCustomBootstrapper"); switch (version) { case Version: break; case 0: // Themes made between Oct 19, 2024 to Mar 11, 2025 (on the feature/custom-bootstrappers branch) - throw new Exception($"BloxstrapCustomBootstrapper version {version} is no longer supported"); + throw new CustomThemeException("CustomTheme.VersionNotSupported", "BloxstrapCustomBootstrapper", version); default: - throw new Exception($"BloxstrapCustomBootstrapper version {version} is not recognised"); + throw new CustomThemeException("CustomTheme.VersionNotRecognised", "BloxstrapCustomBootstrapper", version); } } private void HandleXmlBase(XElement xml) { if (_initialised) - throw new Exception("Custom dialog has already been initialised"); + throw new CustomThemeException("CustomTheme.DialogAlreadyInitialised"); if (xml.Name != "BloxstrapCustomBootstrapper") - throw new Exception("XML root is not a BloxstrapCustomBootstrapper"); + throw new CustomThemeException("CustomTheme.InvalidRoot", "BloxstrapCustomBootstrapper"); AssertThemeVersion(xml.Attribute("Version")?.Value); if (xml.Descendants().Count() > MaxElements) - throw new Exception($"Custom bootstrappers can have a maximum of {MaxElements} elements"); + throw new CustomThemeException("CustomTheme.TooManyElements", MaxElements, xml.Descendants().Count()); _initialised = true; @@ -134,7 +134,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper } catch (Exception ex) { - throw new Exception($"XML parse failed: {ex.Message}", ex); + throw new CustomThemeException(ex, "CustomTheme.XMLParseFailed", ex.Message); } HandleXmlBase(xml); diff --git a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs index d5e8b19..5b01fc7 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs @@ -150,7 +150,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper } catch (Exception ex) { - throw new Exception($"ImageBrush Failed to create BitmapImage: {ex.Message}", ex); + throw new CustomThemeException(ex, "CustomTheme.ElementTypeCreationFailed", "Image", "BitmapImage", ex.Message); } imageBrush.ImageSource = bitmapImage; @@ -217,7 +217,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper var first = brushElement.FirstNode as XElement; if (first == null) - throw new Exception($"{xmlElement.Name} {name} is missing the brush"); + throw new CustomThemeException("CustomTheme.ElementAttributeMissingChild", xmlElement.Name, name); var brush = HandleXml(dialog, first); uiElement.SetValue(dependencyProperty, brush); @@ -620,7 +620,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper } catch (Exception ex) { - throw new Exception($"Image Failed to create BitmapImage: {ex.Message}", ex); + throw new CustomThemeException(ex, "CustomTheme.ElementTypeCreationFailed", "Image", "BitmapImage", ex.Message); } image.Source = bitmapImage; @@ -693,7 +693,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper if (element.Name == "Grid.RowDefinitions") { if (rowsSet) - throw new Exception("Grid can only have one RowDefinitions defined"); + throw new CustomThemeException("CustomTheme.ElementAttributeMultipleDefinitions", "Grid", "RowDefinitions"); rowsSet = true; HandleXmlElement_Grid_RowDefinitions(grid, dialog, element); @@ -701,7 +701,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper else if (element.Name == "Grid.ColumnDefinitions") { if (columnsSet) - throw new Exception("Grid can only have one ColumnDefinitions defined"); + throw new CustomThemeException("CustomTheme.ElementAttributeMultipleDefinitions", "Grid", "ColumnDefinitions"); columnsSet = true; HandleXmlElement_Grid_ColumnDefinitions(grid, dialog, element); @@ -760,7 +760,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper if (children.Any()) { if (children.Count() > 1) - throw new Exception("Border can only have one child"); + throw new CustomThemeException("CustomTheme.ElementMultipleChildren", "Border"); border.Child = HandleXml(dialog, children.First()); } diff --git a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs index 56875b3..ce93d92 100644 --- a/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs +++ b/Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs @@ -26,7 +26,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper if (defaultValue != null) return defaultValue; - throw new Exception($"Element {element.Name} is missing the {attributeName} attribute"); + throw new CustomThemeException("CustomTheme.ElementAttributeMissing", element.Name, attributeName); } return attribute.Value.ToString(); @@ -41,12 +41,12 @@ namespace Bloxstrap.UI.Elements.Bootstrapper if (defaultValue != null) return (T)defaultValue; - throw new Exception($"Element {element.Name} is missing the {attributeName} attribute"); + throw new CustomThemeException("CustomTheme.ElementAttributeMissing", element.Name, attributeName); } T? parsed = ConvertValue(attribute.Value); if (parsed == null) - throw new Exception($"{element.Name} {attributeName} is not a valid {typeof(T).Name}"); + throw new CustomThemeException("CustomTheme.ElementAttributeInvalidType", element.Name, attributeName, typeof(T).Name); return (T)parsed; } @@ -63,7 +63,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper T? parsed = ConvertValue(attribute.Value); if (parsed == null) - throw new Exception($"{element.Name} {attributeName} is not a valid {typeof(T).Name}"); + throw new CustomThemeException("CustomTheme.ElementAttributeInvalidType", element.Name, attributeName, typeof(T).Name); return (T)parsed; } @@ -71,17 +71,17 @@ namespace Bloxstrap.UI.Elements.Bootstrapper 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}"); + throw new CustomThemeException("CustomTheme.ElementAttributeMustBeLargerThanMin", elementName, attributeName, min); if (max != null && value > max) - throw new Exception($"{elementName} {attributeName} must be smaller than {max}"); + throw new CustomThemeException("CustomTheme.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 Exception($"{elementName} {attributeName} must be larger than {min}"); + throw new CustomThemeException("CustomTheme.ElementAttributeMustBeLargerThanMin", elementName, attributeName, min); if (max != null && value > max) - throw new Exception($"{elementName} {attributeName} must be smaller than {max}"); + throw new CustomThemeException("CustomTheme.ElementAttributeMustBeSmallerThanMax", elementName, attributeName, max); } // You can't do numeric only generics in .NET 6. The feature is exclusive to .NET 7+. @@ -136,7 +136,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper return FontWeights.UltraBlack; default: - throw new Exception($"{element.Name} Unknown FontWeight {value}"); + throw new CustomThemeException("CustomTheme.UnknownEnumValue", element.Name, "FontWeight", value); } } @@ -158,7 +158,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper return FontStyles.Oblique; default: - throw new Exception($"{element.Name} Unknown FontStyle {value}"); + throw new CustomThemeException("CustomTheme.UnknownEnumValue", element.Name, "FontStyle", value); } } @@ -183,7 +183,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper return TextDecorations.Underline; default: - throw new Exception($"{element.Name} Unknown TextDecorations {value}"); + throw new CustomThemeException("CustomTheme.UnknownEnumValue", element.Name, "TextDecorations", value); } } @@ -205,6 +205,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper if (sourcePath == null) return null; + // TODO: this is bad :( return sourcePath.Replace("theme://", $"{dialog.ThemeDir}\\"); } @@ -218,13 +219,13 @@ namespace Bloxstrap.UI.Elements.Bootstrapper path = GetFullPath(dialog, path)!; if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out Uri? result)) - throw new Exception($"{xmlElement.Name} failed to parse {name} as Uri"); + throw new CustomThemeException("CustomTheme.ElementAttributeParseError", xmlElement.Name, name, "Uri"); if (result == null) - throw new Exception($"{xmlElement.Name} {name} Uri is null"); + throw new CustomThemeException("CustomTheme.ElementAttributeParseErrorNull", xmlElement.Name, name, "Uri"); if (result.Scheme != "file") - throw new Exception($"{xmlElement.Name} {name} uses blacklisted scheme {result.Scheme}"); + throw new CustomThemeException("CustomTheme.ElementAttributeBlacklistedUriScheme", xmlElement.Name, name, result.Scheme); return new GetImageSourceDataResult { Uri = result }; } @@ -234,7 +235,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper 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"); + throw new CustomThemeException("CustomTheme.ElementAttributeMultipleDefinitions", xmlElement.Name, "Content"); if (contentAttr != null) return GetTranslatedText(contentAttr.Value); @@ -244,11 +245,11 @@ namespace Bloxstrap.UI.Elements.Bootstrapper var children = contentElement.Elements(); if (children.Count() > 1) - throw new Exception($"{xmlElement.Name}.Content can only have one child"); + throw new CustomThemeException("CustomTheme.ElementAttributeMultipleChildren", xmlElement.Name, "Content"); var first = contentElement.FirstNode as XElement; if (first == null) - throw new Exception($"{xmlElement.Name} Content is missing the content"); + throw new CustomThemeException("CustomTheme.ElementAttributeMissingChild", xmlElement.Name, "Content"); var uiElement = HandleXml(dialog, first); return uiElement; @@ -262,7 +263,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper var children = effectElement.Elements(); if (children.Count() > 1) - throw new Exception($"{xmlElement.Name}.Effect can only have one child"); + throw new CustomThemeException("CustomTheme.ElementAttributeMultipleChildren", xmlElement.Name, "Effect"); var child = children.FirstOrDefault(); if (child == null)