Custom bootstrapper themes (#4380)
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

* 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
This commit is contained in:
Matt 2025-03-11 19:18:54 +00:00 committed by GitHub
parent 33243bfd0a
commit 9d356b0b71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 4753 additions and 21 deletions

View File

@ -11,6 +11,8 @@
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary />
<ResourceDictionary x:Name="CustomTheme" Source="UI/Style/Dark.xaml" /> <!-- NOTE: WpfUiWindow::ApplyTheme relies on this order. If you plan to change the order, please update the index in the function. -->
<ResourceDictionary x:Name="Default" Source="UI/Style/Default.xaml" />
</ResourceDictionary.MergedDictionaries>
<FontFamily x:Key="Rubik">pack://application:,,,/Resources/Fonts/#Rubik Light</FontFamily>

View File

@ -25,9 +25,14 @@
<Resource Include="Resources\MessageBox\Information.png" />
<Resource Include="Resources\MessageBox\Question.png" />
<Resource Include="Resources\MessageBox\Warning.png" />
<EmbeddedResource Include="UI\Style\Editor-Theme-Dark.xshd" />
<EmbeddedResource Include="UI\Style\Editor-Theme-Light.xshd" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\CustomBootstrapperSchema.json" />
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate_Blank.xml" />
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate_Simple.xml" />
<EmbeddedResource Include="Resources\Icon2008.ico" />
<EmbeddedResource Include="Resources\Icon2011.ico" />
<EmbeddedResource Include="Resources\Icon2017.ico" />
@ -49,6 +54,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Markdig" Version="0.40.0" />
@ -59,6 +65,7 @@
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Resources.ResourceManager" Version="4.3.0" />
<PackageReference Include="XamlAnimatedGif" Version="2.3.0" />
</ItemGroup>
<ItemGroup>

View File

@ -10,6 +10,7 @@
ByfronDialog,
[EnumName(StaticName = "Bloxstrap")]
FluentDialog,
FluentAeroDialog
FluentAeroDialog,
CustomDialog
}
}

View File

@ -0,0 +1,8 @@
namespace Bloxstrap.Enums
{
public enum CustomThemeTemplate
{
Blank,
Simple
}
}

View File

@ -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
{
/// <summary>
/// The exception message in English (for logging)
/// </summary>
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();
}
}
}

View File

@ -13,7 +13,8 @@
BootstrapperStyle.ProgressDialog,
BootstrapperStyle.LegacyDialog2011,
BootstrapperStyle.LegacyDialog2008,
BootstrapperStyle.VistaDialog
BootstrapperStyle.VistaDialog,
BootstrapperStyle.CustomDialog
};
}
}

View File

@ -0,0 +1,10 @@
namespace Bloxstrap.Extensions
{
static class CustomThemeTemplateEx
{
public static string GetFileName(this CustomThemeTemplate template)
{
return $"CustomBootstrapperTemplate_{template}.xml";
}
}
}

View File

@ -17,6 +17,7 @@ namespace Bloxstrap.Models.Persistable
public bool UseFastFlagManager { get; set; } = true;
public bool WPFSoftwareRender { get; set; } = false;
public bool EnableAnalytics { get; set; } = true;
public string? SelectedCustomTheme { get; set; } = null;
// integration configuration
public bool EnableActivityTracking { get; set; } = true;

View File

@ -22,6 +22,7 @@
public static string Integrations { get; private set; } = "";
public static string Versions { get; private set; } = "";
public static string Modifications { get; private set; } = "";
public static string CustomThemes { get; private set; } = "";
public static string Application { get; private set; } = "";
@ -37,6 +38,7 @@
Integrations = Path.Combine(Base, "Integrations");
Versions = Path.Combine(Base, "Versions");
Modifications = Path.Combine(Base, "Modifications");
CustomThemes = Path.Combine(Base, "CustomThemes");
Application = Path.Combine(Base, $"{App.ProjectName}.exe");
}

View File

@ -21,5 +21,10 @@ namespace Bloxstrap
await stream.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
public static async Task<string> GetString(string name)
{
return Encoding.UTF8.GetString(await Get(name));
}
}
}

View File

@ -0,0 +1,521 @@
{
"Elements": {
"FrameworkElement": {
"IsCreatable": false,
"Attributes": {
"Name": "string",
"Visibility": "Visibility",
"IsEnabled": "bool",
"Margin": "Thickness",
"Height": "double",
"Width": "double",
"HorizontalAlignment": "HorizontalAlignment",
"VerticalAlignment": "VerticalAlignment",
"RenderTransform": "Transform",
"LayoutTransform": "Transform",
"Opacity": "double",
"OpacityMask": "Brush",
"RenderTransformOrigin": "Point",
"Panel.ZIndex": "int",
"Grid.Row": "int",
"Grid.RowSpan": "int",
"Grid.Column": "int",
"Grid.ColumnSpan": "int"
}
},
"Control": {
"SuperClass": "FrameworkElement",
"IsCreatable": false,
"Attributes": {
"Padding": "Thickness",
"BorderThickness": "Thickness",
"Foreground": "Brush",
"Background": "Brush",
"BorderBrush": "Brush",
"FontSize": "double",
"FontWeight": "FontWeight",
"FontStyle": "FontStyle",
"FontFamily": "FontFamily"
}
},
"BloxstrapCustomBootstrapper": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"Version": "int",
"Theme": "Theme",
"Title": "string",
"IgnoreTitleBarInset": "bool",
"WindowCornerPreference": "WindowCornerPreference"
}
},
"TitleBar": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"ShowMinimize": "bool",
"ShowClose": "bool",
"Title": "string"
}
},
"Button": {
"SuperClass": "Control",
"IsCreatable": true,
"Attributes": {
"Content": "object"
}
},
"RangeBase": {
"SuperClass": "Control",
"IsCreatable": false,
"Attributes": {
"Value": "double",
"Maximum": "double"
}
},
"ProgressBar": {
"SuperClass": "RangeBase",
"IsCreatable": true,
"Attributes": {
"IsIndeterminate": "bool",
"CornerRadius": "CornerRadius",
"IndicatorCornerRadius": "CornerRadius"
}
},
"ProgressRing": {
"SuperClass": "RangeBase",
"IsCreatable": true,
"Attributes": {
"IsIndeterminate": "bool"
}
},
"TextBlock": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Text": "string",
"Foreground": "Brush",
"Background": "Brush",
"FontSize": "double",
"FontWeight": "FontWeight",
"FontStyle": "FontStyle",
"FontFamily": "FontFamily",
"LineHeight": "double",
"LineStackingStrategy": "LineStackingStrategy",
"TextAlignment": "TextAlignment",
"TextTrimming": "TextTrimming",
"TextWrapping": "TextWrapping",
"TextDecorations": "TextDecorations",
"IsHyphenationEnabled": "bool",
"BaselineOffset": "double",
"Padding": "Thickness"
}
},
"MarkdownTextBlock": {
"SuperClass": "TextBlock",
"IsCreatable": true,
"Attributes": {}
},
"Image": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Stretch": "Stretch",
"StretchDirection": "StretchDirection",
"Source": "ImageSource",
"IsAnimated": "bool"
}
},
"Grid": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"RowDefinitions": "object",
"ColumnDefinitions": "object"
}
},
"StackPanel": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Orientation": "Orientation"
}
},
"Border": {
"SuperClass": "FrameworkElement",
"IsCreatable": true,
"Attributes": {
"Background": "Brush",
"BorderBrush": "Brush",
"BorderThickness": "Thickness",
"Padding": "Thickness",
"CornerRadius": "CornerRadius"
}
},
"RowDefinition": {
"IsCreatable": true,
"Attributes": {
"Height": "GridLength",
"MinHeight": "double",
"MaxHeight": "double"
}
},
"ColumnDefinition": {
"IsCreatable": true,
"Attributes": {
"Width": "GridLength",
"MinWidth": "double",
"MaxWidth": "double"
}
},
"ScaleTransform": {
"IsCreatable": true,
"Attributes": {
"ScaleX": "double",
"ScaleY": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"SkewTransform": {
"IsCreatable": true,
"Attributes": {
"AngleX": "double",
"AngleY": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"RotateTransform": {
"IsCreatable": true,
"Attributes": {
"Angle": "double",
"CenterX": "double",
"CenterY": "double"
}
},
"TranslateTransform": {
"IsCreatable": true,
"Attributes": {
"X": "double",
"Y": "double"
}
},
"Brush": {
"IsCreatable": false,
"Attributes": {
"Opacity": "double"
}
},
"SolidColorBrush": {
"SuperClass": "Brush",
"IsCreatable": true,
"Attributes": {
"Color": "Color"
}
},
"ImageBrush": {
"SuperClass": "Brush",
"IsCreatable": true,
"Attributes": {
"AlignmentX": "AlignmentX",
"AlignmentY": "AlignmentY",
"Stretch": "Stretch",
"TileMode": "TileMode",
"ViewboxUnits": "BrushMappingMode",
"ViewportUnits": "BrushMappingMode",
"Viewbox": "Rect",
"Viewport": "Rect",
"ImageSource": "ImageSource"
}
},
"LinearGradientBrush": {
"SuperClass": "Brush",
"IsCreatable": true,
"Attributes": {
"StartPoint": "Point",
"EndPoint": "Point",
"ColorInterpolationMode": "ColorInterpolationMode",
"MappingMode": "BrushMappingMode",
"SpreadMethod": "GradientSpreadMethod"
}
},
"GradientStop": {
"IsCreatable": true,
"Attributes": {
"Color": "Color",
"Offset": "double"
}
},
"Shape": {
"SuperClass": "FrameworkElement",
"IsCreatable": false,
"Attributes": {
"Fill": "Brush",
"Stroke": "Brush",
"Stretch": "Stretch",
"StrokeDashCap": "PenLineCap",
"StrokeDashOffset": "double",
"StrokeEndLineCap": "PenLineCap",
"StrokeLineJoin": "PenLineJoin",
"StrokeMiterLimit": "double",
"StrokeStartLineCap": "PenLineCap",
"StrokeThickness": "double"
}
},
"Ellipse": {
"SuperClass": "Shape",
"IsCreatable": true,
"Attributes": {}
},
"Line": {
"SuperClass": "Shape",
"IsCreatable": true,
"Attributes": {
"X1": "double",
"X2": "double",
"Y1": "double",
"Y2": "double"
}
},
"Rectangle": {
"SuperClass": "Shape",
"IsCreatable": true,
"Attributes": {
"RadiusX": "double",
"RadiusY": "double"
}
},
"BlurEffect": {
"IsCreatable": true,
"Attributes": {
"KernelType": "KernelType",
"Radius": "double",
"RenderingBias": "RenderingBias"
}
},
"DropShadowEffect": {
"IsCreatable": true,
"Attributes": {
"BlurRadius": "double",
"Direction": "double",
"Opacity": "double",
"ShadowDepth": "double",
"RenderingBias": "RenderingBias",
"Color": "Color"
}
}
},
"Types": {
"string": {},
"bool": {
"Values": [
"True",
"False"
]
},
"int": {},
"double": {},
"object": { "CanHaveElement": true },
"Thickness": {},
"Rect": {},
"Point": {},
"CornerRadius": {},
"Brush": { "CanHaveElement": true },
"Color": {},
"ImageSource": {},
"Transform": { "CanHaveElement": true },
"FontFamily": {},
"GridLength": {},
"Visibility": {
"Values": [
"Visible",
"Hidden",
"Collapsed"
]
},
"HorizontalAlignment": {
"Values": [
"Left",
"Center",
"Right",
"Stretch"
]
},
"VerticalAlignment": {
"Values": [
"Top",
"Center",
"Bottom",
"Stretch"
]
},
"Theme": {
"Values": [
"Default",
"Dark",
"Light"
]
},
"FontWeight": {
"Values": [
"Thin",
"ExtraLight",
"UltraLight",
"Medium",
"Normal",
"Regular",
"DemiBold",
"SemiBold",
"Bold",
"ExtraBold",
"UltraBold",
"Black",
"Heavy",
"ExtraBlack",
"ExtraHeavy"
]
},
"FontStyle": {
"Values": [
"Normal",
"Italic",
"Oblique"
]
},
"LineStackingStrategy": {
"Values": [
"BlockLineHeight",
"MaxHeight"
]
},
"TextAlignment": {
"Values": [
"Left",
"Right",
"Center",
"Justify"
]
},
"TextTrimming": {
"Values": [
"None",
"CharacterEllipsis",
"WordEllipsis"
]
},
"TextWrapping": {
"Values": [
"WrapWithOverflow",
"NoWrap",
"Wrap"
]
},
"TextDecorations": {
"Values": [
"Baseline",
"OverLine",
"Strikethrough",
"Underline"
]
},
"Stretch": {
"Values": [
"None",
"Fill",
"Uniform",
"UniformToFill"
]
},
"StretchDirection": {
"Values": [
"UpOnly",
"DownOnly",
"Both"
]
},
"AlignmentX": {
"Values": [
"Left",
"Center",
"Right"
]
},
"AlignmentY": {
"Values": [
"Top",
"Center",
"Bottom"
]
},
"TileMode": {
"Values": [
"None",
"FlipX",
"FlipY",
"FlipXY",
"Tile"
]
},
"BrushMappingMode": {
"Values": [
"Absolute",
"RelativeToBoundingBox"
]
},
"ColorInterpolationMode": {
"Values": [
"ScRgbLinearInterpolation",
"SRgbLinearInterpolation"
]
},
"GradientSpreadMethod": {
"Values": [
"Pad",
"Reflect",
"Repeat"
]
},
"PenLineCap": {
"Values": [
"Flat",
"Square",
"Round",
"Triangle"
]
},
"PenLineJoin": {
"Values": [
"Miter",
"Bevel",
"Round"
]
},
"KernelType": {
"Values": [
"Gaussian",
"Box"
]
},
"RenderingBias": {
"Values": [
"Performance",
"Quality"
]
},
"Orientation": {
"Values": [
"Horizontal",
"Vertical"
]
},
"WindowCornerPreference": {
"Values": [
"Default",
"DoNotRound",
"Round",
"RoundSmall"
]
}
}
}

View File

@ -0,0 +1,4 @@
<BloxstrapCustomBootstrapper Version="1" Height="320" Width="500">
<!-- Put UI elements here -->
<!-- Examples of custom bootstrappers can be found at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
</BloxstrapCustomBootstrapper>

View File

@ -0,0 +1,9 @@
<BloxstrapCustomBootstrapper Version="1" Height="320" Width="520" IgnoreTitleBarInset="True" Theme="Default" Margin="30">
<!-- Find more custom bootstrapper examples at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
<TitleBar Title="" ShowMinimize="False" ShowClose="False" />
<Image Source="{Icon}" Height="100" Width="100" HorizontalAlignment="Center" Margin="0,15,0,0" />
<TextBlock HorizontalAlignment="Center" Name="StatusText" FontSize="20" Margin="0,170,0,0" />
<ProgressBar Width="450" Height="12" Name="PrimaryProgressBar" HorizontalAlignment="Center" Margin="0,200,0,0" />
<Button Content="Cancel" Name="CancelButton" HorizontalAlignment="Center" Margin="0,225,0,0" Height="30" Width="100" />
</BloxstrapCustomBootstrapper>

View File

@ -441,6 +441,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Create New.
/// </summary>
public static string Common_CreateNew {
get {
return ResourceManager.GetString("Common.CreateNew", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom.
/// </summary>
@ -477,6 +486,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
public static string Common_Edit {
get {
return ResourceManager.GetString("Common.Edit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Export.
/// </summary>
@ -495,6 +513,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Import.
/// </summary>
public static string Common_Import {
get {
return ResourceManager.GetString("Common.Import", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import from file.
/// </summary>
@ -630,6 +657,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Rename.
/// </summary>
public static string Common_Rename {
get {
return ResourceManager.GetString("Common.Rename", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reset.
/// </summary>
@ -684,6 +720,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Template.
/// </summary>
public static string Common_Template {
get {
return ResourceManager.GetString("Common.Template", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Value.
/// </summary>
@ -847,6 +892,385 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to File must be a ZIP.
/// </summary>
public static string CustomTheme_Add_Errors_FileNotZip {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.FileNotZip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name cannot be empty.
/// </summary>
public static string CustomTheme_Add_Errors_NameEmpty {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.NameEmpty", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name contains illegal characters.
/// </summary>
public static string CustomTheme_Add_Errors_NameIllegalCharacters {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.NameIllegalCharacters", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name cannot be used.
/// </summary>
public static string CustomTheme_Add_Errors_NameReserved {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.NameReserved", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name is already in use.
/// </summary>
public static string CustomTheme_Add_Errors_NameTaken {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.NameTaken", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown error.
/// </summary>
public static string CustomTheme_Add_Errors_Unknown {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.Unknown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid or corrupted ZIP file.
/// </summary>
public static string CustomTheme_Add_Errors_ZipInvalidData {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.ZipInvalidData", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Theme file could not be found in the ZIP file.
/// </summary>
public static string CustomTheme_Add_Errors_ZipMissingThemeFile {
get {
return ResourceManager.GetString("CustomTheme.Add.Errors.ZipMissingThemeFile", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add Custom Theme.
/// </summary>
public static string CustomTheme_Add_Title {
get {
return ResourceManager.GetString("CustomTheme.Add.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save changes to {0}?.
/// </summary>
public static string CustomTheme_Editor_ConfirmSave {
get {
return ResourceManager.GetString("CustomTheme.Editor.ConfirmSave", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Theme Directory.
/// </summary>
public static string CustomTheme_Editor_OpenThemeDirectory {
get {
return ResourceManager.GetString("CustomTheme.Editor.OpenThemeDirectory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preview.
/// </summary>
public static string CustomTheme_Editor_Preview {
get {
return ResourceManager.GetString("CustomTheme.Editor.Preview", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save.
/// </summary>
public static string CustomTheme_Editor_Save {
get {
return ResourceManager.GetString("CustomTheme.Editor.Save", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An error occurred while saving your theme..
/// </summary>
public static string CustomTheme_Editor_Save_Error {
get {
return ResourceManager.GetString("CustomTheme.Editor.Save.Error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Theme successfully saved!.
/// </summary>
public static string CustomTheme_Editor_Save_Success {
get {
return ResourceManager.GetString("CustomTheme.Editor.Save.Success", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Editing &quot;{0}&quot;.
/// </summary>
public static string CustomTheme_Editor_Title {
get {
return ResourceManager.GetString("CustomTheme.Editor.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom dialog has already been initialised.
/// </summary>
public static string CustomTheme_Errors_DialogAlreadyInitialised {
get {
return ResourceManager.GetString("CustomTheme.Errors.DialogAlreadyInitialised", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} uses blacklisted scheme {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeBlacklistedUriScheme {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeBlacklistedUriScheme", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} has invalid {1}: {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeConversionError {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeConversionError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} {1} is not a valid {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeInvalidType {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeInvalidType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Element {0} is missing the {1} attribute.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMissing {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMissing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} is missing it&apos;s child.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMissingChild {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMissingChild", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} can only have one child.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMultipleChildren {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMultipleChildren", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} can only have one {1} defined.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMultipleDefinitions {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMultipleDefinitions", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} {1} must be larger than {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMustBeLargerThanMin {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMustBeLargerThanMin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} {1} must be smaller than {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeMustBeSmallerThanMax {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} could not be parsed into a {2}.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeParseError {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeParseError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}.{1} {2} is null.
/// </summary>
public static string CustomTheme_Errors_ElementAttributeParseErrorNull {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementAttributeParseErrorNull", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} cannot have a child of {1}.
/// </summary>
public static string CustomTheme_Errors_ElementInvalidChild {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementInvalidChild", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} can only have one child.
/// </summary>
public static string CustomTheme_Errors_ElementMultipleChildren {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementMultipleChildren", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} failed to create {1}: {2}.
/// </summary>
public static string CustomTheme_Errors_ElementTypeCreationFailed {
get {
return ResourceManager.GetString("CustomTheme.Errors.ElementTypeCreationFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Theme XML root is not {0}.
/// </summary>
public static string CustomTheme_Errors_InvalidRoot {
get {
return ResourceManager.GetString("CustomTheme.Errors.InvalidRoot", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No custom theme selected.
/// </summary>
public static string CustomTheme_Errors_NoThemeSelected {
get {
return ResourceManager.GetString("CustomTheme.Errors.NoThemeSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to setup custom bootstrapper: {0}.
///Defaulting to Fluent..
/// </summary>
public static string CustomTheme_Errors_SetupFailed {
get {
return ResourceManager.GetString("CustomTheme.Errors.SetupFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Custom bootstrappers can only have a maximum of {0} elements, got {1}..
/// </summary>
public static string CustomTheme_Errors_TooManyElements {
get {
return ResourceManager.GetString("CustomTheme.Errors.TooManyElements", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown element {0}.
/// </summary>
public static string CustomTheme_Errors_UnknownElement {
get {
return ResourceManager.GetString("CustomTheme.Errors.UnknownElement", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} Unknown {1} {2}.
/// </summary>
public static string CustomTheme_Errors_UnknownEnumValue {
get {
return ResourceManager.GetString("CustomTheme.Errors.UnknownEnumValue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} version is not a number.
/// </summary>
public static string CustomTheme_Errors_VersionNotNumber {
get {
return ResourceManager.GetString("CustomTheme.Errors.VersionNotNumber", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} version {1} is not recognised.
/// </summary>
public static string CustomTheme_Errors_VersionNotRecognised {
get {
return ResourceManager.GetString("CustomTheme.Errors.VersionNotRecognised", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} version is not set.
/// </summary>
public static string CustomTheme_Errors_VersionNotSet {
get {
return ResourceManager.GetString("CustomTheme.Errors.VersionNotSet", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} version {1} is no longer supported.
/// </summary>
public static string CustomTheme_Errors_VersionNotSupported {
get {
return ResourceManager.GetString("CustomTheme.Errors.VersionNotSupported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to parse the theme file: {0}.
/// </summary>
public static string CustomTheme_Errors_XMLParseFailed {
get {
return ResourceManager.GetString("CustomTheme.Errors.XMLParseFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Add Fast Flag.
/// </summary>
@ -1143,6 +1567,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Custom.
/// </summary>
public static string Enums_BootstrapperStyle_CustomDialog {
get {
return ResourceManager.GetString("Enums.BootstrapperStyle.CustomDialog", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap (Glass).
/// </summary>
@ -1206,6 +1639,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Blank.
/// </summary>
public static string Enums_CustomThemeTemplate_Blank {
get {
return ResourceManager.GetString("Enums.CustomThemeTemplate.Blank", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Simple.
/// </summary>
public static string Enums_CustomThemeTemplate_Simple {
get {
return ResourceManager.GetString("Enums.CustomThemeTemplate.Simple", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Catmoji.
/// </summary>
@ -1806,6 +2257,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Apache License 2.0.
/// </summary>
public static string Menu_About_Licenses_Apache {
get {
return ResourceManager.GetString("Menu.About.Licenses.Apache", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to BSD 2-Clause License.
/// </summary>
@ -1950,6 +2410,33 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Failed to delete custom theme {0}: {1}.
/// </summary>
public static string Menu_Appearance_CustomThemes_DeleteFailed {
get {
return ResourceManager.GetString("Menu.Appearance.CustomThemes.DeleteFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No custom theme selected..
/// </summary>
public static string Menu_Appearance_CustomThemes_NoneSelected {
get {
return ResourceManager.GetString("Menu.Appearance.CustomThemes.NoneSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to rename custom theme {0}: {1}.
/// </summary>
public static string Menu_Appearance_CustomThemes_RenameFailed {
get {
return ResourceManager.GetString("Menu.Appearance.CustomThemes.RenameFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Configure how Bloxstrap should look..
/// </summary>

View File

@ -1239,6 +1239,9 @@ Would you like to enable test mode?</value>
<data name="Dialog.Exception.Version" xml:space="preserve">
<value>Version {0}</value>
</data>
<data name="Enums.BootstrapperStyle.CustomDialog" xml:space="preserve">
<value>Custom</value>
</data>
<data name="Bootstrapper.FilesInUse" xml:space="preserve">
<value>Bloxstrap tried to upgrade Roblox but can't because Roblox's files are still in use.
@ -1282,4 +1285,164 @@ Please close any applications that may be using Roblox's files, and relaunch.</v
<data name="Bootstrapper.ModificationsFailed.Message" xml:space="preserve">
<value>Not all modifications will be present in the current launch.</value>
</data>
<data name="Menu.About.Licenses.Apache" xml:space="preserve">
<value>Apache License 2.0</value>
</data>
<data name="Enums.CustomThemeTemplate.Blank" xml:space="preserve">
<value>Blank</value>
</data>
<data name="Enums.CustomThemeTemplate.Simple" xml:space="preserve">
<value>Simple</value>
</data>
<data name="CustomTheme.Errors.InvalidRoot" xml:space="preserve">
<value>Theme XML root is not {0}</value>
</data>
<data name="CustomTheme.Errors.DialogAlreadyInitialised" xml:space="preserve">
<value>Custom dialog has already been initialised</value>
</data>
<data name="CustomTheme.Errors.TooManyElements" xml:space="preserve">
<value>Custom bootstrappers can only have a maximum of {0} elements, got {1}.</value>
</data>
<data name="CustomTheme.Errors.VersionNotSet" xml:space="preserve">
<value>{0} version is not set</value>
</data>
<data name="CustomTheme.Errors.VersionNotNumber" xml:space="preserve">
<value>{0} version is not a number</value>
</data>
<data name="CustomTheme.Errors.VersionNotSupported" xml:space="preserve">
<value>{0} version {1} is no longer supported</value>
</data>
<data name="CustomTheme.Errors.VersionNotRecognised" xml:space="preserve">
<value>{0} version {1} is not recognised</value>
</data>
<data name="CustomTheme.Errors.ElementInvalidChild" xml:space="preserve">
<value>{0} cannot have a child of {1}</value>
</data>
<data name="CustomTheme.Errors.UnknownElement" xml:space="preserve">
<value>Unknown element {0}</value>
</data>
<data name="CustomTheme.Errors.XMLParseFailed" xml:space="preserve">
<value>Failed to parse the theme file: {0}</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeConversionError" xml:space="preserve">
<value>{0} has invalid {1}: {2}</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeMissing" xml:space="preserve">
<value>Element {0} is missing the {1} attribute</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeInvalidType" xml:space="preserve">
<value>{0} {1} is not a valid {2}</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeMustBeLargerThanMin" xml:space="preserve">
<value>{0} {1} must be larger than {2}</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeMustBeSmallerThanMax" xml:space="preserve">
<value>{0} {1} must be smaller than {2}</value>
</data>
<data name="CustomTheme.Errors.UnknownEnumValue" xml:space="preserve">
<value>{0} Unknown {1} {2}</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeMultipleDefinitions" xml:space="preserve">
<value>{0} can only have one {1} defined</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeMultipleChildren" xml:space="preserve">
<value>{0}.{1} can only have one child</value>
</data>
<data name="CustomTheme.Errors.ElementMultipleChildren" xml:space="preserve">
<value>{0} can only have one child</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeMissingChild" xml:space="preserve">
<value>{0}.{1} is missing it's child</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeParseError" xml:space="preserve">
<value>{0}.{1} could not be parsed into a {2}</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeParseErrorNull" xml:space="preserve">
<value>{0}.{1} {2} is null</value>
</data>
<data name="CustomTheme.Errors.ElementAttributeBlacklistedUriScheme" xml:space="preserve">
<value>{0}.{1} uses blacklisted scheme {2}</value>
</data>
<data name="CustomTheme.Errors.ElementTypeCreationFailed" xml:space="preserve">
<value>{0} failed to create {1}: {2}</value>
</data>
<data name="CustomTheme.Editor.Title" xml:space="preserve">
<value>Editing "{0}"</value>
</data>
<data name="CustomTheme.Editor.Save.Success" xml:space="preserve">
<value>Theme successfully saved!</value>
</data>
<data name="CustomTheme.Editor.Save.Error" xml:space="preserve">
<value>An error occurred while saving your theme.</value>
</data>
<data name="CustomTheme.Editor.ConfirmSave" xml:space="preserve">
<value>Save changes to {0}?</value>
</data>
<data name="CustomTheme.Editor.Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="CustomTheme.Editor.Preview" xml:space="preserve">
<value>Preview</value>
</data>
<data name="CustomTheme.Editor.OpenThemeDirectory" xml:space="preserve">
<value>Open Theme Directory</value>
</data>
<data name="Common.CreateNew" xml:space="preserve">
<value>Create New</value>
</data>
<data name="Common.Import" xml:space="preserve">
<value>Import</value>
</data>
<data name="CustomTheme.Add.Title" xml:space="preserve">
<value>Add Custom Theme</value>
</data>
<data name="Common.Template" xml:space="preserve">
<value>Template</value>
</data>
<data name="CustomTheme.Add.Errors.NameEmpty" xml:space="preserve">
<value>Name cannot be empty</value>
</data>
<data name="CustomTheme.Add.Errors.NameIllegalCharacters" xml:space="preserve">
<value>Name contains illegal characters</value>
</data>
<data name="CustomTheme.Add.Errors.NameReserved" xml:space="preserve">
<value>Name cannot be used</value>
</data>
<data name="CustomTheme.Add.Errors.Unknown" xml:space="preserve">
<value>Unknown error</value>
</data>
<data name="CustomTheme.Add.Errors.NameTaken" xml:space="preserve">
<value>Name is already in use</value>
</data>
<data name="CustomTheme.Add.Errors.FileNotZip" xml:space="preserve">
<value>File must be a ZIP</value>
</data>
<data name="CustomTheme.Add.Errors.ZipMissingThemeFile" xml:space="preserve">
<value>Theme file could not be found in the ZIP file</value>
</data>
<data name="CustomTheme.Add.Errors.ZipInvalidData" xml:space="preserve">
<value>Invalid or corrupted ZIP file</value>
</data>
<data name="CustomTheme.Errors.NoThemeSelected" xml:space="preserve">
<value>No custom theme selected</value>
</data>
<data name="CustomTheme.Errors.SetupFailed" xml:space="preserve">
<value>Failed to setup custom bootstrapper: {0}.
Defaulting to Fluent.</value>
</data>
<data name="Menu.Appearance.CustomThemes.NoneSelected" xml:space="preserve">
<value>No custom theme selected.</value>
</data>
<data name="Common.Rename" xml:space="preserve">
<value>Rename</value>
</data>
<data name="Common.Edit" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Menu.Appearance.CustomThemes.DeleteFailed" xml:space="preserve">
<value>Failed to delete custom theme {0}: {1}</value>
</data>
<data name="Menu.Appearance.CustomThemes.RenameFailed" xml:space="preserve">
<value>Failed to rename custom theme {0}: {1}</value>
</data>
</root>

View File

@ -10,8 +10,8 @@
mc:Ignorable="d"
Title="{x:Static resources:Strings.About_Title}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
MinWidth="740"
Width="740"
MinWidth="800"
Width="800"
Height="440"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"

View File

@ -43,30 +43,44 @@
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/Lachee/discord-rpc-csharp/blob/master/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="DiscordRPC" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
<ui:CardAction Grid.Row="1" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="13" Text="Roblox Studio Mod Manager" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
<ui:CardAction Grid.Row="1" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="13" Text="SharpZipLib" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/xoofx/markdig/blob/master/license.txt">
<ui:CardAction Grid.Row="2" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/xoofx/markdig/blob/master/license.txt">
<StackPanel>
<TextBlock FontSize="14" Text="Markdig" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_BSD2}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/AvalonEdit/blob/master/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="AvalonEdit" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/XamlAnimatedGif/XamlAnimatedGif/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="14" Text="XamlAnimatedGif" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_Apache}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -18,9 +18,15 @@ namespace Bloxstrap.UI.Elements.Base
public void ApplyTheme()
{
const int customThemeIndex = 2; // index for CustomTheme merged dictionary
_themeService.SetTheme(App.Settings.Prop.Theme.GetFinal() == Enums.Theme.Dark ? ThemeType.Dark : ThemeType.Light);
_themeService.SetSystemAccent();
// there doesn't seem to be a way to query the name for merged dictionaries
var dict = new ResourceDictionary { Source = new Uri($"pack://application:,,,/UI/Style/{Enum.GetName(App.Settings.Prop.Theme.GetFinal())}.xaml") };
Application.Current.Resources.MergedDictionaries[customThemeIndex] = dict;
#if QA_BUILD
this.BorderBrush = System.Windows.Media.Brushes.Red;
this.BorderThickness = new Thickness(4);

View File

@ -0,0 +1,90 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using System.Xml;
using System.Xml.Linq;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
public partial class CustomDialog
{
// https://stackoverflow.com/a/2961702
private static T? ConvertValue<T>(string input) where T : struct
{
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
return (T?)converter.ConvertFromInvariantString(input);
}
return default;
}
catch (NotSupportedException)
{
return default;
}
}
private static object? GetTypeFromXElement(TypeConverter converter, XElement xmlElement, string attributeName)
{
string? attributeValue = xmlElement.Attribute(attributeName)?.Value?.ToString();
if (attributeValue == null)
return null;
try
{
return converter.ConvertFromInvariantString(attributeValue);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementAttributeConversionError", xmlElement.Name, attributeName, ex.Message);
}
}
private static ThicknessConverter ThicknessConverter { get; } = new ThicknessConverter();
private static object? GetThicknessFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(ThicknessConverter, xmlElement, attributeName);
private static RectConverter RectConverter { get; } = new RectConverter();
private static object? GetRectFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(RectConverter, xmlElement, attributeName);
private static ColorConverter ColorConverter { get; } = new ColorConverter();
private static object? GetColorFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(ColorConverter, xmlElement, attributeName);
private static PointConverter PointConverter { get; } = new PointConverter();
private static object? GetPointFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(PointConverter, xmlElement, attributeName);
private static CornerRadiusConverter CornerRadiusConverter { get; } = new CornerRadiusConverter();
private static object? GetCornerRadiusFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(CornerRadiusConverter, xmlElement, attributeName);
private static GridLengthConverter GridLengthConverter { get; } = new GridLengthConverter();
private static object? GetGridLengthFromXElement(XElement xmlElement, string attributeName) => GetTypeFromXElement(GridLengthConverter, xmlElement, attributeName);
private static BrushConverter BrushConverter { get; } = new BrushConverter();
/// <summary>
/// Return type of string = Name of DynamicResource
/// Return type of brush = ... The Brush!!!
/// </summary>
private static object? GetBrushFromXElement(XElement element, string attributeName)
{
string? value = element.Attribute(attributeName)?.Value?.ToString();
if (value == null)
return null;
// dynamic resource name
if (value.StartsWith('{') && value.EndsWith('}'))
return value[1..^1];
try
{
return BrushConverter.ConvertFromInvariantString(value);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementAttributeConversionError", element.Name, attributeName, ex.Message);
}
}
}
}

View File

@ -0,0 +1,151 @@
using System.Windows;
using System.Xml.Linq;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
public partial class CustomDialog
{
const int Version = 1;
private class DummyFrameworkElement : FrameworkElement { }
private const int MaxElements = 100;
private bool _initialised = false;
// prevent users from creating elements with the same name multiple times
private List<string> UsedNames { get; } = new List<string>();
private string ThemeDir { get; set; } = "";
delegate object HandleXmlElementDelegate(CustomDialog dialog, XElement xmlElement);
private static Dictionary<string, HandleXmlElementDelegate> _elementHandlerMap = new Dictionary<string, HandleXmlElementDelegate>()
{
["BloxstrapCustomBootstrapper"] = HandleXmlElement_BloxstrapCustomBootstrapper_Fake,
["TitleBar"] = HandleXmlElement_TitleBar,
["Button"] = HandleXmlElement_Button,
["ProgressBar"] = HandleXmlElement_ProgressBar,
["ProgressRing"] = HandleXmlElement_ProgressRing,
["TextBlock"] = HandleXmlElement_TextBlock,
["MarkdownTextBlock"] = HandleXmlElement_MarkdownTextBlock,
["Image"] = HandleXmlElement_Image,
["Grid"] = HandleXmlElement_Grid,
["StackPanel"] = HandleXmlElement_StackPanel,
["Border"] = HandleXmlElement_Border,
["SolidColorBrush"] = HandleXmlElement_SolidColorBrush,
["ImageBrush"] = HandleXmlElement_ImageBrush,
["LinearGradientBrush"] = HandleXmlElement_LinearGradientBrush,
["GradientStop"] = HandleXmlElement_GradientStop,
["ScaleTransform"] = HandleXmlElement_ScaleTransform,
["SkewTransform"] = HandleXmlElement_SkewTransform,
["RotateTransform"] = HandleXmlElement_RotateTransform,
["TranslateTransform"] = HandleXmlElement_TranslateTransform,
["BlurEffect"] = HandleXmlElement_BlurEffect,
["DropShadowEffect"] = HandleXmlElement_DropShadowEffect,
["Ellipse"] = HandleXmlElement_Ellipse,
["Line"] = HandleXmlElement_Line,
["Rectangle"] = HandleXmlElement_Rectangle,
["RowDefinition"] = HandleXmlElement_RowDefinition,
["ColumnDefinition"] = HandleXmlElement_ColumnDefinition
};
private static T HandleXml<T>(CustomDialog dialog, XElement xmlElement) where T : class
{
if (!_elementHandlerMap.ContainsKey(xmlElement.Name.ToString()))
throw new CustomThemeException("CustomTheme.Errors.UnknownElement", xmlElement.Name);
var element = _elementHandlerMap[xmlElement.Name.ToString()](dialog, xmlElement);
if (element is not T)
throw new CustomThemeException("CustomTheme.Errors.ElementInvalidChild", xmlElement.Parent!.Name, xmlElement.Name);
return (T)element;
}
private static void AddXml(CustomDialog dialog, XElement xmlElement)
{
if (xmlElement.Name.ToString().StartsWith($"{xmlElement.Parent!.Name}."))
return; // not an xml element
var uiElement = HandleXml<UIElement>(dialog, xmlElement);
if (uiElement is not DummyFrameworkElement)
dialog.ElementGrid.Children.Add(uiElement);
}
private static void AssertThemeVersion(string? versionStr)
{
if (string.IsNullOrEmpty(versionStr))
throw new CustomThemeException("CustomTheme.Errors.VersionNotSet", "BloxstrapCustomBootstrapper");
if (!uint.TryParse(versionStr, out uint version))
throw new CustomThemeException("CustomTheme.Errors.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 CustomThemeException("CustomTheme.Errors.VersionNotSupported", "BloxstrapCustomBootstrapper", version);
default:
throw new CustomThemeException("CustomTheme.Errors.VersionNotRecognised", "BloxstrapCustomBootstrapper", version);
}
}
private void HandleXmlBase(XElement xml)
{
if (_initialised)
throw new CustomThemeException("CustomTheme.Errors.DialogAlreadyInitialised");
if (xml.Name != "BloxstrapCustomBootstrapper")
throw new CustomThemeException("CustomTheme.Errors.InvalidRoot", "BloxstrapCustomBootstrapper");
AssertThemeVersion(xml.Attribute("Version")?.Value);
if (xml.Descendants().Count() > MaxElements)
throw new CustomThemeException("CustomTheme.Errors.TooManyElements", MaxElements, xml.Descendants().Count());
_initialised = true;
// handle root
HandleXmlElement_BloxstrapCustomBootstrapper(this, xml);
// handle everything else
foreach (var child in xml.Elements())
AddXml(this, child);
}
#region Public APIs
public void ApplyCustomTheme(string name, string contents)
{
ThemeDir = System.IO.Path.Combine(Paths.CustomThemes, name);
XElement xml;
try
{
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
xml = XElement.Load(ms);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.XMLParseFailed", ex.Message);
}
HandleXmlBase(xml);
}
public void ApplyCustomTheme(string name)
{
string path = System.IO.Path.Combine(Paths.CustomThemes, name, "Theme.xml");
ApplyCustomTheme(name, File.ReadAllText(path));
}
#endregion
}
}

View File

@ -0,0 +1,772 @@
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<double>(xmlElement, "ScaleX", 1);
st.ScaleY = ParseXmlAttribute<double>(xmlElement, "ScaleY", 1);
st.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
st.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
return st;
}
private static Transform HandleXmlElement_SkewTransform(CustomDialog dialog, XElement xmlElement)
{
var st = new SkewTransform();
st.AngleX = ParseXmlAttribute<double>(xmlElement, "AngleX", 0);
st.AngleY = ParseXmlAttribute<double>(xmlElement, "AngleY", 0);
st.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
st.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
return st;
}
private static Transform HandleXmlElement_RotateTransform(CustomDialog dialog, XElement xmlElement)
{
var rt = new RotateTransform();
rt.Angle = ParseXmlAttribute<double>(xmlElement, "Angle", 0);
rt.CenterX = ParseXmlAttribute<double>(xmlElement, "CenterX", 0);
rt.CenterY = ParseXmlAttribute<double>(xmlElement, "CenterY", 0);
return rt;
}
private static Transform HandleXmlElement_TranslateTransform(CustomDialog dialog, XElement xmlElement)
{
var tt = new TranslateTransform();
tt.X = ParseXmlAttribute<double>(xmlElement, "X", 0);
tt.Y = ParseXmlAttribute<double>(xmlElement, "Y", 0);
return tt;
}
#endregion
#region Effects
private static BlurEffect HandleXmlElement_BlurEffect(CustomDialog dialog, XElement xmlElement)
{
var effect = new BlurEffect();
effect.KernelType = ParseXmlAttribute<KernelType>(xmlElement, "KernelType", KernelType.Gaussian);
effect.Radius = ParseXmlAttribute<double>(xmlElement, "Radius", 5);
effect.RenderingBias = ParseXmlAttribute<RenderingBias>(xmlElement, "RenderingBias", RenderingBias.Performance);
return effect;
}
private static DropShadowEffect HandleXmlElement_DropShadowEffect(CustomDialog dialog, XElement xmlElement)
{
var effect = new DropShadowEffect();
effect.BlurRadius = ParseXmlAttribute<double>(xmlElement, "BlurRadius", 5);
effect.Direction = ParseXmlAttribute<double>(xmlElement, "Direction", 315);
effect.Opacity = ParseXmlAttribute<double>(xmlElement, "Opacity", 1);
effect.ShadowDepth = ParseXmlAttribute<double>(xmlElement, "ShadowDepth", 5);
effect.RenderingBias = ParseXmlAttribute<RenderingBias>(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<double>(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<AlignmentX>(xmlElement, "AlignmentX", AlignmentX.Center);
imageBrush.AlignmentY = ParseXmlAttribute<AlignmentY>(xmlElement, "AlignmentY", AlignmentY.Center);
imageBrush.Stretch = ParseXmlAttribute<Stretch>(xmlElement, "Stretch", Stretch.Fill);
imageBrush.TileMode = ParseXmlAttribute<TileMode>(xmlElement, "TileMode", TileMode.None);
imageBrush.ViewboxUnits = ParseXmlAttribute<BrushMappingMode>(xmlElement, "ViewboxUnits", BrushMappingMode.RelativeToBoundingBox);
imageBrush.ViewportUnits = ParseXmlAttribute<BrushMappingMode>(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 CustomThemeException(ex, "CustomTheme.Errors.ElementTypeCreationFailed", "Image", "BitmapImage", ex.Message);
}
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<double>(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<ColorInterpolationMode>(xmlElement, "ColorInterpolationMode", ColorInterpolationMode.SRgbLinearInterpolation);
brush.MappingMode = ParseXmlAttribute<BrushMappingMode>(xmlElement, "MappingMode", BrushMappingMode.RelativeToBoundingBox);
brush.SpreadMethod = ParseXmlAttribute<GradientSpreadMethod>(xmlElement, "SpreadMethod", GradientSpreadMethod.Pad);
foreach (var child in xmlElement.Elements())
brush.GradientStops.Add(HandleXml<GradientStop>(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 CustomThemeException("CustomTheme.Errors.ElementAttributeMissingChild", xmlElement.Name, name);
var brush = HandleXml<Brush>(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<Stretch>(xmlElement, "Stretch", Stretch.Fill);
shape.StrokeDashCap = ParseXmlAttribute<PenLineCap>(xmlElement, "StrokeDashCap", PenLineCap.Flat);
shape.StrokeDashOffset = ParseXmlAttribute<double>(xmlElement, "StrokeDashOffset", 0);
shape.StrokeEndLineCap = ParseXmlAttribute<PenLineCap>(xmlElement, "StrokeEndLineCap", PenLineCap.Flat);
shape.StrokeLineJoin = ParseXmlAttribute<PenLineJoin>(xmlElement, "StrokeLineJoin", PenLineJoin.Miter);
shape.StrokeMiterLimit = ParseXmlAttribute<double>(xmlElement, "StrokeMiterLimit", 10);
shape.StrokeStartLineCap = ParseXmlAttribute<PenLineCap>(xmlElement, "StrokeStartLineCap", PenLineCap.Flat);
shape.StrokeThickness = ParseXmlAttribute<double>(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<double>(xmlElement, "X1", 0);
line.X2 = ParseXmlAttribute<double>(xmlElement, "X2", 0);
line.Y1 = ParseXmlAttribute<double>(xmlElement, "Y1", 0);
line.Y2 = ParseXmlAttribute<double>(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<double>(xmlElement, "RadiusX", 0);
rectangle.RadiusY = ParseXmlAttribute<double>(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<Visibility>(xmlElement, "Visibility", Visibility.Visible);
uiElement.IsEnabled = ParseXmlAttribute<bool>(xmlElement, "IsEnabled", true);
object? margin = GetThicknessFromXElement(xmlElement, "Margin");
if (margin != null)
uiElement.Margin = (Thickness)margin;
uiElement.Height = ParseXmlAttribute<double>(xmlElement, "Height", double.NaN);
uiElement.Width = ParseXmlAttribute<double>(xmlElement, "Width", double.NaN);
// default values of these were originally Stretch but that was no good
uiElement.HorizontalAlignment = ParseXmlAttribute<HorizontalAlignment>(xmlElement, "HorizontalAlignment", HorizontalAlignment.Left);
uiElement.VerticalAlignment = ParseXmlAttribute<VerticalAlignment>(xmlElement, "VerticalAlignment", VerticalAlignment.Top);
uiElement.Opacity = ParseXmlAttribute<double>(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, "Panel.ZIndex", defaultValue: 0, min: 0, max: 1000);
Panel.SetZIndex(uiElement, zIndex);
int gridRow = ParseXmlAttribute<int>(xmlElement, "Grid.Row", 0);
Grid.SetRow(uiElement, gridRow);
int gridRowSpan = ParseXmlAttribute<int>(xmlElement, "Grid.RowSpan", 1);
Grid.SetRowSpan(uiElement, gridRowSpan);
int gridColumn = ParseXmlAttribute<int>(xmlElement, "Grid.Column", 0);
Grid.SetColumn(uiElement, gridColumn);
int gridColumnSpan = ParseXmlAttribute<int>(xmlElement, "Grid.ColumnSpan", 1);
Grid.SetColumnSpan(uiElement, gridColumnSpan);
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<double>(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<Theme>(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;
dialog.WindowCornerPreference = ParseXmlAttribute<Wpf.Ui.Appearance.WindowCornerPreference>(xmlElement, "WindowCornerPreference", Wpf.Ui.Appearance.WindowCornerPreference.Round);
// 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;
bool ignoreTitleBarInset = ParseXmlAttribute<bool>(xmlElement, "IgnoreTitleBarInset", false);
if (ignoreTitleBarInset)
{
Grid.SetRow(dialog.ElementGrid, 0);
Grid.SetRowSpan(dialog.ElementGrid, 2);
}
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<bool>(xmlElement, "ShowMinimize", true);
dialog.RootTitleBar.ShowClose = ParseXmlAttribute<bool>(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<double>(xmlElement, "Value", 0);
rangeBase.Maximum = ParseXmlAttribute<double>(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<bool>(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<bool>(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<double>(xmlElement, "FontSize");
if (fontSize is double)
textBlock.FontSize = (double)fontSize;
textBlock.FontWeight = GetFontWeightFromXElement(xmlElement);
textBlock.FontStyle = GetFontStyleFromXElement(xmlElement);
textBlock.LineHeight = ParseXmlAttribute<double>(xmlElement, "LineHeight", double.NaN);
textBlock.LineStackingStrategy = ParseXmlAttribute<LineStackingStrategy>(xmlElement, "LineStackingStrategy", LineStackingStrategy.MaxHeight);
textBlock.TextAlignment = ParseXmlAttribute<TextAlignment>(xmlElement, "TextAlignment", TextAlignment.Center);
textBlock.TextTrimming = ParseXmlAttribute<TextTrimming>(xmlElement, "TextTrimming", TextTrimming.None);
textBlock.TextWrapping = ParseXmlAttribute<TextWrapping>(xmlElement, "TextWrapping", TextWrapping.NoWrap);
textBlock.TextDecorations = GetTextDecorationsFromXElement(xmlElement);
textBlock.IsHyphenationEnabled = ParseXmlAttribute<bool>(xmlElement, "IsHyphenationEnabled", false);
textBlock.BaselineOffset = ParseXmlAttribute<double>(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;
}
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);
if (xmlElement.Attribute("Name")?.Value == "StatusText")
{
Binding textBinding = new Binding("Message") { Mode = BindingMode.OneWay };
BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, textBinding);
}
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<Stretch>(xmlElement, "Stretch", Stretch.Uniform);
image.StretchDirection = ParseXmlAttribute<StretchDirection>(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<bool>(xmlElement, "IsAnimated", false);
if (!isAnimated)
{
BitmapImage bitmapImage;
try
{
bitmapImage = new BitmapImage(sourceData.Uri!);
}
catch (Exception ex)
{
throw new CustomThemeException(ex, "CustomTheme.Errors.ElementTypeCreationFailed", "Image", "BitmapImage", ex.Message);
}
image.Source = bitmapImage;
}
else
{
XamlAnimatedGif.AnimationBehavior.SetSourceUri(image, sourceData.Uri!);
}
}
return image;
}
private static RowDefinition HandleXmlElement_RowDefinition(CustomDialog dialog, XElement xmlElement)
{
var rowDefinition = new RowDefinition();
var height = GetGridLengthFromXElement(xmlElement, "Height");
if (height != null)
rowDefinition.Height = (GridLength)height;
rowDefinition.MinHeight = ParseXmlAttribute<double>(xmlElement, "MinHeight", 0);
rowDefinition.MaxHeight = ParseXmlAttribute<double>(xmlElement, "MaxHeight", double.PositiveInfinity);
return rowDefinition;
}
private static ColumnDefinition HandleXmlElement_ColumnDefinition(CustomDialog dialog, XElement xmlElement)
{
var columnDefinition = new ColumnDefinition();
var width = GetGridLengthFromXElement(xmlElement, "Width");
if (width != null)
columnDefinition.Width = (GridLength)width;
columnDefinition.MinWidth = ParseXmlAttribute<double>(xmlElement, "MinWidth", 0);
columnDefinition.MaxWidth = ParseXmlAttribute<double>(xmlElement, "MaxWidth", double.PositiveInfinity);
return columnDefinition;
}
private static void HandleXmlElement_Grid_RowDefinitions(Grid grid, CustomDialog dialog, XElement xmlElement)
{
foreach (var element in xmlElement.Elements())
{
var rowDefinition = HandleXml<RowDefinition>(dialog, element);
grid.RowDefinitions.Add(rowDefinition);
}
}
private static void HandleXmlElement_Grid_ColumnDefinitions(Grid grid, CustomDialog dialog, XElement xmlElement)
{
foreach (var element in xmlElement.Elements())
{
var columnDefinition = HandleXml<ColumnDefinition>(dialog, element);
grid.ColumnDefinitions.Add(columnDefinition);
}
}
private static Grid HandleXmlElement_Grid(CustomDialog dialog, XElement xmlElement)
{
var grid = new Grid();
HandleXmlElement_FrameworkElement(dialog, grid, xmlElement);
bool rowsSet = false;
bool columnsSet = false;
foreach (var element in xmlElement.Elements())
{
if (element.Name == "Grid.RowDefinitions")
{
if (rowsSet)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", "Grid", "RowDefinitions");
rowsSet = true;
HandleXmlElement_Grid_RowDefinitions(grid, dialog, element);
}
else if (element.Name == "Grid.ColumnDefinitions")
{
if (columnsSet)
throw new CustomThemeException("CustomTheme.Errors.ElementAttributeMultipleDefinitions", "Grid", "ColumnDefinitions");
columnsSet = true;
HandleXmlElement_Grid_ColumnDefinitions(grid, dialog, element);
}
else if (element.Name.ToString().StartsWith("Grid."))
{
continue; // ignore others
}
else
{
var uiElement = HandleXml<FrameworkElement>(dialog, element);
grid.Children.Add(uiElement);
}
}
return grid;
}
private static StackPanel HandleXmlElement_StackPanel(CustomDialog dialog, XElement xmlElement)
{
var stackPanel = new StackPanel();
HandleXmlElement_FrameworkElement(dialog, stackPanel, xmlElement);
stackPanel.Orientation = ParseXmlAttribute<Orientation>(xmlElement, "Orientation", Orientation.Vertical);
foreach (var element in xmlElement.Elements())
{
var uiElement = HandleXml<FrameworkElement>(dialog, element);
stackPanel.Children.Add(uiElement);
}
return stackPanel;
}
private static Border HandleXmlElement_Border(CustomDialog dialog, XElement xmlElement)
{
var border = new Border();
HandleXmlElement_FrameworkElement(dialog, border, xmlElement);
ApplyBrush_UIElement(dialog, border, "Background", Border.BackgroundProperty, xmlElement);
ApplyBrush_UIElement(dialog, border, "BorderBrush", Border.BorderBrushProperty, xmlElement);
object? borderThickness = GetThicknessFromXElement(xmlElement, "BorderThickness");
if (borderThickness != null)
border.BorderThickness = (Thickness)borderThickness;
object? padding = GetThicknessFromXElement(xmlElement, "Padding");
if (padding != null)
border.Padding = (Thickness)padding;
object? cornerRadius = GetCornerRadiusFromXElement(xmlElement, "CornerRadius");
if (cornerRadius != null)
border.CornerRadius = (CornerRadius)cornerRadius;
var children = xmlElement.Elements().Where(x => !x.Name.ToString().StartsWith("Border."));
if (children.Any())
{
if (children.Count() > 1)
throw new CustomThemeException("CustomTheme.Errors.ElementMultipleChildren", "Border");
border.Child = HandleXml<UIElement>(dialog, children.First());
}
return border;
}
#endregion
}
}

View File

@ -0,0 +1,300 @@
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);
}
}
}

View File

@ -0,0 +1,55 @@
<base:WpfUiWindow
x:Class="Bloxstrap.UI.Elements.Bootstrapper.CustomDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Bootstrapper"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="Bloxstrap"
Width="800"
Height="450"
MinWidth="150"
MinHeight="150"
MaxWidth="1000"
MaxHeight="1000"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
ResizeMode="NoResize"
WindowBackdropType="Disable"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.TaskbarItemInfo>
<TaskbarItemInfo ProgressState="{Binding Path=TaskbarProgressState}" ProgressValue="{Binding Path=TaskbarProgressValue}" />
</Window.TaskbarItemInfo>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TitleBar
x:Name="RootTitleBar"
Title="Bloxstrap"
Grid.Row="0"
Padding="8"
Panel.ZIndex="1001"
CanMaximize="False"
ShowClose="False"
ShowMaximize="False"
ShowMinimize="False" />
<Grid x:Name="ElementGrid" Grid.Row="1" />
</Grid>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,122 @@
using Bloxstrap.UI.Elements.Bootstrapper.Base;
using Bloxstrap.UI.ViewModels.Bootstrapper;
using System.ComponentModel;
using System.Windows.Forms;
using System.Windows.Shell;
namespace Bloxstrap.UI.Elements.Bootstrapper
{
/// <summary>
/// Interaction logic for CustomDialog.xaml
/// </summary>
public partial class CustomDialog : IBootstrapperDialog
{
private readonly BootstrapperDialogViewModel _viewModel;
public Bloxstrap.Bootstrapper? Bootstrapper { get; set; }
private bool _isClosing;
#region UI Elements
public string Message
{
get => _viewModel.Message;
set
{
_viewModel.Message = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.Message));
}
}
public ProgressBarStyle ProgressStyle
{
get => _viewModel.ProgressIndeterminate ? ProgressBarStyle.Marquee : ProgressBarStyle.Continuous;
set
{
_viewModel.ProgressIndeterminate = (value == ProgressBarStyle.Marquee);
_viewModel.OnPropertyChanged(nameof(_viewModel.ProgressIndeterminate));
}
}
public int ProgressMaximum
{
get => _viewModel.ProgressMaximum;
set
{
_viewModel.ProgressMaximum = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.ProgressMaximum));
}
}
public int ProgressValue
{
get => _viewModel.ProgressValue;
set
{
_viewModel.ProgressValue = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.ProgressValue));
}
}
public TaskbarItemProgressState TaskbarProgressState
{
get => _viewModel.TaskbarProgressState;
set
{
_viewModel.TaskbarProgressState = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.TaskbarProgressState));
}
}
public double TaskbarProgressValue
{
get => _viewModel.TaskbarProgressValue;
set
{
_viewModel.TaskbarProgressValue = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.TaskbarProgressValue));
}
}
public bool CancelEnabled
{
get => _viewModel.CancelEnabled;
set
{
_viewModel.CancelEnabled = value;
_viewModel.OnPropertyChanged(nameof(_viewModel.CancelButtonVisibility));
_viewModel.OnPropertyChanged(nameof(_viewModel.CancelEnabled));
}
}
#endregion
public CustomDialog()
{
InitializeComponent();
_viewModel = new BootstrapperDialogViewModel(this);
DataContext = _viewModel;
Title = App.Settings.Prop.BootstrapperTitle;
Icon = App.Settings.Prop.BootstrapperIcon.GetIcon().GetImageSource();
}
private void UiWindow_Closing(object sender, CancelEventArgs e)
{
if (!_isClosing)
Bootstrapper?.Cancel();
}
#region IBootstrapperDialog Methods
public void ShowBootstrapper() => this.ShowDialog();
public void CloseBootstrapper()
{
_isClosing = true;
Dispatcher.BeginInvoke(this.Close);
}
public void ShowSuccess(string message, Action? callback) => BaseFunctions.ShowSuccess(message, callback);
#endregion
}
}

View File

@ -0,0 +1,165 @@
<base:WpfUiWindow
x:Class="Bloxstrap.UI.Elements.Dialogs.AddCustomThemeDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:viewmodels="clr-namespace:Bloxstrap.UI.ViewModels.Dialogs"
Title="Add Custom Theme"
Width="480"
MinHeight="0"
d:DataContext="{d:DesignInstance viewmodels:AddCustomThemeViewModel,
IsDesignTimeCreatable=True}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar
Title="{x:Static resources:Strings.CustomTheme_Add_Title}"
Grid.Row="0"
Grid.ColumnSpan="2"
Padding="8"
CanMaximize="False"
KeyboardNavigation.TabNavigation="None"
ShowMaximize="False"
ShowMinimize="False" />
<TabControl
x:Name="Tabs"
Grid.Row="1"
Margin="16"
SelectedIndex="{Binding Path=SelectedTab, Mode=TwoWay}">
<TabItem Header="{x:Static resources:Strings.Common_CreateNew}">
<Grid Grid.Row="1" Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid
Grid.Row="0"
Grid.ColumnSpan="2"
Margin="0,0,0,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
MinWidth="100"
VerticalAlignment="Center"
Text="{x:Static resources:Strings.Common_Name}" />
<TextBox
Grid.Row="0"
Grid.Column="1"
Text="{Binding Path=Name, Mode=TwoWay}" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
Text="{Binding Path=NameError, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="Wrap"
Visibility="{Binding Path=NameErrorVisibility, Mode=OneWay}" />
</Grid>
<TextBlock
Grid.Row="1"
Grid.Column="0"
MinWidth="100"
VerticalAlignment="Center"
Text="{x:Static resources:Strings.Common_Template}" />
<ComboBox
Grid.Row="1"
Grid.Column="1"
ItemsSource="{Binding Path=Templates, Mode=OneTime}"
Text="{Binding Path=Template, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</TabItem>
<TabItem Header="{x:Static resources:Strings.Common_Import}">
<Grid Margin="11">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
FontSize="14"
Text="{Binding Path=FilePath}"
TextAlignment="Center"
TextWrapping="Wrap"
Visibility="{Binding Path=FilePathVisibility}" />
<ui:Button
Grid.Row="1"
Margin="4"
HorizontalAlignment="Stretch"
Click="OnImportButtonClicked"
Content="{x:Static resources:Strings.Common_ImportFromFile}"
Icon="DocumentArrowUp16" />
<TextBlock
Grid.Row="2"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
Text="{Binding Path=FileError}"
TextAlignment="Center"
TextWrapping="Wrap"
Visibility="{Binding Path=FileErrorVisibility}" />
</Grid>
</TabItem>
</TabControl>
<Border
Grid.Row="2"
Margin="0,10,0,0"
Padding="15"
Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel
HorizontalAlignment="Right"
FlowDirection="LeftToRight"
Orientation="Horizontal">
<Button
MinWidth="100"
Click="OnOkButtonClicked"
Content="{x:Static resources:Strings.Common_OK}" />
<Button
MinWidth="100"
Margin="12,0,0,0"
Content="{x:Static resources:Strings.Common_Cancel}"
IsCancel="True" />
</StackPanel>
</Border>
</Grid>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,230 @@
using Bloxstrap.UI.Elements.Base;
using Bloxstrap.UI.ViewModels.Dialogs;
using Microsoft.Win32;
using System.IO.Compression;
using System.Windows;
namespace Bloxstrap.UI.Elements.Dialogs
{
/// <summary>
/// Interaction logic for AddCustomThemeDialog.xaml
/// </summary>
public partial class AddCustomThemeDialog : WpfUiWindow
{
private const int CreateNewTabId = 0;
private const int ImportTabId = 1;
private readonly AddCustomThemeViewModel _viewModel;
public bool Created { get; private set; } = false;
public string ThemeName { get; private set; } = "";
public bool OpenEditor { get; private set; } = false;
public AddCustomThemeDialog()
{
_viewModel = new AddCustomThemeViewModel();
_viewModel.Name = GenerateRandomName();
DataContext = _viewModel;
InitializeComponent();
}
private static string GetThemePath(string name)
{
return Path.Combine(Paths.CustomThemes, name, "Theme.xml");
}
private static string GenerateRandomName()
{
int count = Directory.GetDirectories(Paths.CustomThemes).Count();
string name = $"Custom Theme {count + 1}";
// TODO: this sucks
if (File.Exists(GetThemePath(name)))
name += " " + Random.Shared.Next(1, 100000).ToString(); // easy
return name;
}
private static string GetUniqueName(string name)
{
const int maxTries = 100;
if (!File.Exists(GetThemePath(name)))
return name;
for (int i = 1; i <= maxTries; i++)
{
string newName = $"{name}_{i}";
if (!File.Exists(GetThemePath(newName)))
return newName;
}
// last resort
return $"{name}_{Random.Shared.Next(maxTries+1, 1_000_000)}";
}
private static void CreateCustomTheme(string name, CustomThemeTemplate template)
{
string dir = Path.Combine(Paths.CustomThemes, name);
if (Directory.Exists(dir))
Directory.Delete(dir, true);
Directory.CreateDirectory(dir);
string themeFilePath = Path.Combine(dir, "Theme.xml");
string templateContent = Encoding.UTF8.GetString(Resource.Get(template.GetFileName()).Result);
File.WriteAllText(themeFilePath, templateContent);
}
private bool ValidateCreateNew()
{
const string LOG_IDENT = "AddCustomThemeDialog::ValidateCreateNew";
if (string.IsNullOrEmpty(_viewModel.Name))
{
_viewModel.NameError = Strings.CustomTheme_Add_Errors_NameEmpty;
return false;
}
var validationResult = PathValidator.IsFileNameValid(_viewModel.Name);
if (validationResult != PathValidator.ValidationResult.Ok)
{
switch (validationResult)
{
case PathValidator.ValidationResult.IllegalCharacter:
_viewModel.NameError = Strings.CustomTheme_Add_Errors_NameIllegalCharacters;
break;
case PathValidator.ValidationResult.ReservedFileName:
_viewModel.NameError = Strings.CustomTheme_Add_Errors_NameReserved;
break;
default:
App.Logger.WriteLine(LOG_IDENT, $"Got unhandled PathValidator::ValidationResult {validationResult}");
Debug.Assert(false);
_viewModel.NameError = Strings.CustomTheme_Add_Errors_Unknown;
break;
}
return false;
}
// better to check for the file instead of the directory so broken themes can be overwritten
string path = Path.Combine(Paths.CustomThemes, _viewModel.Name, "Theme.xml");
if (File.Exists(path))
{
_viewModel.NameError = Strings.CustomTheme_Add_Errors_NameTaken;
return false;
}
return true;
}
private bool ValidateImport()
{
const string LOG_IDENT = "AddCustomThemeDialog::ValidateImport";
if (!_viewModel.FilePath.EndsWith(".zip"))
{
_viewModel.FileError = Strings.CustomTheme_Add_Errors_FileNotZip;
return false;
}
try
{
using var zipFile = ZipFile.OpenRead(_viewModel.FilePath);
var entries = zipFile.Entries;
bool foundThemeFile = false;
foreach (var entry in entries)
{
if (entry.FullName == "Theme.xml")
{
foundThemeFile = true;
break;
}
}
if (!foundThemeFile)
{
_viewModel.FileError = Strings.CustomTheme_Add_Errors_ZipMissingThemeFile;
return false;
}
return true;
}
catch (InvalidDataException ex)
{
App.Logger.WriteLine(LOG_IDENT, "Got invalid data");
App.Logger.WriteException(LOG_IDENT, ex);
_viewModel.FileError = Strings.CustomTheme_Add_Errors_ZipInvalidData;
return false;
}
}
private void CreateNew()
{
if (!ValidateCreateNew())
return;
CreateCustomTheme(_viewModel.Name, _viewModel.Template);
Created = true;
ThemeName = _viewModel.Name;
OpenEditor = true;
Close();
}
private void Import()
{
if (!ValidateImport())
return;
string fileName = Path.GetFileNameWithoutExtension(_viewModel.FilePath);
string name = GetUniqueName(fileName);
string directory = Path.Combine(Paths.CustomThemes, name);
if (Directory.Exists(directory))
Directory.Delete(directory, true);
Directory.CreateDirectory(directory);
var fastZip = new ICSharpCode.SharpZipLib.Zip.FastZip();
fastZip.ExtractZip(_viewModel.FilePath, directory, null);
Created = true;
ThemeName = name;
OpenEditor = false;
Close();
}
private void OnOkButtonClicked(object sender, RoutedEventArgs e)
{
if (_viewModel.SelectedTab == CreateNewTabId)
CreateNew();
else
Import();
}
private void OnImportButtonClicked(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Filter = $"{Strings.FileTypes_ZipArchive}|*.zip"
};
if (dialog.ShowDialog() != true)
return;
_viewModel.FilePath = dialog.FileName;
}
}
}

View File

@ -0,0 +1,84 @@
<base:WpfUiWindow
x:Class="Bloxstrap.UI.Elements.Editor.BootstrapperEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Editor"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Editor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="{Binding Path=Title, Mode=OneTime}"
Width="1000"
Height="500"
d:DataContext="{d:DesignInstance dmodels:BootstrapperEditorWindowViewModel,
IsDesignTimeCreatable=True}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
Closing="OnClosing"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar
x:Name="RootTitleBar"
Title="{Binding Path=Title, Mode=OneTime}"
Grid.Row="0"
Padding="8"
ForceShutdown="False"
Icon="pack://application:,,,/Bloxstrap.ico"
MinimizeToTray="False"
UseSnapLayout="True" />
<avalonedit:TextEditor
x:Name="UIXML"
Grid.Row="1"
Margin="10,10,10,0"
ShowLineNumbers="True"
Style="{StaticResource NewTextEditor}"
SyntaxHighlighting="XML" />
<ui:Button
Grid.Row="2"
Margin="10"
Command="{Binding Path=OpenThemeFolderCommand, Mode=OneTime}"
Content="{x:Static resources:Strings.CustomTheme_Editor_OpenThemeDirectory}" />
<Grid
Grid.Row="2"
Margin="10"
HorizontalAlignment="Right">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:Button
Grid.Column="0"
Margin="0,0,4,0"
Command="{Binding Path=PreviewCommand, Mode=OneTime}"
Content="{x:Static resources:Strings.CustomTheme_Editor_Preview}" />
<ui:Button
Grid.Column="1"
Margin="4,0,0,0"
Appearance="Primary"
Command="{Binding Path=SaveCommand, Mode=OneTime}"
Content="{x:Static resources:Strings.CustomTheme_Editor_Save}" />
</Grid>
<ui:Snackbar
x:Name="Snackbar"
Grid.RowSpan="3"
Margin="200,0,200,20"
Panel.ZIndex="9"
Timeout="3000" />
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,580 @@
using System.Windows.Input;
using System.Xml;
using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Highlighting.Xshd;
using ICSharpCode.AvalonEdit.Highlighting;
using Bloxstrap.UI.Elements.Base;
using Bloxstrap.UI.ViewModels.Editor;
using System.Windows;
namespace Bloxstrap.UI.Elements.Editor
{
/// <summary>
/// Interaction logic for BootstrapperEditorWindow.xaml
/// </summary>
public partial class BootstrapperEditorWindow : WpfUiWindow
{
private static class CustomBootstrapperSchema
{
private class Schema
{
public Dictionary<string, Element> Elements { get; set; } = new Dictionary<string, Element>();
public Dictionary<string, Type> Types { get; set; } = new Dictionary<string, Type>();
}
private class Element
{
public string? SuperClass { get; set; } = null;
public bool IsCreatable { get; set; } = false;
// [AttributeName] = [TypeName]
public Dictionary<string, string> Attributes { get; set; } = new Dictionary<string, string>();
}
public class Type
{
public bool CanHaveElement { get; set; } = false;
public List<string>? Values { get; set; } = null;
}
private static Schema? _schema;
/// <summary>
/// Elements and their attributes
/// </summary>
public static SortedDictionary<string, SortedDictionary<string, string>> ElementInfo { get; set; } = new();
/// <summary>
/// Attributes of elements that can have property elements
/// </summary>
public static Dictionary<string, List<string>> PropertyElements { get; set; } = new();
/// <summary>
/// All type info
/// </summary>
public static SortedDictionary<string, Type> Types { get; set; } = new();
public static void ParseSchema()
{
if (_schema != null)
return;
_schema = JsonSerializer.Deserialize<Schema>(Resource.GetString("CustomBootstrapperSchema.json").Result);
if (_schema == null)
throw new Exception("Deserialised CustomBootstrapperSchema is null");
foreach (var type in _schema.Types)
Types.Add(type.Key, type.Value);
PopulateElementInfo();
}
private static (SortedDictionary<string, string>, List<string>) GetElementAttributes(string name, Element element)
{
if (ElementInfo.ContainsKey(name))
return (ElementInfo[name], PropertyElements[name]);
List<string> properties = new List<string>();
SortedDictionary<string, string> attributes = new();
foreach (var attribute in element.Attributes)
{
attributes.Add(attribute.Key, attribute.Value);
if (!Types.ContainsKey(attribute.Value))
throw new Exception($"Schema for type {attribute.Value} is missing. Blame Matt!");
Type type = Types[attribute.Value];
if (type.CanHaveElement)
properties.Add(attribute.Key);
}
if (element.SuperClass != null)
{
(SortedDictionary<string, string> superAttributes, List<string> superProperties) = GetElementAttributes(element.SuperClass, _schema!.Elements[element.SuperClass]);
foreach (var attribute in superAttributes)
attributes.Add(attribute.Key, attribute.Value);
foreach (var property in superProperties)
properties.Add(property);
}
properties.Sort();
ElementInfo[name] = attributes;
PropertyElements[name] = properties;
return (attributes, properties);
}
private static void PopulateElementInfo()
{
List<string> toRemove = new List<string>();
foreach (var element in _schema!.Elements)
{
GetElementAttributes(element.Key, element.Value);
if (!element.Value.IsCreatable)
toRemove.Add(element.Key);
}
// remove non-creatable from list now that everything is done
foreach (var name in toRemove)
{
ElementInfo.Remove(name);
}
}
}
private BootstrapperEditorWindowViewModel _viewModel;
private CompletionWindow? _completionWindow = null;
public BootstrapperEditorWindow(string name)
{
CustomBootstrapperSchema.ParseSchema();
string directory = Path.Combine(Paths.CustomThemes, name);
string themeContents = File.ReadAllText(Path.Combine(directory, "Theme.xml"));
themeContents = ToCRLF(themeContents); // make sure the theme is in CRLF. a function expects CRLF.
_viewModel = new BootstrapperEditorWindowViewModel();
_viewModel.ThemeSavedCallback = ThemeSavedCallback;
_viewModel.Directory = directory;
_viewModel.Name = name;
_viewModel.Title = string.Format(Strings.CustomTheme_Editor_Title, name);
_viewModel.Code = themeContents;
DataContext = _viewModel;
InitializeComponent();
UIXML.Text = _viewModel.Code;
UIXML.TextChanged += OnCodeChanged;
UIXML.TextArea.TextEntered += OnTextAreaTextEntered;
LoadHighlightingTheme();
}
private void LoadHighlightingTheme()
{
string name = $"Editor-Theme-{App.Settings.Prop.Theme.GetFinal()}.xshd";
using Stream xmlStream = Resource.GetStream(name);
using XmlReader reader = XmlReader.Create(xmlStream);
UIXML.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance);
UIXML.TextArea.TextView.SetResourceReference(ICSharpCode.AvalonEdit.Rendering.TextView.LinkTextForegroundBrushProperty, "NewTextEditorLink");
}
private void ThemeSavedCallback(bool success, string message)
{
if (success)
Snackbar.Show(Strings.CustomTheme_Editor_Save_Success, message, Wpf.Ui.Common.SymbolRegular.CheckmarkCircle32, Wpf.Ui.Common.ControlAppearance.Success);
else
Snackbar.Show(Strings.CustomTheme_Editor_Save_Error, message, Wpf.Ui.Common.SymbolRegular.ErrorCircle24, Wpf.Ui.Common.ControlAppearance.Danger);
}
private static string ToCRLF(string text)
{
return text.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
}
private void OnCodeChanged(object? sender, EventArgs e)
{
_viewModel.Code = UIXML.Text;
_viewModel.CodeChanged = true;
}
private void OnClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (!_viewModel.CodeChanged)
return;
var result = Frontend.ShowMessageBox(string.Format(Strings.CustomTheme_Editor_ConfirmSave, _viewModel.Name), MessageBoxImage.Information, MessageBoxButton.YesNoCancel);
if (result == MessageBoxResult.Cancel)
{
e.Cancel = true;
}
else if (result == MessageBoxResult.Yes)
{
_viewModel.SaveCommand.Execute(null);
}
}
private void OnTextAreaTextEntered(object sender, TextCompositionEventArgs e)
{
switch (e.Text)
{
case "<":
OpenElementAutoComplete();
break;
case " ":
OpenAttributeAutoComplete();
break;
case ".":
OpenPropertyElementAutoComplete();
break;
case "/":
AddEndTag();
break;
case ">":
CloseCompletionWindow();
break;
case "!":
CloseCompletionWindow();
break;
}
}
private (string, int) GetLineAndPosAtCaretPosition()
{
// this assumes the file was saved as CSLF (\r\n newlines)
int offset = UIXML.CaretOffset - 1;
int lineStartIdx = UIXML.Text.LastIndexOf('\n', offset);
int lineEndIdx = UIXML.Text.IndexOf('\n', offset);
string line;
int pos;
if (lineStartIdx == -1 && lineEndIdx == -1)
{
line = UIXML.Text;
pos = offset;
}
else if (lineStartIdx == -1)
{
line = UIXML.Text[..(lineEndIdx - 1)];
pos = offset;
}
else if (lineEndIdx == -1)
{
line = UIXML.Text[(lineStartIdx + 1)..];
pos = offset - lineStartIdx - 2;
}
else
{
line = UIXML.Text[(lineStartIdx + 1)..(lineEndIdx - 1)];
pos = offset - lineStartIdx - 2;
}
return (line, pos);
}
/// <summary>
/// Source: https://xsemmel.codeplex.com
/// </summary>
/// <param name="xml"></param>
/// <param name="offset"></param>
/// <returns></returns>
public static string? GetElementAtCursor(string xml, int offset, bool onlyAllowInside = false)
{
if (offset == xml.Length)
{
offset--;
}
int startIdx = xml.LastIndexOf('<', offset);
if (startIdx < 0) return null;
if (startIdx < xml.Length && xml[startIdx + 1] == '/')
{
startIdx = startIdx + 1;
}
int endIdx1 = xml.IndexOf(' ', startIdx);
if (endIdx1 == -1 /*|| endIdx1 > offset*/) endIdx1 = int.MaxValue;
int endIdx2 = xml.IndexOf('>', startIdx);
if (endIdx2 == -1 /*|| endIdx2 > offset*/)
{
endIdx2 = int.MaxValue;
}
else
{
if (onlyAllowInside && endIdx2 < offset)
return null; // we dont want attribute auto complete to show outside of elements
if (endIdx2 < xml.Length && xml[endIdx2 - 1] == '/')
{
endIdx2 = endIdx2 - 1;
}
}
int endIdx = Math.Min(endIdx1, endIdx2);
if (endIdx2 > 0 && endIdx2 < int.MaxValue && endIdx > startIdx)
{
string element = xml.Substring(startIdx + 1, endIdx - startIdx - 1);
return element == "!--" ? null : element; // dont treat comments as elements
}
else
{
return null;
}
}
/// <summary>
/// A space between the cursor and the element will completely cancel this function
/// </summary>
private string? GetElementAtCursorNoSpaces(string xml, int offset)
{
(string line, int pos) = GetLineAndPosAtCaretPosition();
string curr = "";
while (pos != -1)
{
char c = line[pos];
if (c == ' ' || c == '\t')
return null;
if (c == '<')
return curr;
curr = c + curr;
pos--;
}
return null;
}
/// <summary>
/// Returns null if not eligible to auto complete there.
/// Returns the name of the element to show the attributes for
/// </summary>
/// <returns></returns>
private string? ShowAttributesForElementName()
{
(string line, int pos) = GetLineAndPosAtCaretPosition();
// check if theres an even number of speech marks on the line
int numSpeech = line.Count(x => x == '"');
if (numSpeech % 2 == 0)
{
// we have an equal number, let's check if pos is in between the speech marks
int count = -1;
int idx = pos;
int size = line.Length - 1;
while (idx != -1)
{
count++;
if (size > idx + 1)
idx = line.IndexOf('"', idx + 1);
else
idx = -1;
}
if (count % 2 != 0)
{
// odd number of speech marks means we're inside a string right now
// we dont want to display attribute auto complete while we're inside a string
return null;
}
}
return GetElementAtCursor(UIXML.Text, UIXML.CaretOffset, true);
}
private void AddEndTag()
{
CloseCompletionWindow();
if (UIXML.Text.Length > 2 && UIXML.Text[UIXML.CaretOffset - 2] == '<')
{
var elementName = GetElementAtCursor(UIXML.Text, UIXML.CaretOffset - 3);
if (elementName == null)
return;
UIXML.TextArea.Document.Insert(UIXML.CaretOffset, $"{elementName}>");
}
else
{
if (UIXML.Text.Length > UIXML.CaretOffset && UIXML.Text[UIXML.CaretOffset] == '>')
return;
var elementName = ShowAttributesForElementName(); // re-using functions :)
if (elementName != null)
UIXML.TextArea.Document.Insert(UIXML.CaretOffset, ">");
}
}
private void OpenElementAutoComplete()
{
var data = new List<ICompletionData>();
foreach (var element in CustomBootstrapperSchema.ElementInfo.Keys)
data.Add(new ElementCompletionData(element));
ShowCompletionWindow(data);
}
private void OpenAttributeAutoComplete()
{
string? element = ShowAttributesForElementName();
if (element == null)
{
CloseCompletionWindow();
return;
}
if (!CustomBootstrapperSchema.ElementInfo.ContainsKey(element))
{
CloseCompletionWindow();
return;
}
var attributes = CustomBootstrapperSchema.ElementInfo[element];
var data = new List<ICompletionData>();
foreach (var attribute in attributes)
data.Add(new AttributeCompletionData(attribute.Key, () => OpenTypeValueAutoComplete(attribute.Value)));
ShowCompletionWindow(data);
}
private void OpenTypeValueAutoComplete(string typeName)
{
var typeValues = CustomBootstrapperSchema.Types[typeName].Values;
if (typeValues == null)
return;
var data = new List<ICompletionData>();
foreach (var value in typeValues)
data.Add(new TypeValueCompletionData(value));
ShowCompletionWindow(data);
}
private void OpenPropertyElementAutoComplete()
{
string? element = GetElementAtCursorNoSpaces(UIXML.Text, UIXML.CaretOffset);
if (element == null)
{
CloseCompletionWindow();
return;
}
if (!CustomBootstrapperSchema.PropertyElements.ContainsKey(element))
{
CloseCompletionWindow();
return;
}
var properties = CustomBootstrapperSchema.PropertyElements[element];
var data = new List<ICompletionData>();
foreach (var property in properties)
data.Add(new TypeValueCompletionData(property));
ShowCompletionWindow(data);
}
private void CloseCompletionWindow()
{
if (_completionWindow != null)
{
_completionWindow.Close();
_completionWindow = null;
}
}
private void ShowCompletionWindow(List<ICompletionData> completionData)
{
CloseCompletionWindow();
if (!completionData.Any())
return;
_completionWindow = new CompletionWindow(UIXML.TextArea);
IList<ICompletionData> data = _completionWindow.CompletionList.CompletionData;
foreach (var c in completionData)
data.Add(c);
_completionWindow.Show();
_completionWindow.Closed += (_, _) => _completionWindow = null;
}
}
public class ElementCompletionData : ICompletionData
{
public ElementCompletionData(string text)
{
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text);
}
}
public class AttributeCompletionData : ICompletionData
{
private Action _openValueAutoCompleteAction;
public AttributeCompletionData(string text, Action openValueAutoCompleteAction)
{
_openValueAutoCompleteAction = openValueAutoCompleteAction;
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text + "=\"\"");
textArea.Caret.Offset = textArea.Caret.Offset - 1;
_openValueAutoCompleteAction();
}
}
public class TypeValueCompletionData : ICompletionData
{
public TypeValueCompletionData(string text)
{
this.Text = text;
}
public System.Windows.Media.ImageSource? Image => null;
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the list.
public object Content => Text;
public object? Description => null;
public double Priority { get; }
public void Complete(TextArea textArea, ISegment completionSegment,
EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment, this.Text);
}
}
}

View File

@ -8,7 +8,7 @@
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="640" d:DesignWidth="800"
d:DesignHeight="900" d:DesignWidth="800"
Title="AppearancePage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
@ -43,17 +43,83 @@
<ui:Button Grid.Column="1" Content="{x:Static resources:Strings.Menu_Appearance_Preview}" HorizontalAlignment="Stretch" Margin="0,16,0,0" Command="{Binding PreviewBootstrapperCommand}" />
</Grid>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Appearance_Style_Title}"
Description="{x:Static resources:Strings.Menu_Appearance_Style_Description}">
<ComboBox Width="200" Padding="10,5,10,5" ItemsSource="{Binding Dialogs, Mode=OneTime}" Text="{Binding Dialog, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:OptionControl>
<ui:CardExpander Margin="0,8,0,0" IsExpanded="{Binding Path=CustomThemesExpanded, Mode=OneWay}" Style="{StaticResource NoUserExpansionCardExpanderStyle}">
<ui:CardExpander.Header>
<Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Appearance_Style_Title}" />
<TextBlock FontSize="12" Text="{x:Static resources:Strings.Menu_Appearance_Style_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
<ComboBox Width="200" Padding="10,5,10,5" ItemsSource="{Binding Dialogs, Mode=OneTime}" Text="{Binding Dialog, Mode=TwoWay}" Grid.Column="1">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Grid>
</ui:CardExpander.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="CustomThemesListBox" Height="265" Grid.Row="0" Grid.Column="0" Margin="0,0,4,0" ItemsSource="{Binding CustomThemes, Mode=OneWay}" SelectionChanged="CustomThemeSelection" SelectedIndex="{Binding SelectedCustomThemeIndex, Mode=TwoWay}" />
<Grid Grid.Row="1" Grid.Column="0" Margin="0,8,4,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:Button Grid.Column="0" Margin="0,0,4,0" Icon="Add28" Content="{x:Static resources:Strings.Common_New}" HorizontalAlignment="Stretch" Command="{Binding AddCustomThemeCommand, Mode=OneTime}" />
<ui:Button Grid.Column="1" Margin="4,0,0,0" Icon="Delete28" Content="{x:Static resources:Strings.Common_Delete}" HorizontalAlignment="Stretch" Appearance="Danger" IsEnabled="{Binding IsCustomThemeSelected, Mode=OneWay}" Command="{Binding DeleteCustomThemeCommand, Mode=OneTime}" />
</Grid>
<StackPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Margin="4,0,0,0">
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCustomThemeSelected}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Hidden"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="{x:Static resources:Strings.Common_Name}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:TextBox Margin="0,4,0,0" Text="{Binding SelectedCustomThemeName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Grid Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:Button Grid.Column="0" Margin="0,0,4,0" Icon="Edit28" Content="{x:Static resources:Strings.Common_Rename}" HorizontalAlignment="Stretch" Command="{Binding RenameCustomThemeCommand, Mode=OneTime}" />
<ui:Button Grid.Column="1" Margin="4,0,4,0" Icon="DesktopEdit24" Content="{x:Static resources:Strings.Common_Edit}" HorizontalAlignment="Stretch" Command="{Binding EditCustomThemeCommand, Mode=OneTime}" />
<ui:Button Grid.Column="2" Margin="4,0,0,0" Icon="ArrowExportRtl24" Content="{x:Static resources:Strings.Common_Export}" HorizontalAlignment="Stretch" Command="{Binding ExportCustomThemeCommand, Mode=OneTime}" />
</Grid>
</StackPanel>
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="{x:Static resources:Strings.Menu_Appearance_CustomThemes_NoneSelected}" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCustomThemeSelected}" Value="True">
<Setter Property="TextBlock.Visibility" Value="Hidden"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</ui:CardExpander>
<controls:OptionControl
x:Name="IconSelector"

View File

@ -1,5 +1,7 @@
using Bloxstrap.UI.ViewModels.Settings;
using System.Windows.Controls;
namespace Bloxstrap.UI.Elements.Settings.Pages
{
/// <summary>
@ -12,5 +14,16 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
DataContext = new AppearanceViewModel(this);
InitializeComponent();
}
public void CustomThemeSelection(object sender, SelectionChangedEventArgs e)
{
AppearanceViewModel viewModel = (AppearanceViewModel)DataContext;
viewModel.SelectedCustomTheme = (string)((ListBox)sender).SelectedItem;
viewModel.SelectedCustomThemeName = viewModel.SelectedCustomTheme;
viewModel.OnPropertyChanged(nameof(viewModel.SelectedCustomTheme));
viewModel.OnPropertyChanged(nameof(viewModel.SelectedCustomThemeName));
}
}
}

View File

@ -58,6 +58,32 @@ namespace Bloxstrap.UI
});
}
private static IBootstrapperDialog GetCustomBootstrapper()
{
const string LOG_IDENT = "Frontend::GetCustomBootstrapper";
Directory.CreateDirectory(Paths.CustomThemes);
try
{
if (App.Settings.Prop.SelectedCustomTheme == null)
throw new CustomThemeException("CustomTheme.Errors.NoThemeSelected");
CustomDialog dialog = new CustomDialog();
dialog.ApplyCustomTheme(App.Settings.Prop.SelectedCustomTheme);
return dialog;
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
if (!App.LaunchSettings.QuietFlag.Active)
ShowMessageBox(string.Format(Strings.CustomTheme_Errors_SetupFailed, ex.Message), MessageBoxImage.Error);
return GetBootstrapperDialog(BootstrapperStyle.FluentDialog);
}
}
public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style)
{
return style switch
@ -70,6 +96,7 @@ namespace Bloxstrap.UI
BootstrapperStyle.ByfronDialog => new ByfronDialog(),
BootstrapperStyle.FluentDialog => new FluentDialog(false),
BootstrapperStyle.FluentAeroDialog => new FluentDialog(true),
BootstrapperStyle.CustomDialog => GetCustomBootstrapper(),
_ => new FluentDialog(false)
};
}

View File

@ -0,0 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="NewTextEditorBackground" Color="#2D2D2D" />
<SolidColorBrush x:Key="NewTextEditorForeground" Color="White" />
<SolidColorBrush x:Key="NewTextEditorLink" Color="#3A9CEA" />
</ResourceDictionary>

View File

@ -0,0 +1,189 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml">
<!-- Taken from DefaultUiCardExpanderStyle -->
<Style x:Key="NoUserExpansionCardExpanderStyle" TargetType="{x:Type ui:CardExpander}">
<!-- Universal WPF UI focus -->
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<!-- Universal WPF UI focus -->
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource ControlFillColorDefault}" />
</Setter.Value>
</Setter>
<Setter Property="Foreground">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource TextFillColorPrimary}" />
</Setter.Value>
</Setter>
<Setter Property="IconForeground">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource TextFillColorPrimary}" />
</Setter.Value>
</Setter>
<Setter Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
<Setter Property="BorderThickness" Value="{StaticResource CardExpanderBorderThemeThickness}" />
<Setter Property="Padding" Value="{StaticResource CardExpanderPadding}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Border.CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="IconFilled" Value="False" />
<Setter Property="Icon" Value="Empty" />
<Setter Property="IsExpanded" Value="False" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:CardExpander}">
<!-- Top level border should not have padding or margin -->
<Border
x:Name="ContentBorder"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
Padding="0"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding Border.CornerRadius}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Top level controls always visible -->
<Grid
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Column="1"
Content="{TemplateBinding Header}"
TextElement.Foreground="{TemplateBinding Foreground}" />
</Grid>
<!-- Collapsed content to expand -->
<Border
x:Name="ContentPresenterBorder"
Grid.Row="1"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
Opacity="0.0">
<ContentPresenter
x:Name="ContentPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}" />
<Border.LayoutTransform>
<ScaleTransform ScaleY="0" />
</Border.LayoutTransform>
</Border>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="ContentPresenterBorder" Property="BorderThickness" Value="0,1,0,0" />
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
From="0.0"
To="1.0"
Duration="00:00:00.167" />
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.Opacity)"
From="0.0"
To="1.0"
Duration="00:00:00.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
From="1.0"
To="0"
Duration="00:00:00.167" />
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.Opacity)"
From="1.0"
To="0.0"
Duration="00:00:00.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<!--<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}" />-->
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource ControlFillColorDisabledBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
<Setter TargetName="HeaderContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Taken from https://github.com/icsharpcode/AvalonEdit/blob/30cad99ce905412ed5f5e847e3c00e72e69aee77/ICSharpCode.AvalonEdit/TextEditor.xaml -->
<Style x:Key="NewTextEditor" TargetType="{x:Type avalonedit:TextEditor}">
<Setter Property="Foreground" Value="{DynamicResource NewTextEditorForeground}" />
<Setter Property="Background" Value="{DynamicResource NewTextEditorBackground}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type avalonedit:TextEditor}">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ScrollViewer
Name="PART_ScrollViewer"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Top"
CanContentScroll="True"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TextArea}"
Focusable="False"
HorizontalScrollBarVisibility="{TemplateBinding HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding VerticalScrollBarVisibility}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="WordWrap" Value="True">
<Setter TargetName="PART_ScrollViewer" Property="HorizontalScrollBarVisibility" Value="Disabled" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,63 @@
<SyntaxDefinition name="XML" extensions=".xml;.xsl;.xslt;.xsd;.manifest;.config;.addin;.xshd;.wxs;.wxi;.wxl;.proj;.csproj;.vbproj;.ilproj;.booproj;.build;.xfrm;.targets;.xaml;.xpt;.xft;.map;.wsdl;.disco;.ps1xml;.nuspec" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color foreground="#529955" name="Comment" exampleText="&lt;!-- comment --&gt;" />
<Color foreground="White" name="CData" exampleText="&lt;![CDATA[data]]&gt;" />
<Color foreground="White" name="DocType" exampleText="&lt;!DOCTYPE rootElement&gt;" />
<Color foreground="White" name="XmlDeclaration" exampleText='&lt;?xml version="1.0"?&gt;' />
<Color foreground="#569CD6" name="XmlTag" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="#9CDCFE" name="AttributeName" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="#CE9178" name="AttributeValue" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="White" name="Entity" exampleText="index.aspx?a=1&amp;amp;b=2" />
<Color foreground="White" name="BrokenEntity" exampleText="index.aspx?a=1&amp;b=2" />
<RuleSet>
<Span color="Comment" multiline="true">
<Begin>&lt;!--</Begin>
<End>--&gt;</End>
</Span>
<Span color="CData" multiline="true">
<Begin>&lt;!\[CDATA\[</Begin>
<End>]]&gt;</End>
</Span>
<Span color="DocType" multiline="true">
<Begin>&lt;!DOCTYPE</Begin>
<End>&gt;</End>
</Span>
<Span color="XmlDeclaration" multiline="true">
<Begin>&lt;\?</Begin>
<End>\?&gt;</End>
</Span>
<Span color="XmlTag" multiline="true">
<Begin>&lt;</Begin>
<End>&gt;</End>
<RuleSet>
<!-- Treat the position before '<' as end, as that's not a valid character
in attribute names and indicates the user forgot a closing quote. -->
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
<Begin>"</Begin>
<End>"|(?=&lt;)</End>
</Span>
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
<Begin>'</Begin>
<End>'|(?=&lt;)</End>
</Span>
<Rule color="AttributeName">[\d\w_\-\.]+(?=(\s*=))</Rule>
<Rule color="AttributeValue">=</Rule>
</RuleSet>
</Span>
<Import ruleSet="EntitySet"/>
</RuleSet>
<RuleSet name="EntitySet">
<Rule color="Entity">
&amp;
[\w\d\#]+
;
</Rule>
<Rule color="BrokenEntity">
&amp;
[\w\d\#]*
#missing ;
</Rule>
</RuleSet>
</SyntaxDefinition>

View File

@ -0,0 +1,63 @@
<SyntaxDefinition name="XML" extensions=".xml;.xsl;.xslt;.xsd;.manifest;.config;.addin;.xshd;.wxs;.wxi;.wxl;.proj;.csproj;.vbproj;.ilproj;.booproj;.build;.xfrm;.targets;.xaml;.xpt;.xft;.map;.wsdl;.disco;.ps1xml;.nuspec" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color foreground="Green" name="Comment" exampleText="&lt;!-- comment --&gt;" />
<Color foreground="Blue" name="CData" exampleText="&lt;![CDATA[data]]&gt;" />
<Color foreground="Blue" name="DocType" exampleText="&lt;!DOCTYPE rootElement&gt;" />
<Color foreground="Blue" name="XmlDeclaration" exampleText='&lt;?xml version="1.0"?&gt;' />
<Color foreground="DarkMagenta" name="XmlTag" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="Red" name="AttributeName" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="Blue" name="AttributeValue" exampleText='&lt;tag attribute="value" /&gt;' />
<Color foreground="Teal" name="Entity" exampleText="index.aspx?a=1&amp;amp;b=2" />
<Color foreground="Olive" name="BrokenEntity" exampleText="index.aspx?a=1&amp;b=2" />
<RuleSet>
<Span color="Comment" multiline="true">
<Begin>&lt;!--</Begin>
<End>--&gt;</End>
</Span>
<Span color="CData" multiline="true">
<Begin>&lt;!\[CDATA\[</Begin>
<End>]]&gt;</End>
</Span>
<Span color="DocType" multiline="true">
<Begin>&lt;!DOCTYPE</Begin>
<End>&gt;</End>
</Span>
<Span color="XmlDeclaration" multiline="true">
<Begin>&lt;\?</Begin>
<End>\?&gt;</End>
</Span>
<Span color="XmlTag" multiline="true">
<Begin>&lt;</Begin>
<End>&gt;</End>
<RuleSet>
<!-- Treat the position before '<' as end, as that's not a valid character
in attribute names and indicates the user forgot a closing quote. -->
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
<Begin>"</Begin>
<End>"|(?=&lt;)</End>
</Span>
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
<Begin>'</Begin>
<End>'|(?=&lt;)</End>
</Span>
<Rule color="AttributeName">[\d\w_\-\.]+(?=(\s*=))</Rule>
<Rule color="AttributeValue">=</Rule>
</RuleSet>
</Span>
<Import ruleSet="EntitySet"/>
</RuleSet>
<RuleSet name="EntitySet">
<Rule color="Entity">
&amp;
[\w\d\#]+
;
</Rule>
<Rule color="BrokenEntity">
&amp;
[\w\d\#]*
#missing ;
</Rule>
</RuleSet>
</SyntaxDefinition>

View File

@ -0,0 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="NewTextEditorBackground" Color="White" />
<SolidColorBrush x:Key="NewTextEditorForeground" Color="Black" />
<SolidColorBrush x:Key="NewTextEditorLink" Color="Blue" />
</ResourceDictionary>

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace Bloxstrap.UI.ViewModels.Dialogs
{
internal class AddCustomThemeViewModel : NotifyPropertyChangedViewModel
{
public static CustomThemeTemplate[] Templates => Enum.GetValues<CustomThemeTemplate>();
public CustomThemeTemplate Template { get; set; } = CustomThemeTemplate.Simple;
public string Name { get; set; } = "";
private string _filePath = "";
public string FilePath
{
get => _filePath;
set
{
if (_filePath != value)
{
_filePath = value;
OnPropertyChanged(nameof(FilePath));
OnPropertyChanged(nameof(FilePathVisibility));
}
}
}
public Visibility FilePathVisibility => string.IsNullOrEmpty(FilePath) ? Visibility.Collapsed : Visibility.Visible;
public int SelectedTab { get; set; } = 0;
private string _nameError = "";
public string NameError
{
get => _nameError;
set
{
if (_nameError != value)
{
_nameError = value;
OnPropertyChanged(nameof(NameError));
OnPropertyChanged(nameof(NameErrorVisibility));
}
}
}
public Visibility NameErrorVisibility => string.IsNullOrEmpty(NameError) ? Visibility.Collapsed : Visibility.Visible;
private string _fileError = "";
public string FileError
{
get => _fileError;
set
{
if (_fileError != value)
{
_fileError = value;
OnPropertyChanged(nameof(FileError));
OnPropertyChanged(nameof(FileErrorVisibility));
}
}
}
public Visibility FileErrorVisibility => string.IsNullOrEmpty(FileError) ? Visibility.Collapsed : Visibility.Visible;
}
}

View File

@ -0,0 +1,84 @@
using Bloxstrap.UI.Elements.Bootstrapper;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace Bloxstrap.UI.ViewModels.Editor
{
public class BootstrapperEditorWindowViewModel : NotifyPropertyChangedViewModel
{
private CustomDialog? _dialog = null;
public ICommand PreviewCommand => new RelayCommand(Preview);
public ICommand SaveCommand => new RelayCommand(Save);
public ICommand OpenThemeFolderCommand => new RelayCommand(OpenThemeFolder);
public Action<bool, string> ThemeSavedCallback { get; set; } = null!;
public string Directory { get; set; } = "";
public string Name { get; set; } = "";
public string Title { get; set; } = "Editing \"Custom Theme\"";
public string Code { get; set; } = "";
public bool CodeChanged { get; set; } = false;
private void Preview()
{
const string LOG_IDENT = "BootstrapperEditorWindowViewModel::Preview";
try
{
CustomDialog dialog = new CustomDialog();
dialog.ApplyCustomTheme(Name, Code);
_dialog?.CloseBootstrapper();
_dialog = dialog;
dialog.Message = Strings.Bootstrapper_StylePreview_TextCancel;
dialog.CancelEnabled = true;
dialog.ShowBootstrapper();
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to preview custom theme");
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox($"Failed to preview theme: {ex.Message}", MessageBoxImage.Error, MessageBoxButton.OK);
}
}
private void Save()
{
const string LOG_IDENT = "BootstrapperEditorWindowViewModel::Save";
string path = Path.Combine(Directory, "Theme.xml");
try
{
File.WriteAllText(path, Code);
CodeChanged = false;
ThemeSavedCallback.Invoke(true, "Your theme has been saved!");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to save custom theme");
App.Logger.WriteException(LOG_IDENT, ex);
//Frontend.ShowMessageBox($"Failed to save theme: {ex.Message}", MessageBoxImage.Error, MessageBoxButton.OK);
ThemeSavedCallback.Invoke(false, ex.Message);
}
}
private void OpenThemeFolder()
{
Process.Start("explorer.exe", Directory);
}
}
}

View File

@ -4,10 +4,13 @@ using System.Windows.Controls;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Win32;
using Bloxstrap.UI.Elements.Settings;
using Bloxstrap.UI.Elements.Editor;
using Bloxstrap.UI.Elements.Dialogs;
namespace Bloxstrap.UI.ViewModels.Settings
{
@ -18,6 +21,12 @@ namespace Bloxstrap.UI.ViewModels.Settings
public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper);
public ICommand BrowseCustomIconLocationCommand => new RelayCommand(BrowseCustomIconLocation);
public ICommand AddCustomThemeCommand => new RelayCommand(AddCustomTheme);
public ICommand DeleteCustomThemeCommand => new RelayCommand(DeleteCustomTheme);
public ICommand RenameCustomThemeCommand => new RelayCommand(RenameCustomTheme);
public ICommand EditCustomThemeCommand => new RelayCommand(EditCustomTheme);
public ICommand ExportCustomThemeCommand => new RelayCommand(ExportCustomTheme);
private void PreviewBootstrapper()
{
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
@ -51,6 +60,8 @@ namespace Bloxstrap.UI.ViewModels.Settings
foreach (var entry in BootstrapperIconEx.Selections)
Icons.Add(new BootstrapperIconEntry { IconType = entry });
PopulateCustomThemes();
}
public IEnumerable<Theme> Themes { get; } = Enum.GetValues(typeof(Theme)).Cast<Theme>();
@ -78,9 +89,15 @@ namespace Bloxstrap.UI.ViewModels.Settings
public BootstrapperStyle Dialog
{
get => App.Settings.Prop.BootstrapperStyle;
set => App.Settings.Prop.BootstrapperStyle = value;
set
{
App.Settings.Prop.BootstrapperStyle = value;
OnPropertyChanged(nameof(CustomThemesExpanded)); // TODO: only fire when needed
}
}
public bool CustomThemesExpanded => App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.CustomDialog;
public ObservableCollection<BootstrapperIconEntry> Icons { get; set; } = new();
public BootstrapperIcon Icon
@ -116,5 +133,183 @@ namespace Bloxstrap.UI.ViewModels.Settings
OnPropertyChanged(nameof(Icons));
}
}
private void DeleteCustomThemeStructure(string name)
{
string dir = Path.Combine(Paths.CustomThemes, name);
Directory.Delete(dir, true);
}
private void RenameCustomThemeStructure(string oldName, string newName)
{
string oldDir = Path.Combine(Paths.CustomThemes, oldName);
string newDir = Path.Combine(Paths.CustomThemes, newName);
Directory.Move(oldDir, newDir);
}
private void AddCustomTheme()
{
var dialog = new AddCustomThemeDialog();
dialog.ShowDialog();
if (dialog.Created)
{
CustomThemes.Add(dialog.ThemeName);
SelectedCustomThemeIndex = CustomThemes.Count - 1;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
OnPropertyChanged(nameof(IsCustomThemeSelected));
if (dialog.OpenEditor)
EditCustomTheme();
}
}
private void DeleteCustomTheme()
{
if (SelectedCustomTheme is null)
return;
try
{
DeleteCustomThemeStructure(SelectedCustomTheme);
}
catch (Exception ex)
{
App.Logger.WriteException("AppearanceViewModel::DeleteCustomTheme", ex);
Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_DeleteFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error);
return;
}
CustomThemes.Remove(SelectedCustomTheme);
if (CustomThemes.Any())
{
SelectedCustomThemeIndex = CustomThemes.Count - 1;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
OnPropertyChanged(nameof(IsCustomThemeSelected));
}
private void RenameCustomTheme()
{
if (SelectedCustomTheme is null)
return;
if (SelectedCustomTheme == SelectedCustomThemeName)
return;
try
{
RenameCustomThemeStructure(SelectedCustomTheme, SelectedCustomThemeName);
}
catch (Exception ex)
{
App.Logger.WriteException("AppearanceViewModel::RenameCustomTheme", ex);
Frontend.ShowMessageBox(string.Format(Strings.Menu_Appearance_CustomThemes_RenameFailed, SelectedCustomTheme, ex.Message), MessageBoxImage.Error);
return;
}
int idx = CustomThemes.IndexOf(SelectedCustomTheme);
CustomThemes[idx] = SelectedCustomThemeName;
SelectedCustomThemeIndex = idx;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
private void EditCustomTheme()
{
if (SelectedCustomTheme is null)
return;
new BootstrapperEditorWindow(SelectedCustomTheme).ShowDialog();
}
private void ExportCustomTheme()
{
if (SelectedCustomTheme is null)
return;
var dialog = new SaveFileDialog
{
FileName = $"{SelectedCustomTheme}.zip",
Filter = $"{Strings.FileTypes_ZipArchive}|*.zip"
};
if (dialog.ShowDialog() != true)
return;
string themeDir = Path.Combine(Paths.CustomThemes, SelectedCustomTheme);
using var memStream = new MemoryStream();
using var zipStream = new ZipOutputStream(memStream);
foreach (var filePath in Directory.EnumerateFiles(themeDir, "*.*", SearchOption.AllDirectories))
{
string relativePath = filePath[(themeDir.Length + 1)..];
var entry = new ZipEntry(relativePath);
entry.DateTime = DateTime.Now;
zipStream.PutNextEntry(entry);
using var fileStream = File.OpenRead(filePath);
fileStream.CopyTo(zipStream);
}
zipStream.CloseEntry();
zipStream.Finish();
memStream.Position = 0;
using var outputStream = File.OpenWrite(dialog.FileName);
memStream.CopyTo(outputStream);
Process.Start("explorer.exe", $"/select,\"{dialog.FileName}\"");
}
private void PopulateCustomThemes()
{
string? selected = App.Settings.Prop.SelectedCustomTheme;
Directory.CreateDirectory(Paths.CustomThemes);
foreach (string directory in Directory.GetDirectories(Paths.CustomThemes))
{
if (!File.Exists(Path.Combine(directory, "Theme.xml")))
continue; // missing the main theme file, ignore
string name = Path.GetFileName(directory);
CustomThemes.Add(name);
}
if (selected != null)
{
int idx = CustomThemes.IndexOf(selected);
if (idx != -1)
{
SelectedCustomThemeIndex = idx;
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
}
else
{
SelectedCustomTheme = null;
}
}
}
public string? SelectedCustomTheme
{
get => App.Settings.Prop.SelectedCustomTheme;
set => App.Settings.Prop.SelectedCustomTheme = value;
}
public string SelectedCustomThemeName { get; set; } = "";
public int SelectedCustomThemeIndex { get; set; }
public ObservableCollection<string> CustomThemes { get; set; } = new();
public bool IsCustomThemeSelected => SelectedCustomTheme is not null;
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Utility
{
internal static class PathValidator
{
public enum ValidationResult
{
Ok,
IllegalCharacter,
ReservedFileName,
ReservedDirectoryName
}
private static readonly string[] _reservedNames = new string[]
{
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9"
};
private static readonly char[] _directorySeperatorDelimiters = new char[]
{
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar
};
private static readonly char[] _invalidPathChars = GetInvalidPathChars();
public static char[] GetInvalidPathChars()
{
char[] invalids = new char[] { '/', '\\', ':', '*', '?', '"', '<', '>', '|' };
char[] otherInvalids = Path.GetInvalidPathChars();
char[] result = new char[invalids.Length + otherInvalids.Length];
invalids.CopyTo(result, 0);
otherInvalids.CopyTo(result, invalids.Length);
return result;
}
public static ValidationResult IsFileNameValid(string fileName)
{
if (fileName.IndexOfAny(_invalidPathChars) != -1)
return ValidationResult.IllegalCharacter;
string fileNameNoExt = Path.GetFileNameWithoutExtension(fileName).ToUpperInvariant();
if (_reservedNames.Contains(fileNameNoExt))
return ValidationResult.ReservedFileName;
return ValidationResult.Ok;
}
public static ValidationResult IsPathValid(string path)
{
string? pathRoot = Path.GetPathRoot(path);
string pathNoRoot = pathRoot != null ? path[pathRoot.Length..] : path;
string[] pathParts = pathNoRoot.Split(_directorySeperatorDelimiters);
foreach (var part in pathParts)
{
if (part.IndexOfAny(_invalidPathChars) != -1)
return ValidationResult.IllegalCharacter;
if (_reservedNames.Contains(part))
return ValidationResult.ReservedDirectoryName;
}
string fileName = Path.GetFileName(path);
if (fileName.IndexOfAny(_invalidPathChars) != -1)
return ValidationResult.IllegalCharacter;
string fileNameNoExt = Path.GetFileNameWithoutExtension(path).ToUpperInvariant();
if (_reservedNames.Contains(fileNameNoExt))
return ValidationResult.ReservedFileName;
return ValidationResult.Ok;
}
}
}

2
wpfui

@ -1 +1 @@
Subproject commit 9080158ba8d496501146d1167aae910898eff9af
Subproject commit dca423b724ec24bd3377da3a27f4055ae317b50a