mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-19 00:51:30 -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>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ui:ThemesDictionary Theme="Dark" />
|
<ui:ThemesDictionary Theme="Dark" />
|
||||||
<ui:ControlsDictionary />
|
<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>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
<FontFamily x:Key="Rubik">pack://application:,,,/Resources/Fonts/#Rubik Light</FontFamily>
|
<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\Information.png" />
|
||||||
<Resource Include="Resources\MessageBox\Question.png" />
|
<Resource Include="Resources\MessageBox\Question.png" />
|
||||||
<Resource Include="Resources\MessageBox\Warning.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>
|
||||||
|
|
||||||
<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\Icon2008.ico" />
|
||||||
<EmbeddedResource Include="Resources\Icon2011.ico" />
|
<EmbeddedResource Include="Resources\Icon2011.ico" />
|
||||||
<EmbeddedResource Include="Resources\Icon2017.ico" />
|
<EmbeddedResource Include="Resources\Icon2017.ico" />
|
||||||
@ -49,6 +54,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
<PackageReference Include="Markdig" Version="0.40.0" />
|
<PackageReference Include="Markdig" Version="0.40.0" />
|
||||||
@ -59,6 +65,7 @@
|
|||||||
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
|
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageReference Include="System.Resources.ResourceManager" Version="4.3.0" />
|
<PackageReference Include="System.Resources.ResourceManager" Version="4.3.0" />
|
||||||
|
<PackageReference Include="XamlAnimatedGif" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
ByfronDialog,
|
ByfronDialog,
|
||||||
[EnumName(StaticName = "Bloxstrap")]
|
[EnumName(StaticName = "Bloxstrap")]
|
||||||
FluentDialog,
|
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.ProgressDialog,
|
||||||
BootstrapperStyle.LegacyDialog2011,
|
BootstrapperStyle.LegacyDialog2011,
|
||||||
BootstrapperStyle.LegacyDialog2008,
|
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 UseFastFlagManager { get; set; } = true;
|
||||||
public bool WPFSoftwareRender { get; set; } = false;
|
public bool WPFSoftwareRender { get; set; } = false;
|
||||||
public bool EnableAnalytics { get; set; } = true;
|
public bool EnableAnalytics { get; set; } = true;
|
||||||
|
public string? SelectedCustomTheme { get; set; } = null;
|
||||||
|
|
||||||
// integration configuration
|
// integration configuration
|
||||||
public bool EnableActivityTracking { get; set; } = true;
|
public bool EnableActivityTracking { get; set; } = true;
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
public static string Integrations { get; private set; } = "";
|
public static string Integrations { get; private set; } = "";
|
||||||
public static string Versions { get; private set; } = "";
|
public static string Versions { get; private set; } = "";
|
||||||
public static string Modifications { get; private set; } = "";
|
public static string Modifications { get; private set; } = "";
|
||||||
|
public static string CustomThemes { get; private set; } = "";
|
||||||
|
|
||||||
public static string Application { get; private set; } = "";
|
public static string Application { get; private set; } = "";
|
||||||
|
|
||||||
@ -37,6 +38,7 @@
|
|||||||
Integrations = Path.Combine(Base, "Integrations");
|
Integrations = Path.Combine(Base, "Integrations");
|
||||||
Versions = Path.Combine(Base, "Versions");
|
Versions = Path.Combine(Base, "Versions");
|
||||||
Modifications = Path.Combine(Base, "Modifications");
|
Modifications = Path.Combine(Base, "Modifications");
|
||||||
|
CustomThemes = Path.Combine(Base, "CustomThemes");
|
||||||
|
|
||||||
Application = Path.Combine(Base, $"{App.ProjectName}.exe");
|
Application = Path.Combine(Base, $"{App.ProjectName}.exe");
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,10 @@ namespace Bloxstrap
|
|||||||
await stream.CopyToAsync(memoryStream);
|
await stream.CopyToAsync(memoryStream);
|
||||||
return memoryStream.ToArray();
|
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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Custom.
|
/// Looks up a localized string similar to Custom.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Export.
|
/// Looks up a localized string similar to Export.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Import from file.
|
/// Looks up a localized string similar to Import from file.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Reset.
|
/// Looks up a localized string similar to Reset.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Value.
|
/// Looks up a localized string similar to Value.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Add Fast Flag.
|
/// Looks up a localized string similar to Add Fast Flag.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Bloxstrap (Glass).
|
/// Looks up a localized string similar to Bloxstrap (Glass).
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Catmoji.
|
/// Looks up a localized string similar to Catmoji.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to BSD 2-Clause License.
|
/// Looks up a localized string similar to BSD 2-Clause License.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Configure how Bloxstrap should look..
|
/// Looks up a localized string similar to Configure how Bloxstrap should look..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1239,6 +1239,9 @@ Would you like to enable test mode?</value>
|
|||||||
<data name="Dialog.Exception.Version" xml:space="preserve">
|
<data name="Dialog.Exception.Version" xml:space="preserve">
|
||||||
<value>Version {0}</value>
|
<value>Version {0}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Enums.BootstrapperStyle.CustomDialog" xml:space="preserve">
|
||||||
|
<value>Custom</value>
|
||||||
|
</data>
|
||||||
<data name="Bootstrapper.FilesInUse" xml:space="preserve">
|
<data name="Bootstrapper.FilesInUse" xml:space="preserve">
|
||||||
<value>Bloxstrap tried to upgrade Roblox but can't because Roblox's files are still in use.
|
<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">
|
<data name="Bootstrapper.ModificationsFailed.Message" xml:space="preserve">
|
||||||
<value>Not all modifications will be present in the current launch.</value>
|
<value>Not all modifications will be present in the current launch.</value>
|
||||||
</data>
|
</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>
|
</root>
|
@ -10,8 +10,8 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="{x:Static resources:Strings.About_Title}"
|
Title="{x:Static resources:Strings.About_Title}"
|
||||||
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
|
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
|
||||||
MinWidth="740"
|
MinWidth="800"
|
||||||
Width="740"
|
Width="800"
|
||||||
Height="440"
|
Height="440"
|
||||||
ExtendsContentIntoTitleBar="True"
|
ExtendsContentIntoTitleBar="True"
|
||||||
WindowBackdropType="Mica"
|
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}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardAction>
|
</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">
|
<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>
|
<StackPanel>
|
||||||
<TextBlock FontSize="14" Text="DiscordRPC" />
|
<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}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardAction>
|
</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>
|
<StackPanel>
|
||||||
<TextBlock FontSize="13" Text="Roblox Studio Mod Manager" />
|
<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}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardAction>
|
</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>
|
<StackPanel>
|
||||||
<TextBlock FontSize="13" Text="SharpZipLib" />
|
<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}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardAction>
|
</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>
|
<StackPanel>
|
||||||
<TextBlock FontSize="14" Text="Markdig" />
|
<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}" />
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_BSD2}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:CardAction>
|
</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>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:UiPage>
|
</ui:UiPage>
|
||||||
|
@ -18,9 +18,15 @@ namespace Bloxstrap.UI.Elements.Base
|
|||||||
|
|
||||||
public void ApplyTheme()
|
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.SetTheme(App.Settings.Prop.Theme.GetFinal() == Enums.Theme.Dark ? ThemeType.Dark : ThemeType.Light);
|
||||||
_themeService.SetSystemAccent();
|
_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
|
#if QA_BUILD
|
||||||
this.BorderBrush = System.Windows.Media.Brushes.Red;
|
this.BorderBrush = System.Windows.Media.Brushes.Red;
|
||||||
this.BorderThickness = new Thickness(4);
|
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:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
|
||||||
xmlns:resources="clr-namespace:Bloxstrap.Resources"
|
xmlns:resources="clr-namespace:Bloxstrap.Resources"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="640" d:DesignWidth="800"
|
d:DesignHeight="900" d:DesignWidth="800"
|
||||||
Title="AppearancePage"
|
Title="AppearancePage"
|
||||||
Scrollable="True">
|
Scrollable="True">
|
||||||
<StackPanel Margin="0,0,14,14">
|
<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}" />
|
<ui:Button Grid.Column="1" Content="{x:Static resources:Strings.Menu_Appearance_Preview}" HorizontalAlignment="Stretch" Margin="0,16,0,0" Command="{Binding PreviewBootstrapperCommand}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<controls:OptionControl
|
<ui:CardExpander Margin="0,8,0,0" IsExpanded="{Binding Path=CustomThemesExpanded, Mode=OneWay}" Style="{StaticResource NoUserExpansionCardExpanderStyle}">
|
||||||
Header="{x:Static resources:Strings.Menu_Appearance_Style_Title}"
|
<ui:CardExpander.Header>
|
||||||
Description="{x:Static resources:Strings.Menu_Appearance_Style_Description}">
|
<Grid>
|
||||||
<ComboBox Width="200" Padding="10,5,10,5" ItemsSource="{Binding Dialogs, Mode=OneTime}" Text="{Binding Dialog, Mode=TwoWay}">
|
<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>
|
<ComboBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
|
<TextBlock Text="{Binding Path=., Converter={StaticResource EnumNameConverter}}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ComboBox.ItemTemplate>
|
</ComboBox.ItemTemplate>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</controls:OptionControl>
|
</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
|
<controls:OptionControl
|
||||||
x:Name="IconSelector"
|
x:Name="IconSelector"
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using Bloxstrap.UI.ViewModels.Settings;
|
using Bloxstrap.UI.ViewModels.Settings;
|
||||||
|
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
namespace Bloxstrap.UI.Elements.Settings.Pages
|
namespace Bloxstrap.UI.Elements.Settings.Pages
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -12,5 +14,16 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
|
|||||||
DataContext = new AppearanceViewModel(this);
|
DataContext = new AppearanceViewModel(this);
|
||||||
InitializeComponent();
|
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)
|
public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style)
|
||||||
{
|
{
|
||||||
return style switch
|
return style switch
|
||||||
@ -70,6 +96,7 @@ namespace Bloxstrap.UI
|
|||||||
BootstrapperStyle.ByfronDialog => new ByfronDialog(),
|
BootstrapperStyle.ByfronDialog => new ByfronDialog(),
|
||||||
BootstrapperStyle.FluentDialog => new FluentDialog(false),
|
BootstrapperStyle.FluentDialog => new FluentDialog(false),
|
||||||
BootstrapperStyle.FluentAeroDialog => new FluentDialog(true),
|
BootstrapperStyle.FluentAeroDialog => new FluentDialog(true),
|
||||||
|
BootstrapperStyle.CustomDialog => GetCustomBootstrapper(),
|
||||||
_ => new FluentDialog(false)
|
_ => 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 System.Windows.Input;
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
|
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
using Bloxstrap.UI.Elements.Settings;
|
using Bloxstrap.UI.Elements.Settings;
|
||||||
|
using Bloxstrap.UI.Elements.Editor;
|
||||||
|
using Bloxstrap.UI.Elements.Dialogs;
|
||||||
|
|
||||||
namespace Bloxstrap.UI.ViewModels.Settings
|
namespace Bloxstrap.UI.ViewModels.Settings
|
||||||
{
|
{
|
||||||
@ -18,6 +21,12 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper);
|
public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper);
|
||||||
public ICommand BrowseCustomIconLocationCommand => new RelayCommand(BrowseCustomIconLocation);
|
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()
|
private void PreviewBootstrapper()
|
||||||
{
|
{
|
||||||
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
|
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
|
||||||
@ -51,6 +60,8 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
|
|
||||||
foreach (var entry in BootstrapperIconEx.Selections)
|
foreach (var entry in BootstrapperIconEx.Selections)
|
||||||
Icons.Add(new BootstrapperIconEntry { IconType = entry });
|
Icons.Add(new BootstrapperIconEntry { IconType = entry });
|
||||||
|
|
||||||
|
PopulateCustomThemes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Theme> Themes { get; } = Enum.GetValues(typeof(Theme)).Cast<Theme>();
|
public IEnumerable<Theme> Themes { get; } = Enum.GetValues(typeof(Theme)).Cast<Theme>();
|
||||||
@ -78,8 +89,14 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
public BootstrapperStyle Dialog
|
public BootstrapperStyle Dialog
|
||||||
{
|
{
|
||||||
get => App.Settings.Prop.BootstrapperStyle;
|
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 ObservableCollection<BootstrapperIconEntry> Icons { get; set; } = new();
|
||||||
|
|
||||||
@ -116,5 +133,183 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
OnPropertyChanged(nameof(Icons));
|
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