bloxstrap/Bloxstrap/UI/ViewModels/Settings/AppearanceViewModel.cs
Matt 9d356b0b71
Some checks are pending
CI (Debug) / build (push) Waiting to run
CI (Release) / build (push) Waiting to run
CI (Release) / release (push) Blocked by required conditions
CI (Release) / release-test (push) Blocked by required conditions
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
2025-03-11 19:18:54 +00:00

316 lines
10 KiB
C#

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Win32;
using Bloxstrap.UI.Elements.Settings;
using Bloxstrap.UI.Elements.Editor;
using Bloxstrap.UI.Elements.Dialogs;
namespace Bloxstrap.UI.ViewModels.Settings
{
public class AppearanceViewModel : NotifyPropertyChangedViewModel
{
private readonly Page _page;
public ICommand PreviewBootstrapperCommand => new RelayCommand(PreviewBootstrapper);
public ICommand BrowseCustomIconLocationCommand => new RelayCommand(BrowseCustomIconLocation);
public ICommand AddCustomThemeCommand => new RelayCommand(AddCustomTheme);
public ICommand DeleteCustomThemeCommand => new RelayCommand(DeleteCustomTheme);
public ICommand RenameCustomThemeCommand => new RelayCommand(RenameCustomTheme);
public ICommand EditCustomThemeCommand => new RelayCommand(EditCustomTheme);
public ICommand ExportCustomThemeCommand => new RelayCommand(ExportCustomTheme);
private void PreviewBootstrapper()
{
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog)
dialog.Message = Strings.Bootstrapper_StylePreview_ImageCancel;
else
dialog.Message = Strings.Bootstrapper_StylePreview_TextCancel;
dialog.CancelEnabled = true;
dialog.ShowBootstrapper();
}
private void BrowseCustomIconLocation()
{
var dialog = new OpenFileDialog
{
Filter = $"{Strings.Menu_IconFiles}|*.ico"
};
if (dialog.ShowDialog() != true)
return;
CustomIconLocation = dialog.FileName;
OnPropertyChanged(nameof(CustomIconLocation));
}
public AppearanceViewModel(Page page)
{
_page = page;
foreach (var entry in BootstrapperIconEx.Selections)
Icons.Add(new BootstrapperIconEntry { IconType = entry });
PopulateCustomThemes();
}
public IEnumerable<Theme> Themes { get; } = Enum.GetValues(typeof(Theme)).Cast<Theme>();
public Theme Theme
{
get => App.Settings.Prop.Theme;
set
{
App.Settings.Prop.Theme = value;
((MainWindow)Window.GetWindow(_page)!).ApplyTheme();
}
}
public static List<string> Languages => Locale.GetLanguages();
public string SelectedLanguage
{
get => Locale.SupportedLocales[App.Settings.Prop.Locale];
set => App.Settings.Prop.Locale = Locale.GetIdentifierFromName(value);
}
public IEnumerable<BootstrapperStyle> Dialogs { get; } = BootstrapperStyleEx.Selections;
public BootstrapperStyle Dialog
{
get => App.Settings.Prop.BootstrapperStyle;
set
{
App.Settings.Prop.BootstrapperStyle = value;
OnPropertyChanged(nameof(CustomThemesExpanded)); // TODO: only fire when needed
}
}
public bool CustomThemesExpanded => App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.CustomDialog;
public ObservableCollection<BootstrapperIconEntry> Icons { get; set; } = new();
public BootstrapperIcon Icon
{
get => App.Settings.Prop.BootstrapperIcon;
set => App.Settings.Prop.BootstrapperIcon = value;
}
public string Title
{
get => App.Settings.Prop.BootstrapperTitle;
set => App.Settings.Prop.BootstrapperTitle = value;
}
public string CustomIconLocation
{
get => App.Settings.Prop.BootstrapperIconCustomLocation;
set
{
if (String.IsNullOrEmpty(value))
{
if (App.Settings.Prop.BootstrapperIcon == BootstrapperIcon.IconCustom)
App.Settings.Prop.BootstrapperIcon = BootstrapperIcon.IconBloxstrap;
}
else
{
App.Settings.Prop.BootstrapperIcon = BootstrapperIcon.IconCustom;
}
App.Settings.Prop.BootstrapperIconCustomLocation = value;
OnPropertyChanged(nameof(Icon));
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;
}
}