Modernize WPF UI using MahApps.Metro: switch MainWindow to MetroWindow, add MetroTabControl/MetroTabItem, custom tab styles, and icon-based theme toggle. Move version display to window commands. Integrate MahApps resource dictionaries and ThemeManager for runtime theme switching. Update startup logic for proper theming. WinForms fallback retained.
380 lines
15 KiB
C#
380 lines
15 KiB
C#
using System.Windows;
|
|
using MahApps.Metro.Controls;
|
|
using ControlzEx.Theming;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using System;
|
|
using System.IO;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Diagnostics;
|
|
using Microsoft.Win32;
|
|
using System.Windows.Forms;
|
|
|
|
namespace ImageCatalog_2
|
|
{
|
|
public partial class MainWindow : MetroWindow
|
|
{
|
|
private readonly DataModel _model;
|
|
private bool _isDarkTheme = false;
|
|
public MainWindow(DataModel model)
|
|
{
|
|
InitializeComponent();
|
|
_model = model;
|
|
DataContext = _model;
|
|
// Set product version in status bar (use ProductVersion rather than AssemblyVersion)
|
|
try
|
|
{
|
|
var entry = System.Reflection.Assembly.GetEntryAssembly();
|
|
string version = string.Empty;
|
|
if (entry is not null && !string.IsNullOrEmpty(entry.Location))
|
|
{
|
|
try
|
|
{
|
|
version = FileVersionInfo.GetVersionInfo(entry.Location).ProductVersion ?? string.Empty;
|
|
}
|
|
catch { }
|
|
}
|
|
if (string.IsNullOrWhiteSpace(version))
|
|
{
|
|
// fallback to assembly version
|
|
version = entry?.GetName().Version?.ToString() ?? string.Empty;
|
|
}
|
|
VersionTextBlock.Text = string.IsNullOrWhiteSpace(version) ? string.Empty : $"v{version}";
|
|
}
|
|
catch { }
|
|
// Ensure MahApps resource dictionaries are loaded so chrome/styles are available
|
|
EnsureMahAppsResourcesLoaded();
|
|
|
|
// Apply theme based on user preference or system setting (default to light)
|
|
ApplyTheme(isDark: false);
|
|
// Subscribe to DataModel events that require UI dialogs
|
|
_model.SelectSourceFolderRequested += Model_SelectSourceFolderRequested;
|
|
_model.SelectDestinationFolderRequested += Model_SelectDestinationFolderRequested;
|
|
_model.SelectLogoFileRequested += Model_SelectLogoFileRequested;
|
|
_model.SaveSettingsRequested += Model_SaveSettingsRequested;
|
|
_model.LoadSettingsRequested += Model_LoadSettingsRequested;
|
|
_model.SelectColorRequested += Model_SelectColorRequested;
|
|
_model.SelectTransparentColorRequested += Model_SelectTransparentColorRequested;
|
|
_model.SelectModelsFolderRequested += Model_SelectModelsFolderRequested;
|
|
_model.SelectCsvOutputRequested += Model_SelectCsvOutputRequested;
|
|
|
|
// Watch for logo changes to update preview
|
|
_model.PropertyChanged += Model_PropertyChanged;
|
|
}
|
|
|
|
private void ApplyTheme(bool isDark)
|
|
{
|
|
try
|
|
{
|
|
var rd = isDark ? (ResourceDictionary)Resources["DarkTheme"] : (ResourceDictionary)Resources["LightTheme"];
|
|
foreach (var key in rd.Keys)
|
|
{
|
|
// If the theme dictionary uses suffixed keys (e.g. "WindowBackgroundBrush.Dark"),
|
|
// map them to the base key ("WindowBackgroundBrush") so existing DynamicResource lookups update.
|
|
string outKey = key?.ToString() ?? string.Empty;
|
|
if (outKey.EndsWith(".Light", StringComparison.OrdinalIgnoreCase))
|
|
outKey = outKey.Substring(0, outKey.Length - ".Light".Length);
|
|
else if (outKey.EndsWith(".Dark", StringComparison.OrdinalIgnoreCase))
|
|
outKey = outKey.Substring(0, outKey.Length - ".Dark".Length);
|
|
|
|
Resources[outKey] = rd[key];
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore theme failures
|
|
}
|
|
}
|
|
|
|
private void Model_SelectModelsFolderRequested(object? sender, EventArgs e)
|
|
{
|
|
var dlg = new System.Windows.Forms.FolderBrowserDialog();
|
|
var starting = string.IsNullOrWhiteSpace(_model.ModelsFolderPath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : _model.ModelsFolderPath;
|
|
dlg.SelectedPath = starting;
|
|
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
{
|
|
_model.ModelsFolderPath = dlg.SelectedPath + Path.DirectorySeparatorChar;
|
|
}
|
|
}
|
|
|
|
private void OpenModelsFolder_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var path = _model.ModelsFolderPath;
|
|
if (string.IsNullOrWhiteSpace(path)) return;
|
|
path = path.Trim().Trim('"');
|
|
if (File.Exists(path))
|
|
{
|
|
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
|
|
return;
|
|
}
|
|
if (Directory.Exists(path))
|
|
{
|
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
|
|
return;
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private void Model_SelectCsvOutputRequested(object? sender, EventArgs e)
|
|
{
|
|
var dlg = new Microsoft.Win32.SaveFileDialog();
|
|
dlg.Filter = "CSV file (*.csv)|*.csv|All files (*.*)|*.*";
|
|
if (!string.IsNullOrWhiteSpace(_model.CsvOutputPath)) dlg.FileName = _model.CsvOutputPath;
|
|
var result = dlg.ShowDialog(this);
|
|
if (result == true)
|
|
{
|
|
_model.CsvOutputPath = dlg.FileName;
|
|
}
|
|
}
|
|
|
|
private void OpenCsvOutputFolder_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var path = _model.CsvOutputPath;
|
|
if (string.IsNullOrWhiteSpace(path)) return;
|
|
path = path.Trim().Trim('"');
|
|
if (File.Exists(path))
|
|
{
|
|
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
|
|
return;
|
|
}
|
|
var dir = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir))
|
|
{
|
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = dir, UseShellExecute = true });
|
|
return;
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private void Model_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
{
|
|
if (e is null || string.IsNullOrWhiteSpace(e.PropertyName)) return;
|
|
if (e.PropertyName == nameof(_model.LogoFile))
|
|
{
|
|
UpdateLogoPreview(_model.LogoFile);
|
|
}
|
|
}
|
|
|
|
private void Model_SelectSourceFolderRequested(object? sender, EventArgs e)
|
|
{
|
|
var dlg = new System.Windows.Forms.FolderBrowserDialog();
|
|
var starting = string.IsNullOrWhiteSpace(_model.SourcePath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : _model.SourcePath;
|
|
dlg.SelectedPath = starting;
|
|
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
{
|
|
_model.SourcePath = dlg.SelectedPath + Path.DirectorySeparatorChar;
|
|
}
|
|
}
|
|
|
|
private void OpenSourceFolder_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var path = _model.SourcePath;
|
|
if (string.IsNullOrWhiteSpace(path)) return;
|
|
path = path.Trim().Trim('"');
|
|
if (File.Exists(path))
|
|
{
|
|
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
|
|
return;
|
|
}
|
|
if (Directory.Exists(path))
|
|
{
|
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
|
|
return;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// ignore for now, or could show a message
|
|
}
|
|
}
|
|
|
|
private void Model_SelectDestinationFolderRequested(object? sender, EventArgs e)
|
|
{
|
|
var dlg = new System.Windows.Forms.FolderBrowserDialog();
|
|
var starting = string.IsNullOrWhiteSpace(_model.DestinationPath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : _model.DestinationPath;
|
|
dlg.SelectedPath = starting;
|
|
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
{
|
|
_model.DestinationPath = dlg.SelectedPath + Path.DirectorySeparatorChar;
|
|
}
|
|
|
|
}
|
|
|
|
private void OpenDestinationFolder_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var path = _model.DestinationPath;
|
|
if (string.IsNullOrWhiteSpace(path)) return;
|
|
path = path.Trim().Trim('"');
|
|
if (File.Exists(path))
|
|
{
|
|
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
|
|
return;
|
|
}
|
|
if (Directory.Exists(path))
|
|
{
|
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
|
|
return;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// ignore for now
|
|
}
|
|
}
|
|
|
|
private void Model_SelectLogoFileRequested(object? sender, EventArgs e)
|
|
{
|
|
var dlg = new Microsoft.Win32.OpenFileDialog();
|
|
dlg.Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif";
|
|
if (!string.IsNullOrWhiteSpace(_model.LogoFile)) dlg.FileName = _model.LogoFile;
|
|
var result = dlg.ShowDialog(this);
|
|
if (result == true)
|
|
{
|
|
_model.LogoFile = dlg.FileName;
|
|
}
|
|
}
|
|
|
|
private async void Model_SaveSettingsRequested(object? sender, string filePath)
|
|
{
|
|
var dlg = new Microsoft.Win32.SaveFileDialog();
|
|
dlg.Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*";
|
|
var result = dlg.ShowDialog(this);
|
|
if (result == true)
|
|
{
|
|
await _model.SaveSettingsToFileAsync(dlg.FileName);
|
|
}
|
|
}
|
|
|
|
private async void Model_LoadSettingsRequested(object? sender, string filePath)
|
|
{
|
|
var dlg = new Microsoft.Win32.OpenFileDialog();
|
|
dlg.Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*";
|
|
var result = dlg.ShowDialog(this);
|
|
if (result == true)
|
|
{
|
|
await _model.LoadSettingsFromFileAsync(dlg.FileName);
|
|
}
|
|
}
|
|
|
|
private void Model_SelectColorRequested(object? sender, EventArgs e)
|
|
{
|
|
var dlg = new System.Windows.Forms.ColorDialog { AllowFullOpen = true };
|
|
if (!string.IsNullOrWhiteSpace(_model.TextColorRGB))
|
|
{
|
|
try { dlg.Color = System.Drawing.ColorTranslator.FromHtml(_model.TextColorRGB); } catch { }
|
|
}
|
|
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
{
|
|
_model.TextColorRGB = System.Drawing.ColorTranslator.ToHtml(dlg.Color);
|
|
}
|
|
}
|
|
|
|
private void Model_SelectTransparentColorRequested(object? sender, EventArgs e)
|
|
{
|
|
var dlg = new System.Windows.Forms.ColorDialog { AllowFullOpen = true };
|
|
try { dlg.Color = System.Drawing.ColorTranslator.FromHtml(_model.TransparentColor); } catch { }
|
|
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
{
|
|
_model.TransparentColor = System.Drawing.ColorTranslator.ToHtml(dlg.Color);
|
|
}
|
|
}
|
|
|
|
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
|
|
{
|
|
ToggleTheme();
|
|
}
|
|
|
|
private void ToggleTheme()
|
|
{
|
|
try
|
|
{
|
|
_isDarkTheme = !_isDarkTheme;
|
|
|
|
// Use MahApps ThemeManager to change the application theme (handles chrome and brushes)
|
|
try
|
|
{
|
|
var themeName = _isDarkTheme ? "Dark.Blue" : "Light.Blue";
|
|
ThemeManager.Current.ChangeTheme(System.Windows.Application.Current, themeName);
|
|
}
|
|
catch
|
|
{
|
|
// Fall back silently if ThemeManager isn't available
|
|
}
|
|
|
|
// Still apply local resource overrides so any app-specific keys update
|
|
ApplyTheme(_isDarkTheme);
|
|
}
|
|
catch
|
|
{
|
|
// ignore toggle failures
|
|
}
|
|
}
|
|
|
|
private void EnsureMahAppsResourcesLoaded()
|
|
{
|
|
try
|
|
{
|
|
var app = System.Windows.Application.Current;
|
|
if (app is null)
|
|
return;
|
|
|
|
var mds = app.Resources.MergedDictionaries;
|
|
|
|
// Helper to add if missing
|
|
void AddIfMissing(string uriString)
|
|
{
|
|
if (!mds.Any(d => d.Source is not null && d.Source.OriginalString.Equals(uriString, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
mds.Add(new ResourceDictionary { Source = new Uri(uriString) });
|
|
}
|
|
}
|
|
|
|
AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml");
|
|
AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml");
|
|
// Ensure a default theme is present
|
|
if (!mds.Any(d => d.Source is not null && d.Source.OriginalString.IndexOf("/MahApps.Metro;component/Styles/Themes/", StringComparison.OrdinalIgnoreCase) >= 0))
|
|
{
|
|
AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml");
|
|
_isDarkTheme = false;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore; styling will fallback to local resources
|
|
}
|
|
}
|
|
|
|
private void UpdateLogoPreview(string? path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
|
{
|
|
LogoPreview.Source = null;
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var bitmap = new BitmapImage();
|
|
bitmap.BeginInit();
|
|
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
|
bitmap.UriSource = new Uri(path);
|
|
bitmap.EndInit();
|
|
LogoPreview.Source = bitmap;
|
|
}
|
|
catch
|
|
{
|
|
LogoPreview.Source = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|