mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 10:01:27 -07:00
add custom bootstrappers
This commit is contained in:
parent
2e63da5779
commit
8ee6beb3de
@ -28,6 +28,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\CustomBootstrapperTemplate.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 +50,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AvalonEdit" Version="6.3.0.90" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
<PackageReference Include="Markdig" Version="0.37.0" />
|
<PackageReference Include="Markdig" Version="0.37.0" />
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
ByfronDialog,
|
ByfronDialog,
|
||||||
[EnumName(StaticName = "Bloxstrap")]
|
[EnumName(StaticName = "Bloxstrap")]
|
||||||
FluentDialog,
|
FluentDialog,
|
||||||
FluentAeroDialog
|
FluentAeroDialog,
|
||||||
|
CustomDialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
BootstrapperStyle.ProgressDialog,
|
BootstrapperStyle.ProgressDialog,
|
||||||
BootstrapperStyle.LegacyDialog2011,
|
BootstrapperStyle.LegacyDialog2011,
|
||||||
BootstrapperStyle.LegacyDialog2008,
|
BootstrapperStyle.LegacyDialog2008,
|
||||||
BootstrapperStyle.VistaDialog
|
BootstrapperStyle.VistaDialog,
|
||||||
|
BootstrapperStyle.CustomDialog
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 Modifications { get; private set; } = "";
|
public static string Modifications { get; private set; } = "";
|
||||||
public static string Roblox { get; private set; } = "";
|
public static string Roblox { 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");
|
||||||
Modifications = Path.Combine(Base, "Modifications");
|
Modifications = Path.Combine(Base, "Modifications");
|
||||||
Roblox = Path.Combine(Base, "Roblox");
|
Roblox = Path.Combine(Base, "Roblox");
|
||||||
|
CustomThemes = Path.Combine(Base, "CustomThemes");
|
||||||
|
|
||||||
Application = Path.Combine(Base, $"{App.ProjectName}.exe");
|
Application = Path.Combine(Base, $"{App.ProjectName}.exe");
|
||||||
}
|
}
|
||||||
|
4
Bloxstrap/Resources/CustomBootstrapperTemplate.xml
Normal file
4
Bloxstrap/Resources/CustomBootstrapperTemplate.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<BloxstrapCustomBootstrapper Version="0" Height="200" Width="500">
|
||||||
|
<!-- Put UI elements here -->
|
||||||
|
<!-- Examples of custom bootstrappers can be found at https://github.com/bloxstraplabs/custom-bootstrapper-examples -->
|
||||||
|
</BloxstrapCustomBootstrapper>
|
9
Bloxstrap/Resources/Strings.Designer.cs
generated
9
Bloxstrap/Resources/Strings.Designer.cs
generated
@ -1084,6 +1084,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>
|
||||||
|
@ -1239,4 +1239,7 @@ 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>
|
||||||
</root>
|
</root>
|
602
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs
Normal file
602
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.Creator.cs
Normal file
@ -0,0 +1,602 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using Wpf.Ui.Markup;
|
||||||
|
|
||||||
|
namespace Bloxstrap.UI.Elements.Bootstrapper
|
||||||
|
{
|
||||||
|
public partial class CustomDialog
|
||||||
|
{
|
||||||
|
private const int MaxElements = 50;
|
||||||
|
|
||||||
|
private static ThicknessConverter? _thicknessConverter = null;
|
||||||
|
private static ThicknessConverter ThicknessConverter { get => _thicknessConverter ??= new ThicknessConverter(); }
|
||||||
|
|
||||||
|
private static BrushConverter? _brushConverter = null;
|
||||||
|
private static BrushConverter BrushConverter { get => _brushConverter ??= new BrushConverter(); }
|
||||||
|
|
||||||
|
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 void HandleXmlElementDelegate(CustomDialog dialog, XElement xmlElement);
|
||||||
|
delegate void HandleXmlTransformationElementDelegate(TransformGroup group, XElement xmlElement);
|
||||||
|
|
||||||
|
private static Dictionary<string, HandleXmlElementDelegate> _elementHandlerMap = new Dictionary<string, HandleXmlElementDelegate>()
|
||||||
|
{
|
||||||
|
//["BloxstrapCustomBootstrapper"] = HandleXmlElement_BloxstrapCustomBootstrapper,
|
||||||
|
["TitleBar"] = HandleXmlElement_TitleBar,
|
||||||
|
["Button"] = HandleXmlElement_Button,
|
||||||
|
["ProgressBar"] = HandleXmlElement_ProgressBar,
|
||||||
|
["TextBlock"] = HandleXmlElement_TextBlock,
|
||||||
|
["Image"] = HandleXmlElement_Image
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Dictionary<string, HandleXmlTransformationElementDelegate> _transformationHandlerMap = new Dictionary<string, HandleXmlTransformationElementDelegate>()
|
||||||
|
{
|
||||||
|
["ScaleTransform"] = HandleXmlTransformationElement_ScaleTransform,
|
||||||
|
["SkewTransform"] = HandleXmlTransformationElement_SkewTransform,
|
||||||
|
["RotateTransform"] = HandleXmlTransformationElement_RotateTransform,
|
||||||
|
["TranslateTransform"] = HandleXmlTransformationElement_TranslateTransform
|
||||||
|
};
|
||||||
|
|
||||||
|
#region Utilities
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
// Cast ConvertFromString(string text) : object to (T)
|
||||||
|
//return (T?)converter.ConvertFromString(input);
|
||||||
|
return (T?)converter.ConvertFromInvariantString(input);
|
||||||
|
}
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
catch (NotSupportedException)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetXmlAttribute(XElement element, string attributeName, string? defaultValue = null)
|
||||||
|
{
|
||||||
|
var attribute = element.Attribute(attributeName);
|
||||||
|
|
||||||
|
if (attribute == null)
|
||||||
|
{
|
||||||
|
if (defaultValue != null)
|
||||||
|
return defaultValue;
|
||||||
|
|
||||||
|
throw new Exception($"Element {element.Name} is missing the {attributeName} attribute");
|
||||||
|
}
|
||||||
|
|
||||||
|
return attribute.Value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T ParseXmlAttribute<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 Exception($"Element {element.Name} is missing the {attributeName} attribute");
|
||||||
|
}
|
||||||
|
|
||||||
|
T? parsed = ConvertValue<T>(attribute.Value);
|
||||||
|
if (parsed == null)
|
||||||
|
throw new Exception($"{element.Name} height is not a valid {typeof(T).Name}");
|
||||||
|
|
||||||
|
return (T)parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateXmlElement(string elementName, string attributeName, int value, int? min = null, int? max = null)
|
||||||
|
{
|
||||||
|
if (min != null && value < min)
|
||||||
|
throw new Exception($"{elementName} {attributeName} must be larger than {min}");
|
||||||
|
if (max != null && value > max)
|
||||||
|
throw new Exception($"{elementName} {attributeName} must be smaller than {max}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateXmlElement(string elementName, string attributeName, double value, double? min = null, double? max = null)
|
||||||
|
{
|
||||||
|
if (min != null && value < min)
|
||||||
|
throw new Exception($"{elementName} {attributeName} must be larger than {min}");
|
||||||
|
if (max != null && value > max)
|
||||||
|
throw new Exception($"{elementName} {attributeName} must be smaller than {max}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can't do numeric only generics in .NET 6. The feature is exclusive to .NET 7+.
|
||||||
|
private static double ParseXmlAttributeClamped(XElement element, string attributeName, double? defaultValue = null, double? min = null, double? max = null)
|
||||||
|
{
|
||||||
|
double value = ParseXmlAttribute<double>(element, attributeName, defaultValue);
|
||||||
|
ValidateXmlElement(element.Name.ToString(), attributeName, value, min, max);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 object? GetThicknessFromXElement(XElement xmlElement, string attributeName)
|
||||||
|
{
|
||||||
|
string? attributeValue = xmlElement.Attribute(attributeName)?.Value?.ToString();
|
||||||
|
if (attributeValue == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
object? thickness;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
thickness = ThicknessConverter.ConvertFromInvariantString(attributeValue);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"{xmlElement.Name} has invalid {attributeName}: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thickness == null)
|
||||||
|
throw new Exception($"{xmlElement.Name} has invalid {attributeName}");
|
||||||
|
|
||||||
|
return thickness;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FontWeight GetFontWeightFromXElement(XElement element)
|
||||||
|
{
|
||||||
|
string? value = element.Attribute("FontWeight")?.Value?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
value = "Normal";
|
||||||
|
|
||||||
|
// bruh
|
||||||
|
// https://learn.microsoft.com/en-us/dotnet/api/system.windows.fontweights?view=windowsdesktop-6.0
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case "Thin":
|
||||||
|
return FontWeights.Thin;
|
||||||
|
|
||||||
|
case "ExtraLight":
|
||||||
|
case "UltraLight":
|
||||||
|
return FontWeights.ExtraLight;
|
||||||
|
|
||||||
|
case "Medium":
|
||||||
|
return FontWeights.Medium;
|
||||||
|
|
||||||
|
case "Normal":
|
||||||
|
case "Regular":
|
||||||
|
return FontWeights.Normal;
|
||||||
|
|
||||||
|
case "DemiBold":
|
||||||
|
case "SemiBold":
|
||||||
|
return FontWeights.DemiBold;
|
||||||
|
|
||||||
|
case "Bold":
|
||||||
|
return FontWeights.Bold;
|
||||||
|
|
||||||
|
case "ExtraBold":
|
||||||
|
case "UltraBold":
|
||||||
|
return FontWeights.ExtraBold;
|
||||||
|
|
||||||
|
case "Black":
|
||||||
|
case "Heavy":
|
||||||
|
return FontWeights.Black;
|
||||||
|
|
||||||
|
case "ExtraBlack":
|
||||||
|
case "UltraBlack":
|
||||||
|
return FontWeights.UltraBlack;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception($"{element.Name} Unknown FontWeight {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FontStyle GetFontStyleFromXElement(XElement element)
|
||||||
|
{
|
||||||
|
string? value = element.Attribute("FontStyle")?.Value?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
value = "Normal";
|
||||||
|
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case "Normal":
|
||||||
|
return FontStyles.Normal;
|
||||||
|
|
||||||
|
case "Italic":
|
||||||
|
return FontStyles.Italic;
|
||||||
|
|
||||||
|
case "Oblique":
|
||||||
|
return FontStyles.Oblique;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception($"{element.Name} Unknown FontStyle {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 Exception($"{element.Name} has invalid {attributeName}: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Transformation Elements
|
||||||
|
private static void HandleXmlTransformationElement_ScaleTransform(TransformGroup group, 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);
|
||||||
|
|
||||||
|
group.Children.Add(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlTransformationElement_SkewTransform(TransformGroup group, 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);
|
||||||
|
|
||||||
|
group.Children.Add(st);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlTransformationElement_RotateTransform(TransformGroup group, 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);
|
||||||
|
|
||||||
|
group.Children.Add(rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlTransformationElement_TranslateTransform(TransformGroup group, XElement xmlElement)
|
||||||
|
{
|
||||||
|
var tt = new TranslateTransform();
|
||||||
|
|
||||||
|
tt.X = ParseXmlAttribute<double>(xmlElement, "X", 0);
|
||||||
|
tt.Y = ParseXmlAttribute<double>(xmlElement, "Y", 0);
|
||||||
|
|
||||||
|
group.Children.Add(tt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlTransformation(TransformGroup group, XElement xmlElement)
|
||||||
|
{
|
||||||
|
if (!_transformationHandlerMap.ContainsKey(xmlElement.Name.ToString()))
|
||||||
|
throw new Exception($"Unknown transformation {xmlElement.Name}");
|
||||||
|
|
||||||
|
_transformationHandlerMap[xmlElement.Name.ToString()](group, xmlElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyTransformations_UIElement(UIElement uiElement, XElement xmlElement)
|
||||||
|
{
|
||||||
|
if (!xmlElement.HasElements)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tg = new TransformGroup();
|
||||||
|
|
||||||
|
foreach (var child in xmlElement.Elements())
|
||||||
|
HandleXmlTransformation(tg, child);
|
||||||
|
|
||||||
|
if (tg.Children.Any())
|
||||||
|
uiElement.RenderTransform = tg;
|
||||||
|
}
|
||||||
|
#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);
|
||||||
|
|
||||||
|
object? margin = GetThicknessFromXElement(xmlElement, "Margin");
|
||||||
|
if (margin != null)
|
||||||
|
uiElement.Margin = (Thickness)margin;
|
||||||
|
|
||||||
|
uiElement.Height = ParseXmlAttributeClamped(xmlElement, "Height", defaultValue: 100.0, min: 0, max: 1000);
|
||||||
|
uiElement.Width = ParseXmlAttributeClamped(xmlElement, "Width", defaultValue: 100.0, min: 0, max: 1000);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
int zIndex = ParseXmlAttributeClamped(xmlElement, "ZIndex", defaultValue: 0, min: 0, max: 1000);
|
||||||
|
Panel.SetZIndex(uiElement, zIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// TODO: this isn't working for BloxstrapCustomBootstrapper. likely because of wpf.ui's themeservice.
|
||||||
|
object? foregroundBrush = GetBrushFromXElement(xmlElement, "Background");
|
||||||
|
if (foregroundBrush is Brush)
|
||||||
|
uiElement.Background = (Brush)foregroundBrush;
|
||||||
|
else if (foregroundBrush is string)
|
||||||
|
uiElement.SetResourceReference(Control.BackgroundProperty, foregroundBrush);
|
||||||
|
|
||||||
|
object? borderBrush = GetBrushFromXElement(xmlElement, "BorderBrush");
|
||||||
|
if (borderBrush is Brush)
|
||||||
|
uiElement.BorderBrush = (Brush)borderBrush;
|
||||||
|
else if (borderBrush is string)
|
||||||
|
uiElement.SetResourceReference(Control.BorderBrushProperty, borderBrush);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlElement_BloxstrapCustomBootstrapper(CustomDialog dialog, XElement xmlElement)
|
||||||
|
{
|
||||||
|
xmlElement.SetAttributeValue("Visibility", "Collapsed"); // don't show the bootstrapper yet!!!
|
||||||
|
HandleXmlElement_Control(dialog, dialog, xmlElement);
|
||||||
|
|
||||||
|
var theme = ParseXmlAttribute<Theme>(xmlElement, "Theme", Theme.Default);
|
||||||
|
dialog.Resources.MergedDictionaries.Clear();
|
||||||
|
dialog.Resources.MergedDictionaries.Add(new ThemesDictionary() { Theme = theme.GetFinal() == Theme.Dark ? Wpf.Ui.Appearance.ThemeType.Dark : Wpf.Ui.Appearance.ThemeType.Light });
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
dialog.MaxHeight = dialog.Height;
|
||||||
|
dialog.MaxWidth = dialog.Width;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlElement_TitleBar(CustomDialog dialog, XElement xmlElement)
|
||||||
|
{
|
||||||
|
xmlElement.SetAttributeValue("Name", "TitleBar"); // prevent two titlebars from existing
|
||||||
|
HandleXmlElement_Control(dialog, dialog.RootTitleBar, xmlElement);
|
||||||
|
|
||||||
|
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.Title = title;
|
||||||
|
dialog.RootTitleBar.Title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlElement_Button(CustomDialog dialog, XElement xmlElement)
|
||||||
|
{
|
||||||
|
var button = new Button();
|
||||||
|
HandleXmlElement_Control(dialog, button, xmlElement);
|
||||||
|
|
||||||
|
button.Content = xmlElement.Attribute("Text")?.Value?.ToString();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTransformations_UIElement(button, xmlElement);
|
||||||
|
|
||||||
|
dialog.ElementGrid.Children.Add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlElement_ProgressBar(CustomDialog dialog, XElement xmlElement)
|
||||||
|
{
|
||||||
|
var progressBar = new ProgressBar();
|
||||||
|
HandleXmlElement_Control(dialog, progressBar, xmlElement);
|
||||||
|
|
||||||
|
progressBar.IsIndeterminate = ParseXmlAttribute<bool>(xmlElement, "IsIndeterminate", false);
|
||||||
|
|
||||||
|
progressBar.Value = ParseXmlAttribute<double>(xmlElement, "Value", 0);
|
||||||
|
progressBar.Maximum = ParseXmlAttribute<double>(xmlElement, "Maximum", 100);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTransformations_UIElement(progressBar, xmlElement);
|
||||||
|
|
||||||
|
dialog.ElementGrid.Children.Add(progressBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlElement_TextBlock(CustomDialog dialog, XElement xmlElement)
|
||||||
|
{
|
||||||
|
var textBlock = new TextBlock();
|
||||||
|
HandleXmlElement_FrameworkElement(dialog, textBlock, xmlElement);
|
||||||
|
|
||||||
|
textBlock.Text = xmlElement.Attribute("Text")?.Value;
|
||||||
|
|
||||||
|
object? foregroundBrush = GetBrushFromXElement(xmlElement, "Foreground");
|
||||||
|
if (foregroundBrush is Brush)
|
||||||
|
textBlock.Foreground = (Brush)foregroundBrush;
|
||||||
|
else if (foregroundBrush is string)
|
||||||
|
textBlock.SetResourceReference(TextBlock.ForegroundProperty, foregroundBrush);
|
||||||
|
|
||||||
|
textBlock.FontSize = ParseXmlAttribute<double>(xmlElement, "FontSize", 12);
|
||||||
|
textBlock.FontWeight = GetFontWeightFromXElement(xmlElement);
|
||||||
|
textBlock.FontStyle = GetFontStyleFromXElement(xmlElement);
|
||||||
|
|
||||||
|
textBlock.TextAlignment = ParseXmlAttribute<TextAlignment>(xmlElement, "TextAlignment", TextAlignment.Center);
|
||||||
|
|
||||||
|
if (xmlElement.Attribute("Name")?.Value == "StatusText")
|
||||||
|
{
|
||||||
|
Binding textBinding = new Binding("Message") { Mode = BindingMode.OneWay };
|
||||||
|
BindingOperations.SetBinding(textBlock, TextBlock.TextProperty, textBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTransformations_UIElement(textBlock, xmlElement);
|
||||||
|
|
||||||
|
dialog.ElementGrid.Children.Add(textBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleXmlElement_Image(CustomDialog dialog, XElement xmlElement)
|
||||||
|
{
|
||||||
|
var image = new Image();
|
||||||
|
HandleXmlElement_FrameworkElement(dialog, image, xmlElement);
|
||||||
|
|
||||||
|
string sourcePath = GetXmlAttribute(xmlElement, "Source");
|
||||||
|
sourcePath = sourcePath.Replace("theme://", $"{dialog.ThemeDir}\\");
|
||||||
|
|
||||||
|
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality); // should this be modifiable by the user?
|
||||||
|
|
||||||
|
if (sourcePath == "{Icon}")
|
||||||
|
{
|
||||||
|
// bind the icon property
|
||||||
|
Binding binding = new Binding("Icon") { Mode = BindingMode.OneWay };
|
||||||
|
BindingOperations.SetBinding(image, Image.SourceProperty, binding);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(sourcePath, UriKind.RelativeOrAbsolute, out Uri? result))
|
||||||
|
throw new Exception("Image failed to parse Source as Uri");
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
throw new Exception("Image Source uri is null");
|
||||||
|
|
||||||
|
BitmapImage bitmapImage;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bitmapImage = new BitmapImage(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Image Failed to create BitmapImage: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
image.Source = bitmapImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTransformations_UIElement(image, xmlElement);
|
||||||
|
|
||||||
|
dialog.ElementGrid.Children.Add(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleXml(CustomDialog dialog, XElement xmlElement)
|
||||||
|
{
|
||||||
|
if (!_elementHandlerMap.ContainsKey(xmlElement.Name.ToString()))
|
||||||
|
throw new Exception($"Unknown element {xmlElement.Name}");
|
||||||
|
|
||||||
|
_elementHandlerMap[xmlElement.Name.ToString()](dialog, xmlElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleXmlBase(XElement xml)
|
||||||
|
{
|
||||||
|
if (_initialised)
|
||||||
|
throw new Exception("Custom dialog has already been initialised");
|
||||||
|
|
||||||
|
if (xml.Name != "BloxstrapCustomBootstrapper")
|
||||||
|
throw new Exception("XML root is not a BloxstrapCustomBootstrapper");
|
||||||
|
|
||||||
|
if (xml.Attribute("Version")?.Value != "0")
|
||||||
|
throw new Exception("Unknown BloxstrapCustomBootstrapper version");
|
||||||
|
|
||||||
|
if (xml.Elements().Count() > MaxElements)
|
||||||
|
throw new Exception($"Custom bootstrappers can have a maximum of {MaxElements} elements");
|
||||||
|
|
||||||
|
_initialised = true;
|
||||||
|
|
||||||
|
// handle root
|
||||||
|
HandleXmlElement_BloxstrapCustomBootstrapper(this, xml);
|
||||||
|
|
||||||
|
// handle everything else
|
||||||
|
foreach (var child in xml.Elements())
|
||||||
|
HandleXml(this, child);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public APIs
|
||||||
|
public void ApplyCustomTheme(string name, string contents)
|
||||||
|
{
|
||||||
|
ThemeDir = 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 Exception($"XML parse failed: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleXmlBase(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyCustomTheme(string name)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(Paths.CustomThemes, name, "Theme.xml");
|
||||||
|
|
||||||
|
ApplyCustomTheme(name, File.ReadAllText(path));
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
51
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.xaml
Normal file
51
Bloxstrap/UI/Elements/Bootstrapper/CustomDialog.xaml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<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"
|
||||||
|
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
|
||||||
|
ExtendsContentIntoTitleBar="True"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowBackdropType="Mica"
|
||||||
|
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="Auto" />
|
||||||
|
</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
|
||||||
|
}
|
||||||
|
}
|
69
Bloxstrap/UI/Elements/Editor/BootstrapperEditorWindow.xaml
Normal file
69
Bloxstrap/UI/Elements/Editor/BootstrapperEditorWindow.xaml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<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: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}"
|
||||||
|
ExtendsContentIntoTitleBar="True"
|
||||||
|
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"
|
||||||
|
SyntaxHighlighting="XML"
|
||||||
|
TextChanged="OnCodeChanged" />
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="10"
|
||||||
|
HorizontalAlignment="Center">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<ui:Button
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="0,0,5,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Command="{Binding Path=PreviewCommand, Mode=OneTime}"
|
||||||
|
Content="Preview" />
|
||||||
|
|
||||||
|
<ui:Button
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="5,0,0,0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Command="{Binding Path=SaveCommand, Mode=OneTime}"
|
||||||
|
Content="Save" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</base:WpfUiWindow>
|
@ -0,0 +1,31 @@
|
|||||||
|
using Bloxstrap.UI.Elements.Base;
|
||||||
|
using Bloxstrap.UI.ViewModels.Editor;
|
||||||
|
|
||||||
|
namespace Bloxstrap.UI.Elements.Editor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for BootstrapperEditorWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class BootstrapperEditorWindow : WpfUiWindow
|
||||||
|
{
|
||||||
|
public BootstrapperEditorWindow(string name)
|
||||||
|
{
|
||||||
|
var viewModel = new BootstrapperEditorWindowViewModel();
|
||||||
|
viewModel.Name = name;
|
||||||
|
viewModel.Title = $"Editing \"{name}\"";
|
||||||
|
viewModel.Code = File.ReadAllText(Path.Combine(Paths.CustomThemes, name, "Theme.xml"));
|
||||||
|
|
||||||
|
DataContext = viewModel;
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
UIXML.Text = viewModel.Code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCodeChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
BootstrapperEditorWindowViewModel viewModel = (BootstrapperEditorWindowViewModel)DataContext;
|
||||||
|
viewModel.Code = UIXML.Text;
|
||||||
|
viewModel.OnPropertyChanged(nameof(viewModel.Code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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">
|
||||||
@ -114,5 +114,59 @@
|
|||||||
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,4,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Appearance_CustomisationIcon_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,4,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Appearance_CustomisationIcon_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ui:CardExpander>
|
</ui:CardExpander>
|
||||||
|
|
||||||
|
<TextBlock Text="Custom Themes" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
|
||||||
|
<TextBlock Text="pizzaboxer help me make this look better" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||||
|
<Grid Margin="0,8,0,0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ListBox x:Name="CustomThemesListBox" Height="200" 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,4,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ui:Button Grid.Column="0" Margin="0,0,2,0" Icon="Add28" Content="Rename" HorizontalAlignment="Stretch" Command="{Binding RenameCustomThemeCommand, Mode=OneTime}" />
|
||||||
|
<ui:Button Grid.Column="1" Margin="2,0,0,0" Icon="Add28" Content="Edit" HorizontalAlignment="Stretch" Command="{Binding EditCustomThemeCommand, Mode=OneTime}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="No custom theme selected." 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>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ui:UiPage>
|
</ui:UiPage>
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,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 Exception("No custom theme selected");
|
||||||
|
|
||||||
|
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)
|
||||||
|
Frontend.ShowMessageBox($"Failed to setup custom bootstrapper: {ex.Message}.\nDefaulting to Fluent.", MessageBoxImage.Error);
|
||||||
|
|
||||||
|
return GetBootstrapperDialog(BootstrapperStyle.FluentDialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style)
|
public static IBootstrapperDialog GetBootstrapperDialog(BootstrapperStyle style)
|
||||||
{
|
{
|
||||||
return style switch
|
return style switch
|
||||||
@ -66,6 +92,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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
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 string Name { get; set; } = "";
|
||||||
|
public string Title { get; set; } = "Editing \"Custom Theme\"";
|
||||||
|
public string Code { get; set; } = "";
|
||||||
|
|
||||||
|
private void Preview()
|
||||||
|
{
|
||||||
|
const string LOG_IDENT = "BootstrapperEditorWindowViewModel::Preview";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CustomDialog dialog = new CustomDialog();
|
||||||
|
|
||||||
|
dialog.ApplyCustomTheme(Name, Code);
|
||||||
|
|
||||||
|
if (_dialog != null)
|
||||||
|
_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(Paths.CustomThemes, Name, "Theme.xml");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(path, Code);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
using Bloxstrap.UI.Elements.Settings;
|
using Bloxstrap.UI.Elements.Settings;
|
||||||
|
using Bloxstrap.UI.Elements.Editor;
|
||||||
|
|
||||||
namespace Bloxstrap.UI.ViewModels.Settings
|
namespace Bloxstrap.UI.ViewModels.Settings
|
||||||
{
|
{
|
||||||
@ -18,6 +19,11 @@ 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);
|
||||||
|
|
||||||
private void PreviewBootstrapper()
|
private void PreviewBootstrapper()
|
||||||
{
|
{
|
||||||
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
|
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
|
||||||
@ -51,6 +57,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>();
|
||||||
@ -116,5 +124,170 @@ namespace Bloxstrap.UI.ViewModels.Settings
|
|||||||
OnPropertyChanged(nameof(Icons));
|
OnPropertyChanged(nameof(Icons));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string CreateCustomThemeName()
|
||||||
|
{
|
||||||
|
int count = Directory.GetDirectories(Paths.CustomThemes).Count();
|
||||||
|
|
||||||
|
string name = $"Custom Theme {count + 1}";
|
||||||
|
|
||||||
|
// TODO: this sucks
|
||||||
|
if (Directory.Exists(Path.Combine(Paths.CustomThemes, name))) // DUCK
|
||||||
|
name += " " + Random.Shared.Next(1, 100000).ToString(); // easy
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateCustomThemeStructure(string name)
|
||||||
|
{
|
||||||
|
string dir = Path.Combine(Paths.CustomThemes, name);
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
string themeFilePath = Path.Combine(dir, "Theme.xml");
|
||||||
|
|
||||||
|
string templateContent = Encoding.UTF8.GetString(Resource.Get("CustomBootstrapperTemplate.xml").Result);
|
||||||
|
|
||||||
|
File.WriteAllText(themeFilePath, templateContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
string name = CreateCustomThemeName();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CreateCustomThemeStructure(name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.WriteException("AppearanceViewModel::AddCustomTheme", ex);
|
||||||
|
Frontend.ShowMessageBox($"Failed to create custom theme: {ex.Message}", MessageBoxImage.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomThemes.Add(name);
|
||||||
|
SelectedCustomThemeIndex = CustomThemes.Count - 1;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(SelectedCustomThemeIndex));
|
||||||
|
OnPropertyChanged(nameof(IsCustomThemeSelected));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteCustomTheme()
|
||||||
|
{
|
||||||
|
if (SelectedCustomTheme is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteCustomThemeStructure(SelectedCustomTheme);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.WriteException("AppearanceViewModel::DeleteCustomTheme", ex);
|
||||||
|
Frontend.ShowMessageBox($"Failed to delete custom theme {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($"Failed to rename custom theme {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 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user