mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-10 15:25:42 -07:00
Custom bootstrapper themes (#4380)
* add custom bootstrappers * add avalonedit to licenses page * add gif support * add stretch & stretchdirection to images * dont create a bitmapimage for gifs * remove maxheight and maxwidth sets * remove comment * add isenabled * add more textblock properties * add markdowntextblocks * update how transform elements are stored * overhaul textbox content * dont set fontsize if not set * fix warnings * add foreground property to control * add background property to textblock * count descendants and increase element cap * add auto complete * dont display completion window if there is no data * sort schema elements and types * make ! close the completion window * add end tag auto complete * fix pos being wrong * dont treat comments as elements * add imagebrushes * follow same conventions as brushes * fix exception messages * fix them again * update schema * fix crash * now it works * wrong attribute name * add solidcolorbrush * move converters into a separate file * add lineargradientbrushes * unify handlers * update schema * add fake BloxstrapCustomBootstrapper * stop adding an extra end character * add property element auto-complete * add title attribute to custombloxstrapbootstrapper * add shapes * add string translation support * use default wpf size instead of 100x100 * update min height of window * fix verticalalignment not working * uncap height and width * add effects * move transformation handler inside frameworkelement * fix title bar effect & transformation removal * add more frameworkelement properties * add layout transform * add font properties to control * improve window border stuff * make sure file contents are in CRLF * add cornerradius to progress bar * add progressring * Update wpfui * update schema * update function names * add children check to content * make sure only one content is defined * add fontfamily * update schema * only allow file uris for images * disable backdrop * move text setter to textblock handler from base * split up creator into multiple files * turn version into a constant * add grids * cleanup converters * add IgnoreTitleBarInset * add Version to schema * reveal custom bootstrapper stuff on selection * increase listbox height * only set statustext binding in textblock * update ui * rename ZIndex to Panel.ZIndex * add stackpanel * add border * fix being unable to apply transforms on grids * rearrange and add new editor button * use snackbars for saving * add close confirmation message * use viewmodel variable * remove pointless onpropertychanged call * add version string format * start editor window in the centre * update licenses page also resized the about window so everything could fit nicely * fix border not inheriting frameworkelement * add WindowCornerPreference * add the import dialog * add an export theme button * update version number * localise CustomDialog exceptions * localise custom theme editor * localise custom theme add dialog * localise frontend * localise appearance menu page * change customtheme error strings namespace * change icons on appearance page * update button margin on appearance page
This commit is contained in:
parent
33243bfd0a
commit
9d356b0b71
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -10,6 +10,7 @@
|
||||
ByfronDialog,
|
||||
[EnumName(StaticName = "Bloxstrap")]
|
||||
FluentDialog,
|
||||
FluentAeroDialog
|
||||
FluentAeroDialog,
|
||||
CustomDialog
|
||||
}
|
||||
}
|
||||
|
8
Bloxstrap/Enums/CustomThemeTemplate.cs
Normal file
8
Bloxstrap/Enums/CustomThemeTemplate.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bloxstrap.Enums
|
||||
{
|
||||
public enum CustomThemeTemplate
|
||||
{
|
||||
Blank,
|
||||
Simple
|
||||
}
|
||||
}
|
60
Bloxstrap/Exceptions/CustomThemeException.cs
Normal file
60
Bloxstrap/Exceptions/CustomThemeException.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,8 @@
|
||||
BootstrapperStyle.ProgressDialog,
|
||||
BootstrapperStyle.LegacyDialog2011,
|
||||
BootstrapperStyle.LegacyDialog2008,
|
||||
BootstrapperStyle.VistaDialog
|
||||
BootstrapperStyle.VistaDialog,
|
||||
BootstrapperStyle.CustomDialog
|
||||
};
|
||||
}
|
||||
}
|
||||
|
10
Bloxstrap/Extensions/CustomThemeTemplateEx.cs
Normal file
10
Bloxstrap/Extensions/CustomThemeTemplateEx.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Bloxstrap.Extensions
|
||||
{
|
||||
static class CustomThemeTemplateEx
|
||||
{
|
||||
public static string GetFileName(this CustomThemeTemplate template)
|
||||
{
|
||||
return $"CustomBootstrapperTemplate_{template}.xml";
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
521
Bloxstrap/Resources/CustomBootstrapperSchema.json
Normal file
521
Bloxstrap/Resources/CustomBootstrapperSchema.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
4
Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml
Normal file
4
Bloxstrap/Resources/CustomBootstrapperTemplate_Blank.xml
Normal 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>
|
@ -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>
|
487
Bloxstrap/Resources/Strings.Designer.cs
generated
487
Bloxstrap/Resources/Strings.Designer.cs
generated
@ -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 "{0}".
|
||||
/// </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'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>
|
||||
|
@ -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>
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
151
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs
Normal file
151
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs
Normal 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
|
||||
}
|
||||
}
|
772
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs
Normal file
772
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Elements.cs
Normal 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
|
||||
}
|
||||
}
|
300
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs
Normal file
300
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Utilities.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
55
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.xaml
Normal file
55
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.xaml
Normal 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>
|
122
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.xaml.cs
Normal file
122
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
165
Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml
Normal file
165
Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml
Normal 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>
|
230
Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml.cs
Normal file
230
Bloxstrap/UI/Elements/Dialogs/AddCustomThemeDialog.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
84
Bloxstrap/UI/Elements/Editor/BootstrapperEditorWindow.xaml
Normal file
84
Bloxstrap/UI/Elements/Editor/BootstrapperEditorWindow.xaml
Normal 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>
|
580
Bloxstrap/UI/Elements/Editor/BootstrapperEditorWindow.xaml.cs
Normal file
580
Bloxstrap/UI/Elements/Editor/BootstrapperEditorWindow.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
5
Bloxstrap/UI/Style/Dark.xaml
Normal file
5
Bloxstrap/UI/Style/Dark.xaml
Normal 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>
|
189
Bloxstrap/UI/Style/Default.xaml
Normal file
189
Bloxstrap/UI/Style/Default.xaml
Normal 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>
|
63
Bloxstrap/UI/Style/Editor-Theme-Dark.xshd
Normal file
63
Bloxstrap/UI/Style/Editor-Theme-Dark.xshd
Normal 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="<!-- comment -->" />
|
||||
<Color foreground="White" name="CData" exampleText="<![CDATA[data]]>" />
|
||||
<Color foreground="White" name="DocType" exampleText="<!DOCTYPE rootElement>" />
|
||||
<Color foreground="White" name="XmlDeclaration" exampleText='<?xml version="1.0"?>' />
|
||||
<Color foreground="#569CD6" name="XmlTag" exampleText='<tag attribute="value" />' />
|
||||
<Color foreground="#9CDCFE" name="AttributeName" exampleText='<tag attribute="value" />' />
|
||||
<Color foreground="#CE9178" name="AttributeValue" exampleText='<tag attribute="value" />' />
|
||||
<Color foreground="White" name="Entity" exampleText="index.aspx?a=1&amp;b=2" />
|
||||
<Color foreground="White" name="BrokenEntity" exampleText="index.aspx?a=1&b=2" />
|
||||
|
||||
<RuleSet>
|
||||
<Span color="Comment" multiline="true">
|
||||
<Begin><!--</Begin>
|
||||
<End>--></End>
|
||||
</Span>
|
||||
<Span color="CData" multiline="true">
|
||||
<Begin><!\[CDATA\[</Begin>
|
||||
<End>]]></End>
|
||||
</Span>
|
||||
<Span color="DocType" multiline="true">
|
||||
<Begin><!DOCTYPE</Begin>
|
||||
<End>></End>
|
||||
</Span>
|
||||
<Span color="XmlDeclaration" multiline="true">
|
||||
<Begin><\?</Begin>
|
||||
<End>\?></End>
|
||||
</Span>
|
||||
<Span color="XmlTag" multiline="true">
|
||||
<Begin><</Begin>
|
||||
<End>></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>"|(?=<)</End>
|
||||
</Span>
|
||||
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
|
||||
<Begin>'</Begin>
|
||||
<End>'|(?=<)</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">
|
||||
&
|
||||
[\w\d\#]+
|
||||
;
|
||||
</Rule>
|
||||
|
||||
<Rule color="BrokenEntity">
|
||||
&
|
||||
[\w\d\#]*
|
||||
#missing ;
|
||||
</Rule>
|
||||
</RuleSet>
|
||||
</SyntaxDefinition>
|
63
Bloxstrap/UI/Style/Editor-Theme-Light.xshd
Normal file
63
Bloxstrap/UI/Style/Editor-Theme-Light.xshd
Normal 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="<!-- comment -->" />
|
||||
<Color foreground="Blue" name="CData" exampleText="<![CDATA[data]]>" />
|
||||
<Color foreground="Blue" name="DocType" exampleText="<!DOCTYPE rootElement>" />
|
||||
<Color foreground="Blue" name="XmlDeclaration" exampleText='<?xml version="1.0"?>' />
|
||||
<Color foreground="DarkMagenta" name="XmlTag" exampleText='<tag attribute="value" />' />
|
||||
<Color foreground="Red" name="AttributeName" exampleText='<tag attribute="value" />' />
|
||||
<Color foreground="Blue" name="AttributeValue" exampleText='<tag attribute="value" />' />
|
||||
<Color foreground="Teal" name="Entity" exampleText="index.aspx?a=1&amp;b=2" />
|
||||
<Color foreground="Olive" name="BrokenEntity" exampleText="index.aspx?a=1&b=2" />
|
||||
|
||||
<RuleSet>
|
||||
<Span color="Comment" multiline="true">
|
||||
<Begin><!--</Begin>
|
||||
<End>--></End>
|
||||
</Span>
|
||||
<Span color="CData" multiline="true">
|
||||
<Begin><!\[CDATA\[</Begin>
|
||||
<End>]]></End>
|
||||
</Span>
|
||||
<Span color="DocType" multiline="true">
|
||||
<Begin><!DOCTYPE</Begin>
|
||||
<End>></End>
|
||||
</Span>
|
||||
<Span color="XmlDeclaration" multiline="true">
|
||||
<Begin><\?</Begin>
|
||||
<End>\?></End>
|
||||
</Span>
|
||||
<Span color="XmlTag" multiline="true">
|
||||
<Begin><</Begin>
|
||||
<End>></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>"|(?=<)</End>
|
||||
</Span>
|
||||
<Span color="AttributeValue" multiline="true" ruleSet="EntitySet">
|
||||
<Begin>'</Begin>
|
||||
<End>'|(?=<)</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">
|
||||
&
|
||||
[\w\d\#]+
|
||||
;
|
||||
</Rule>
|
||||
|
||||
<Rule color="BrokenEntity">
|
||||
&
|
||||
[\w\d\#]*
|
||||
#missing ;
|
||||
</Rule>
|
||||
</RuleSet>
|
||||
</SyntaxDefinition>
|
5
Bloxstrap/UI/Style/Light.xaml
Normal file
5
Bloxstrap/UI/Style/Light.xaml
Normal 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>
|
68
Bloxstrap/UI/ViewModels/Dialogs/AddCustomThemeViewModel.cs
Normal file
68
Bloxstrap/UI/ViewModels/Dialogs/AddCustomThemeViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
104
Bloxstrap/Utility/PathValidator.cs
Normal file
104
Bloxstrap/Utility/PathValidator.cs
Normal 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
2
wpfui
@ -1 +1 @@
|
||||
Subproject commit 9080158ba8d496501146d1167aae910898eff9af
|
||||
Subproject commit dca423b724ec24bd3377da3a27f4055ae317b50a
|
Loading…
Reference in New Issue
Block a user