diff --git a/CheckControl.cs b/CheckControl.cs index e1ac1b0..27278f5 100644 --- a/CheckControl.cs +++ b/CheckControl.cs @@ -1,4 +1,5 @@ using System; +using System.Drawing; using System.Windows.Forms; namespace Material_Editor @@ -7,6 +8,7 @@ public partial class CheckControl : CustomControl { private Label lbLabel; private CheckBox check; + public static Func OffForegroundProvider { get; set; } = () => System.Drawing.Color.Red; public override Label LabelControl { @@ -50,23 +52,34 @@ public override void CreateControls() Tag = this }; check.CheckedChanged += new EventHandler(Check_CheckedChanged); + UpdateCheckVisual(check); } private void Check_CheckedChanged(object sender, EventArgs e) { var check = sender as CheckBox; + UpdateCheckVisual(check); + + InvokeChangedCallback(); + } + + internal static void UpdateCheckVisual(CheckBox check) + { + if (check == null) + return; + if (check.Checked) { check.ForeColor = System.Drawing.Color.FromName("green"); check.Text = "On"; + check.Font = new System.Drawing.Font(check.Font, System.Drawing.FontStyle.Regular); } else { - check.ForeColor = System.Drawing.Color.FromName("red"); + check.ForeColor = OffForegroundProvider(); check.Text = "Off"; + check.Font = new System.Drawing.Font(check.Font, System.Drawing.FontStyle.Regular); } - - InvokeChangedCallback(); } public override object GetProperty() diff --git a/CustomControl.cs b/CustomControl.cs index ffef12c..81dca60 100644 --- a/CustomControl.cs +++ b/CustomControl.cs @@ -118,6 +118,19 @@ public static CustomControl Find(string name) return null; } + public static IEnumerable GetRegisteredNames() + { + return customControls.Keys; + } + + public static string GetTooltip(string name) + { + if (customControls.TryGetValue(name, out CustomControl control)) + return control.BaseToolTip; + + return null; + } + public static bool GetProperty(string name, out object property) { if (customControls.TryGetValue(name, out CustomControl control)) diff --git a/FieldOverwriteTool.cs b/FieldOverwriteTool.cs new file mode 100644 index 0000000..5390387 --- /dev/null +++ b/FieldOverwriteTool.cs @@ -0,0 +1,150 @@ +using MaterialLib; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization.Json; +using System.Text; + +namespace Material_Editor +{ + public enum FieldCopyStatus + { + Success, + Skipped, + Failed + } + + public sealed class FieldCopyResult + { + public string TargetPath { get; } + public FieldCopyStatus Status { get; } + public string Message { get; } + + public FieldCopyResult(string targetPath, FieldCopyStatus status, string message) + { + TargetPath = targetPath; + Status = status; + Message = message; + } + } + + public sealed class FieldOverwriteTool + { + public IReadOnlyList Run(BaseMaterialFile sourceState, IReadOnlyList descriptors, IReadOnlyList targetFiles, bool backupBeforeWrite) + { + var results = new List(); + + foreach (var target in targetFiles) + { + var result = ProcessTarget(sourceState, descriptors, target, backupBeforeWrite); + results.Add(result); + } + + return results; + } + + private FieldCopyResult ProcessTarget(BaseMaterialFile sourceState, IReadOnlyList descriptors, string filePath, bool backupBeforeWrite) + { + if (!File.Exists(filePath)) + return new FieldCopyResult(filePath, FieldCopyStatus.Failed, "Target file does not exist."); + + if (!TryLoadMaterial(filePath, out var targetMaterial, out var isJson, out var loadError)) + return new FieldCopyResult(filePath, FieldCopyStatus.Failed, loadError ?? "Failed to load material."); + + if (targetMaterial.GetType() != sourceState.GetType()) + return new FieldCopyResult(filePath, FieldCopyStatus.Skipped, "Skipped incompatible material type."); + + var supportedDescriptors = descriptors.Where(d => d.IsSupported(targetMaterial)).ToList(); + if (supportedDescriptors.Count == 0) + return new FieldCopyResult(filePath, FieldCopyStatus.Skipped, "No compatible fields for target version."); + + foreach (var descriptor in supportedDescriptors) + { + descriptor.SetValue(targetMaterial, descriptor.GetValue(sourceState)); + } + + try + { + if (backupBeforeWrite) + { + File.Copy(filePath, $"{filePath}.bak", true); + } + + SaveMaterial(filePath, targetMaterial, isJson); + } + catch (Exception ex) + { + return new FieldCopyResult(filePath, FieldCopyStatus.Failed, ex.Message); + } + + return new FieldCopyResult(filePath, FieldCopyStatus.Success, "Updated successfully."); + } + + private static bool TryLoadMaterial(string filePath, out BaseMaterialFile material, out bool isJson, out string errorMessage) + { + material = null; + isJson = false; + errorMessage = null; + + try + { + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + int start = stream.ReadByte(); + if (start == -1) + { + errorMessage = "Target file is empty."; + return false; + } + + stream.Position = 0; + + BaseMaterialFile candidate = Path.GetExtension(filePath).Equals(".bgem", StringComparison.OrdinalIgnoreCase) + ? new BGEM() + : new BGSM(); + + if (start == '{' || start == '[') + { + isJson = true; + var serializer = new DataContractJsonSerializer(candidate.GetType(), new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); + material = (BaseMaterialFile)serializer.ReadObject(stream); + } + else + { + stream.Position = 0; + if (!candidate.Open(stream)) + { + errorMessage = "Failed to read binary material."; + return false; + } + + material = candidate; + } + + return true; + } + catch (Exception ex) + { + errorMessage = ex.Message; + return false; + } + } + + private static void SaveMaterial(string filePath, BaseMaterialFile material, bool asJson) + { + using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write); + if (asJson) + { + using var writer = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, true, true, " "); + var serializer = new DataContractJsonSerializer(material.GetType(), new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); + serializer.WriteObject(writer, material); + writer.Flush(); + } + else + { + if (!material.Save(stream)) + throw new IOException("Failed to write binary material."); + } + } + } +} diff --git a/FieldSelectionDialog.cs b/FieldSelectionDialog.cs new file mode 100644 index 0000000..6ed739f --- /dev/null +++ b/FieldSelectionDialog.cs @@ -0,0 +1,193 @@ +using MaterialLib; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace Material_Editor +{ + internal sealed class FieldSelectionDialog : Form + { + private readonly ListView listView; + private readonly Button selectAllButton; + private readonly Button selectNoneButton; + private readonly Button okButton; + private readonly Label descriptionLabel; + private readonly Label headerLabel; + private readonly Label descriptionHeaderLabel; + private readonly string descriptionPlaceholder = "Select a field to see a short explanation of what it controls."; + + public IReadOnlyList SelectedFields { get; private set; } + + public FieldSelectionDialog(IReadOnlyList descriptors, BaseMaterialFile baseline, BaseMaterialFile current, ThemePalette palette) + { + Text = "Overwrite Fields"; + FormBorderStyle = FormBorderStyle.FixedDialog; + StartPosition = FormStartPosition.CenterParent; + ClientSize = new Size(560, 480); + MinimizeBox = false; + MaximizeBox = false; + ShowInTaskbar = false; + + headerLabel = new Label + { + Text = "Choose the fields you want to copy from the current material.", + AutoSize = false, + Bounds = new Rectangle(12, 12, 536, 32) + }; + Controls.Add(headerLabel); + + listView = new ListView + { + Bounds = new Rectangle(12, 50, 536, 280), + CheckBoxes = true, + FullRowSelect = true, + View = View.Details, + HeaderStyle = ColumnHeaderStyle.Nonclickable + }; + listView.Columns.Add("Field", 320); + listView.Columns.Add("Category", 160); + listView.ItemChecked += (s, e) => UpdateOkState(); + listView.SelectedIndexChanged += (s, e) => UpdateDescription(); + Controls.Add(listView); + + selectAllButton = new Button + { + Text = "Select All", + Bounds = new Rectangle(12, 420, 120, 28) + }; + selectAllButton.Click += (s, e) => SetAllChecks(true); + Controls.Add(selectAllButton); + + selectNoneButton = new Button + { + Text = "Select None", + Bounds = new Rectangle(138, 420, 120, 28) + }; + selectNoneButton.Click += (s, e) => SetAllChecks(false); + Controls.Add(selectNoneButton); + + descriptionHeaderLabel = new Label + { + Text = "Field description", + Bounds = new Rectangle(12, 340, 536, 18) + }; + Controls.Add(descriptionHeaderLabel); + + descriptionLabel = new Label + { + Bounds = new Rectangle(12, 360, 536, 58), + BorderStyle = BorderStyle.FixedSingle, + AutoSize = false, + Text = descriptionPlaceholder + }; + Controls.Add(descriptionLabel); + + okButton = new Button + { + Text = "OK", + Bounds = new Rectangle(382, 420, 80, 28), + DialogResult = DialogResult.OK + }; + okButton.Click += (s, e) => SelectFields(); + Controls.Add(okButton); + + var cancelButton = new Button + { + Text = "Cancel", + Bounds = new Rectangle(474, 420, 74, 28), + DialogResult = DialogResult.Cancel + }; + Controls.Add(cancelButton); + + AcceptButton = okButton; + CancelButton = cancelButton; + + PopulateList(descriptors, baseline, current); + ApplyPalette(palette); + } + + private void PopulateList(IReadOnlyList descriptors, BaseMaterialFile baseline, BaseMaterialFile current) + { + foreach (var descriptor in descriptors) + { + var item = new ListViewItem(new[] { descriptor.Label, descriptor.Category.ToString() }) + { + Tag = descriptor, + Checked = descriptor.HasChanged(baseline, current) + }; + + listView.Items.Add(item); + } + + foreach (ColumnHeader column in listView.Columns) + { + column.Width = -2; + } + + UpdateOkState(); + } + + private void SetAllChecks(bool value) + { + foreach (ListViewItem item in listView.Items) + { + item.Checked = value; + } + + UpdateOkState(); + } + + private void UpdateOkState() + { + okButton.Enabled = listView.CheckedItems.Count > 0; + } + + private void UpdateDescription() + { + if (listView.SelectedItems.Count > 0) + { + var descriptor = listView.SelectedItems[0].Tag as MaterialFieldDescriptor; + descriptionLabel.Text = ControlFactory.GetTooltip(descriptor.Label) ?? descriptionPlaceholder; + } + else + { + descriptionLabel.Text = descriptionPlaceholder; + } + } + + private void SelectFields() + { + SelectedFields = listView.CheckedItems + .Cast() + .Select(item => item.Tag as MaterialFieldDescriptor) + .ToList(); + } + + private void ApplyPalette(ThemePalette palette) + { + BackColor = palette.FormBackground; + ForeColor = palette.Foreground; + + headerLabel.BackColor = palette.FormBackground; + headerLabel.ForeColor = palette.Foreground; + + descriptionHeaderLabel.BackColor = palette.FormBackground; + descriptionHeaderLabel.ForeColor = palette.Foreground; + + descriptionLabel.BackColor = palette.PanelBackground; + descriptionLabel.ForeColor = palette.Foreground; + + listView.BackColor = palette.PanelBackground; + listView.ForeColor = palette.Foreground; + listView.GridLines = false; + listView.HideSelection = false; + + selectAllButton.BackColor = palette.ControlBackground; + selectNoneButton.BackColor = palette.ControlBackground; + okButton.BackColor = palette.ControlBackground; + descriptionLabel.ForeColor = palette.Foreground; + } + } +} diff --git a/Main.Designer.cs b/Main.Designer.cs index e330bfa..5fb1b01 100644 --- a/Main.Designer.cs +++ b/Main.Designer.cs @@ -41,12 +41,17 @@ private void InitializeComponent() exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); optionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); fontToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + themeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + defaultThemeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + darkThemeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + toolsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + overwriteFilesByFieldToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); openFileDialog = new System.Windows.Forms.OpenFileDialog(); saveFileDialog = new System.Windows.Forms.SaveFileDialog(); colorDialog = new System.Windows.Forms.ColorDialog(); - tabControl = new System.Windows.Forms.TabControl(); + tabControl = new Material_Editor.ThemedTabControl(); tabPageGeneral = new System.Windows.Forms.TabPage(); layoutGeneral = new System.Windows.Forms.TableLayoutPanel(); tabPageMaterial = new System.Windows.Forms.TabPage(); @@ -69,7 +74,7 @@ private void InitializeComponent() // menuStrip // menuStrip.ImageScalingSize = new System.Drawing.Size(20, 20); - menuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { fileToolStripMenuItem, optionsToolStripMenuItem, toolStripMenuItem1 }); + menuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { fileToolStripMenuItem, toolsToolStripMenuItem, optionsToolStripMenuItem, toolStripMenuItem1 }); menuStrip.Location = new System.Drawing.Point(0, 0); menuStrip.Name = "menuStrip"; menuStrip.Size = new System.Drawing.Size(624, 24); @@ -142,9 +147,23 @@ private void InitializeComponent() exitToolStripMenuItem.Text = "Exit"; exitToolStripMenuItem.Click += ExitToolStripMenuItem_Click; // + // toolsToolStripMenuItem + // + toolsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { overwriteFilesByFieldToolStripMenuItem }); + toolsToolStripMenuItem.Name = "toolsToolStripMenuItem"; + toolsToolStripMenuItem.Size = new System.Drawing.Size(46, 20); + toolsToolStripMenuItem.Text = "Tools"; + // + // overwriteFilesByFieldToolStripMenuItem + // + overwriteFilesByFieldToolStripMenuItem.Name = "overwriteFilesByFieldToolStripMenuItem"; + overwriteFilesByFieldToolStripMenuItem.Size = new System.Drawing.Size(257, 22); + overwriteFilesByFieldToolStripMenuItem.Text = "Overwrite Files by Field..."; + overwriteFilesByFieldToolStripMenuItem.Click += OverwriteFilesByFieldToolStripMenuItem_Click; + // // optionsToolStripMenuItem // - optionsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { fontToolStripMenuItem }); + optionsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { fontToolStripMenuItem, themeToolStripMenuItem }); optionsToolStripMenuItem.Name = "optionsToolStripMenuItem"; optionsToolStripMenuItem.Size = new System.Drawing.Size(61, 20); optionsToolStripMenuItem.Text = "Options"; @@ -156,6 +175,27 @@ private void InitializeComponent() fontToolStripMenuItem.Text = "Font..."; fontToolStripMenuItem.Click += FontToolStripMenuItem_Click; // + // themeToolStripMenuItem + // + themeToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { defaultThemeToolStripMenuItem, darkThemeToolStripMenuItem }); + themeToolStripMenuItem.Name = "themeToolStripMenuItem"; + themeToolStripMenuItem.Size = new System.Drawing.Size(107, 22); + themeToolStripMenuItem.Text = "Theme"; + // + // defaultThemeToolStripMenuItem + // + defaultThemeToolStripMenuItem.Name = "defaultThemeToolStripMenuItem"; + defaultThemeToolStripMenuItem.Size = new System.Drawing.Size(180, 22); + defaultThemeToolStripMenuItem.Text = "Default"; + defaultThemeToolStripMenuItem.Click += DefaultThemeToolStripMenuItem_Click; + // + // windowsThemeToolStripMenuItem + // + darkThemeToolStripMenuItem.Name = "darkThemeToolStripMenuItem"; + darkThemeToolStripMenuItem.Size = new System.Drawing.Size(180, 22); + darkThemeToolStripMenuItem.Text = "Pip-boy3000"; + darkThemeToolStripMenuItem.Click += DarkThemeToolStripMenuItem_Click; + // // toolStripMenuItem1 // toolStripMenuItem1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { aboutToolStripMenuItem }); @@ -207,7 +247,7 @@ private void InitializeComponent() tabPageGeneral.TabIndex = 2; tabPageGeneral.Text = "General"; tabPageGeneral.ToolTipText = "Affects both BGSM and BGEM files."; - tabPageGeneral.UseVisualStyleBackColor = true; + tabPageGeneral.UseVisualStyleBackColor = false; tabPageGeneral.Scroll += TabScroll; // // layoutGeneral @@ -234,7 +274,7 @@ private void InitializeComponent() tabPageMaterial.TabIndex = 0; tabPageMaterial.Text = "Material"; tabPageMaterial.ToolTipText = "Affects only BGSM files."; - tabPageMaterial.UseVisualStyleBackColor = true; + tabPageMaterial.UseVisualStyleBackColor = false; tabPageMaterial.Scroll += TabScroll; // // layoutMaterial @@ -261,7 +301,7 @@ private void InitializeComponent() tabPageEffect.TabIndex = 1; tabPageEffect.Text = "Effect"; tabPageEffect.ToolTipText = "Affects only BGEM files."; - tabPageEffect.UseVisualStyleBackColor = true; + tabPageEffect.UseVisualStyleBackColor = false; tabPageEffect.Scroll += TabScroll; // // layoutEffect @@ -385,7 +425,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem; private System.Windows.Forms.ColorDialog colorDialog; - private System.Windows.Forms.TabControl tabControl; + private ThemedTabControl tabControl; private System.Windows.Forms.TabPage tabPageMaterial; private System.Windows.Forms.TabPage tabPageGeneral; private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem1; @@ -400,7 +440,12 @@ private void InitializeComponent() private System.Windows.Forms.ToolTip toolTip; private System.Windows.Forms.ComboBox listMatType; private System.Windows.Forms.ToolStripMenuItem optionsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem toolsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem overwriteFilesByFieldToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem fontToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem themeToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem defaultThemeToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem darkThemeToolStripMenuItem; private System.Windows.Forms.ComboBox listVersion; private System.Windows.Forms.Label lbVersion; } diff --git a/Main.cs b/Main.cs index 339fa5c..0fbcdf0 100644 --- a/Main.cs +++ b/Main.cs @@ -1,5 +1,6 @@ using MaterialLib; using System; +using System.Collections.Generic; using System.Configuration; using System.Drawing; using System.Globalization; @@ -16,6 +17,7 @@ public struct Config { public Game GameVersion; public Font Font; + public UITheme Theme; } public enum Game @@ -32,12 +34,18 @@ public enum MaterialType public partial class Main : Form { - private Config config = new(); + private static ThemePalette GetPalette(UITheme theme) + { + return ThemeManager.GetPalette(theme); + } + + private Config config; private string workFilePath; private bool changed; private bool toolTipPopping; private BaseMaterialFile currentMaterial; + private BaseMaterialFile originalMaterial; private const int DefaultVersionFO4 = 2; private const int DefaultVersionFO76 = 21; @@ -64,10 +72,20 @@ private MaterialType CurrentMaterialType get { return (MaterialType)listMatType.SelectedIndex; } } - public Main() + public Main() : this(LoadConfig()) + { + } + + public Main(Config initialConfig) { + config = initialConfig; InitializeComponent(); - ReadSettings(); + tabControl.DrawMode = TabDrawMode.OwnerDrawFixed; + tabControl.PaletteProvider = () => GetPalette(config.Theme); + tabControl.Paint += TabControl_Paint; + ApplyConfigFont(); + ApplyTheme(); + UpdateThemeMenuChecks(); } private string ChangeFileExtension(string filePath) @@ -188,6 +206,7 @@ private void SaveToolStripMenuItem_Click(object sender, EventArgs e) } currentMaterial = material; + originalMaterial = CloneMaterial(currentMaterial); Text = GetTitleText(); changed = false; } @@ -262,14 +281,56 @@ private void SaveAsToolStripMenuItem_Click(object sender, EventArgs e) saveToolStripMenuItem.Enabled = true; currentMaterial = material; + originalMaterial = CloneMaterial(currentMaterial); Text = GetTitleText(); changed = false; } } + private void OverwriteFilesByFieldToolStripMenuItem_Click(object sender, EventArgs e) + { + if (currentMaterial == null) + { + MessageBox.Show("Open or create a material before using the overwrite tool.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var currentState = CaptureCurrentMaterialState(); + if (currentState == null) + return; + + var descriptors = MaterialFieldRegistry.GetDescriptors(currentState); + if (descriptors.Count == 0) + { + MessageBox.Show("No writable fields are available for the current material.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var baseline = originalMaterial ?? CloneMaterial(currentState) ?? currentState; + + var palette = GetPalette(config.Theme); + using var fieldSelection = new FieldSelectionDialog(descriptors, baseline, currentState, palette); + if (fieldSelection.ShowDialog(this) != DialogResult.OK) + return; + + if (fieldSelection.SelectedFields == null || fieldSelection.SelectedFields.Count == 0) + return; + + using var targetDialog = new TargetFileSelectionDialog(palette); + if (targetDialog.ShowDialog(this) != DialogResult.OK) + return; + + var tool = new FieldOverwriteTool(); + var results = tool.Run(currentState, fieldSelection.SelectedFields, targetDialog.TargetFiles, targetDialog.BackupBeforeWrite); + + using var summary = new OverwriteSummaryDialog(results, palette); + summary.ShowDialog(this); + } + private void CloseToolStripMenuItem_Click(object sender, EventArgs e) { currentMaterial = null; + originalMaterial = null; workFilePath = string.Empty; saveToolStripMenuItem.Enabled = false; @@ -310,10 +371,21 @@ private void FontToolStripMenuItem_Click(object sender, EventArgs e) if (result == DialogResult.OK) { config.Font = fontDialog.Font; + ApplyConfigFont(); MessageBox.Show("Changing the font requires a restart of the application.", "Restart required", MessageBoxButtons.OK, MessageBoxIcon.Information); } } + private void DefaultThemeToolStripMenuItem_Click(object sender, EventArgs e) + { + SetTheme(UITheme.Default); + } + + private void DarkThemeToolStripMenuItem_Click(object sender, EventArgs e) + { + SetTheme(UITheme.PipBoy3000); + } + private void AboutToolStripMenuItem_Click(object sender, EventArgs e) { var about = new AboutDialog(); @@ -407,10 +479,65 @@ private void FillVersionDropdown() break; } - if (currentMaterial != null) - listVersion.SelectedItem = (int)currentMaterial.Version; - else - listVersion.SelectedItem = defaultVersion; + uint targetVersion = currentMaterial?.Version ?? (uint)defaultVersion; + SelectVersionInDropdown(targetVersion); + } + + private void SelectVersionInDropdown(uint version) + { + for (int i = 0; i < listVersion.Items.Count; i++) + { + if (TryGetVersionValue(listVersion.Items[i], out var itemVersion) && itemVersion == version) + { + listVersion.SelectedIndex = i; + return; + } + } + + listVersion.SelectedIndex = AddMissingVersionItem(version); + } + + private int AddMissingVersionItem(uint version) + { + listVersion.Items.Add(version); + return listVersion.Items.Count - 1; + } + + private static bool TryGetVersionValue(object value, out uint version) + { + version = 0; + + if (value is uint u) + { + version = u; + return true; + } + + if (value is int i) + { + version = (uint)i; + return true; + } + + if (value is string s && uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)) + { + version = parsed; + return true; + } + + if (value is IConvertible convertible) + { + try + { + version = convertible.ToUInt32(CultureInfo.InvariantCulture); + return true; + } + catch + { + } + } + + return false; } private void ToolTip_Popup(object sender, PopupEventArgs ea) @@ -502,7 +629,7 @@ private void Main_ResizeEnd(object sender, EventArgs e) private void Main_Load(object sender, EventArgs e) { - Font = config.Font; + ApplyConfigFont(); var items = Enum.GetNames(typeof(Game)); listGame.Items.AddRange(items); @@ -530,36 +657,64 @@ private void Main_Load(object sender, EventArgs e) } } - private void ReadSettings() + internal static Config LoadConfig() { + var loadedConfig = new Config + { + GameVersion = Game.FO4, + Font = new Font("Segoe UI", 9f), + Theme = UITheme.Default + }; + try { var appSettings = ConfigurationManager.AppSettings; var gameVersion = appSettings["GameVersion"]; - if (gameVersion != null) + if (gameVersion != null && Enum.TryParse(gameVersion, out Game parsedGame)) { - if (!Enum.TryParse(gameVersion, out config.GameVersion)) - config.GameVersion = Game.FO4; + loadedConfig.GameVersion = parsedGame; } - var fontName = appSettings.Get("FontName"); - var fontSizeStr = appSettings.Get("FontSize"); - if (fontName != null && fontSizeStr != null) + var fontName = appSettings["FontName"]; + var fontSizeStr = appSettings["FontSize"]; + var themeValue = appSettings["Theme"]; + if (!string.IsNullOrEmpty(fontName) && !string.IsNullOrEmpty(fontSizeStr)) { - if (!float.TryParse(fontSizeStr, CultureInfo.InvariantCulture, out float fontSize)) + if (!float.TryParse(fontSizeStr, NumberStyles.Float, CultureInfo.InvariantCulture, out float fontSize)) fontSize = 10.0f; - config.Font = new Font(fontName, fontSize); + try + { + loadedConfig.Font = new Font(fontName, fontSize); + } + catch + { + loadedConfig.Font = new Font("Segoe UI", 9f); + } } - else + + if (!string.IsNullOrEmpty(themeValue) && Enum.TryParse(themeValue, true, out UITheme parsedTheme)) { - config.Font = Font; + loadedConfig.Theme = parsedTheme; } + } + catch + { + } - Application.SetDefaultFont(config.Font); + if (loadedConfig.Font == null) + loadedConfig.Font = new Font("Segoe UI", 9f); + + return loadedConfig; + } + + private void ApplyConfigFont() + { + if (config.Font != null) + { + Font = config.Font; } - catch { } } private void WriteSettings() @@ -586,12 +741,320 @@ private void WriteSettings() else configFile.AppSettings.Settings.Add("FontSize", config.Font.Size.ToString(CultureInfo.InvariantCulture)); + var themeSetting = configFile.AppSettings.Settings["Theme"]; + if (themeSetting != null) + themeSetting.Value = config.Theme.ToString(); + else + configFile.AppSettings.Settings.Add("Theme", config.Theme.ToString()); + configFile.Save(ConfigurationSaveMode.Modified); ConfigurationManager.RefreshSection(configFile.AppSettings.SectionInformation.Name); } catch { } } + private void ApplyTheme() + { + var palette = GetPalette(config.Theme); + + BackColor = palette.FormBackground; + ForeColor = palette.Foreground; + tabControl.DrawMode = config.Theme == UITheme.Default ? TabDrawMode.Normal : TabDrawMode.OwnerDrawFixed; + tabControl.ShouldPaintBody = () => config.Theme != UITheme.Default; + + if (config.Theme == UITheme.Default) + { + ApplyDefaultThemeAppearance(); + } + else + { + ApplyCustomThemeAppearance(palette); + } + + tabControl.Invalidate(); + } + + private void ApplyCustomThemeAppearance(ThemePalette palette) + { + CheckControl.OffForegroundProvider = () => Color.White; + ApplyThemeToControl(tabControl, palette, palette.ControlBackground); + ApplyThemeToControl(layoutGeneral, palette, palette.PanelBackground); + ApplyThemeToControl(layoutMaterial, palette, palette.PanelBackground); + ApplyThemeToControl(layoutEffect, palette, palette.PanelBackground); + ApplyThemeToToolStrip(menuStrip, palette); + ApplyComboTheme(listGame, palette); + ApplyComboTheme(listMatType, palette); + ApplyComboTheme(listVersion, palette); + foreach (TabPage page in tabControl.TabPages) + page.BackColor = palette.PanelBackground; + + ApplyDropdownTheme(ControlNames.AlphaBlendMode, palette); + } + + private void ApplyDefaultThemeAppearance() + { + CheckControl.OffForegroundProvider = () => Color.Red; + ResetControlAppearance(tabControl); + ResetControlAppearance(layoutGeneral); + ResetControlAppearance(layoutMaterial); + ResetControlAppearance(layoutEffect); + ResetComboAppearance(listGame); + ResetComboAppearance(listMatType); + ResetComboAppearance(listVersion); + ResetToolStripAppearance(menuStrip); + foreach (TabPage page in tabControl.TabPages) + { + page.ResetBackColor(); + page.ResetForeColor(); + } + } + + private void ResetControlAppearance(Control control) + { + if (control == null) + return; + + control.ResetBackColor(); + control.ResetForeColor(); + + switch (control) + { + case Button button: + button.FlatStyle = FlatStyle.Standard; + button.FlatAppearance.BorderSize = 1; + button.FlatAppearance.BorderColor = SystemColors.ControlDark; + button.UseVisualStyleBackColor = true; + break; + case TextBoxBase textBox: + textBox.BorderStyle = BorderStyle.Fixed3D; + break; + case ComboBox comboChild: + ResetComboAppearance(comboChild); + break; + case CheckBox checkBox: + checkBox.UseVisualStyleBackColor = true; + if (checkBox.Tag is CheckControl) + CheckControl.UpdateCheckVisual(checkBox); + break; + case NumericUpDown numeric: + numeric.ResetBackColor(); + numeric.ResetForeColor(); + break; + } + + foreach (Control child in control.Controls) + { + ResetControlAppearance(child); + } + } + + private void ResetComboAppearance(ComboBox combo) + { + if (combo == null) + return; + + combo.FlatStyle = FlatStyle.Standard; + combo.DrawMode = DrawMode.Normal; + combo.DrawItem -= ComboBox_DrawItem; + combo.ResetBackColor(); + combo.ResetForeColor(); + } + + private void ResetToolStripAppearance(ToolStrip toolStrip) + { + if (toolStrip == null) + return; + + toolStrip.BackColor = SystemColors.Control; + toolStrip.ForeColor = SystemColors.ControlText; + foreach (ToolStripItem item in toolStrip.Items) + { + item.BackColor = SystemColors.Control; + item.ForeColor = SystemColors.ControlText; + } + } + + private void ApplyThemeToControl(Control control, ThemePalette palette, Color backgroundOverride) + { + if (control == null) + return; + + control.BackColor = backgroundOverride; + control.ForeColor = palette.Foreground; + StyleControl(control, palette, backgroundOverride); + + foreach (Control child in control.Controls) + { + var childBack = child is TabPage ? palette.PanelBackground : backgroundOverride; + ApplyThemeToControl(child, palette, childBack); + } + } + + private static void StyleControl(Control control, ThemePalette palette, Color background) + { + switch (control) + { + case Button button: + button.FlatStyle = FlatStyle.Flat; + button.FlatAppearance.BorderSize = 1; + button.FlatAppearance.BorderColor = palette.Accent.IsEmpty ? Color.Gray : palette.Accent; + if (button.Tag is ColorControl colorControl) + button.BackColor = colorControl.CurrentColor; + else + button.BackColor = background; + if (ShouldOverrideForeColor(button)) + button.ForeColor = palette.Foreground; + break; + case TextBoxBase textBox: + textBox.BorderStyle = BorderStyle.FixedSingle; + textBox.BackColor = background; + if (ShouldOverrideForeColor(textBox)) + textBox.ForeColor = palette.Foreground; + break; + case ComboBox combo: + combo.FlatStyle = FlatStyle.Flat; + combo.BackColor = background; + if (ShouldOverrideForeColor(combo)) + combo.ForeColor = palette.Foreground; + break; + case NumericUpDown numeric: + numeric.BackColor = background; + numeric.ForeColor = palette.Foreground; + break; + case CheckBox checkBox: + checkBox.BackColor = background; + if (ShouldOverrideForeColor(checkBox)) + checkBox.ForeColor = palette.Foreground; + CheckControl.UpdateCheckVisual(checkBox); + break; + case ListView listView: + listView.BackColor = palette.PanelBackground; + listView.ForeColor = palette.Foreground; + listView.BorderStyle = BorderStyle.FixedSingle; + listView.OwnerDraw = false; + break; + default: + break; + } + } + + private static bool ShouldOverrideForeColor(Control control) + { + var color = control.ForeColor; + return color == Color.Empty || color == SystemColors.ControlText || color == SystemColors.WindowText || color == Color.Black; + } + + private void ApplyComboTheme(ComboBox combo, ThemePalette palette) + { + if (combo == null) + return; + + combo.FlatStyle = FlatStyle.Flat; + combo.BackColor = palette.ControlBackground; + if (ShouldOverrideForeColor(combo)) + combo.ForeColor = palette.Foreground; + combo.DrawMode = DrawMode.OwnerDrawFixed; + combo.DrawItem -= ComboBox_DrawItem; + combo.DrawItem += ComboBox_DrawItem; + } + + private void ApplyDropdownTheme(string controlName, ThemePalette palette) + { + var combo = ControlFactory.Find(controlName)?.Control as ComboBox; + if (combo != null) + { + ApplyComboTheme(combo, palette); + } + } + + private void ComboBox_DrawItem(object sender, DrawItemEventArgs e) + { + if (sender is ComboBox combo) + { + var palette = GetPalette(config.Theme); + var bounds = e.Bounds; + var isSelected = (e.State & DrawItemState.Selected) == DrawItemState.Selected; + var background = isSelected + ? palette.Accent.IsEmpty ? palette.MenuBackground : palette.Accent + : palette.PanelBackground; + + using var brush = new SolidBrush(background); + e.Graphics.FillRectangle(brush, bounds); + + if (e.Index >= 0) + { + var text = combo.Items[e.Index]?.ToString() ?? string.Empty; + TextRenderer.DrawText(e.Graphics, text, combo.Font, bounds, palette.Foreground, TextFormatFlags.Left | TextFormatFlags.VerticalCenter); + } + else + { + var text = combo.Text ?? string.Empty; + TextRenderer.DrawText(e.Graphics, text, combo.Font, bounds, palette.Foreground, TextFormatFlags.Left | TextFormatFlags.VerticalCenter); + } + } + } + + private void ApplyThemeToToolStrip(ToolStrip toolStrip, ThemePalette palette) + { + if (toolStrip == null) + return; + + toolStrip.BackColor = palette.MenuBackground; + toolStrip.ForeColor = palette.Foreground; + + foreach (ToolStripItem item in toolStrip.Items) + { + item.BackColor = palette.MenuBackground; + item.ForeColor = palette.Foreground; + } + } + + private void TabControl_Paint(object sender, PaintEventArgs e) + { + if (tabControl == null) + return; + + var palette = GetPalette(config.Theme); + var tabHeaderHeight = tabControl.TabCount > 0 ? tabControl.GetTabRect(0).Bottom : 0; + var headerArea = new Rectangle(0, 0, tabControl.ClientRectangle.Width, tabHeaderHeight); + var display = new Rectangle(0, tabHeaderHeight, tabControl.ClientRectangle.Width, tabControl.ClientRectangle.Height - tabHeaderHeight); + + if (config.Theme == UITheme.Default) + { + using var borderPen = new Pen(ControlPaint.Light(SystemColors.ControlDark, 0.2f)); + e.Graphics.DrawRectangle(borderPen, display); + return; + } + + using (var headerBrush = new SolidBrush(ControlPaint.Dark(palette.PanelBackground, 0.05f))) + { + e.Graphics.FillRectangle(headerBrush, headerArea); + } + + using (var bodyBrush = new SolidBrush(palette.PanelBackground)) + { + e.Graphics.FillRectangle(bodyBrush, display); + } + } + + private void SetTheme(UITheme theme) + { + if (config.Theme == theme) + return; + + config.Theme = theme; + ApplyTheme(); + UpdateThemeMenuChecks(); + } + + private void UpdateThemeMenuChecks() + { + if (defaultThemeToolStripMenuItem != null) + defaultThemeToolStripMenuItem.Checked = config.Theme == UITheme.Default; + if (darkThemeToolStripMenuItem != null) + darkThemeToolStripMenuItem.Checked = config.Theme == UITheme.PipBoy3000; + } + private void Main_Closing(object sender, FormClosingEventArgs e) { if (changed) @@ -1102,6 +1565,65 @@ bool GlassVisibilityV22(CustomControl _) CreateTooltips(); ControlFactory.UpdateVisibility(); + ApplyTheme(); + originalMaterial = CloneMaterial(currentMaterial); + } + + private BaseMaterialFile CaptureCurrentMaterialState() + { + if (currentMaterial == null) + return null; + + BaseMaterialFile snapshot = CurrentMaterialType switch + { + MaterialType.Effect => new BGEM(), + _ => new BGSM(), + }; + + GetMaterialValues(snapshot); + return snapshot; + } + + private BaseMaterialFile CloneMaterial(BaseMaterialFile source) + { + if (source == null) + return null; + + string tempPath = null; + try + { + tempPath = Path.GetTempFileName(); + using var stream = new FileStream(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + if (!source.Save(stream)) + return source; + + stream.Position = 0; + var clone = (BaseMaterialFile)Activator.CreateInstance(source.GetType()); + if (clone == null) + return source; + + if (!clone.Open(stream)) + return source; + + return clone; + } + catch + { + return source; + } + finally + { + if (!string.IsNullOrEmpty(tempPath)) + { + try + { + File.Delete(tempPath); + } + catch + { + } + } + } } private void CreateTooltips() diff --git a/MaterialFieldDescriptor.cs b/MaterialFieldDescriptor.cs new file mode 100644 index 0000000..638150f --- /dev/null +++ b/MaterialFieldDescriptor.cs @@ -0,0 +1,262 @@ +using MaterialLib; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Material_Editor +{ + public enum FieldCategory + { + General, + Material, + Effect + } + + public sealed class MaterialFieldDescriptor + { + public string Label { get; } + public FieldCategory Category { get; } + public Func Getter { get; } + public Action Setter { get; } + public Func Supports { get; } + + public MaterialFieldDescriptor( + string label, + FieldCategory category, + Func getter, + Action setter, + Func supports = null) + { + Label = label; + Category = category; + Getter = getter; + Setter = setter; + Supports = supports; + } + + public bool IsSupported(BaseMaterialFile material) + { + return material != null && (Supports?.Invoke(material) ?? true); + } + + public object GetValue(BaseMaterialFile material) => Getter(material); + + public void SetValue(BaseMaterialFile material, object value) + { + Setter(material, value); + } + + public bool HasChanged(BaseMaterialFile baseline, BaseMaterialFile current) + { + if (baseline == null || current == null) + return false; + + var original = Getter(baseline); + var modified = Getter(current); + + return !ValuesEqual(original, modified); + } + + private static bool ValuesEqual(object original, object modified) + { + if (original == null && modified == null) + return true; + + if (original == null || modified == null) + return false; + + if (original is float of && modified is float mf) + return Math.Abs(of - mf) < 0.0001f; + + if (original is double od && modified is double md) + return Math.Abs(od - md) < 0.0001; + + if (original is decimal oc && modified is decimal mc) + return Math.Abs(oc - mc) < 0.0001m; + + return original.Equals(modified); + } + } + + public static class MaterialFieldRegistry + { + public static IReadOnlyList GetDescriptors(BaseMaterialFile material) + { + if (material == null) + return Array.Empty(); + + return AllFields.Where(descriptor => descriptor.IsSupported(material)).ToList(); + } + + private static readonly IReadOnlyList GeneralFields = new MaterialFieldDescriptor[] { + CreateGeneral(ControlNames.TileU, m => m.TileU, (m, v) => m.TileU = v), + CreateGeneral(ControlNames.TileV, m => m.TileV, (m, v) => m.TileV = v), + CreateGeneral(ControlNames.OffsetU, m => m.UOffset, (m, v) => m.UOffset = v), + CreateGeneral(ControlNames.OffsetV, m => m.VOffset, (m, v) => m.VOffset = v), + CreateGeneral(ControlNames.ScaleU, m => m.UScale, (m, v) => m.UScale = v), + CreateGeneral(ControlNames.ScaleV, m => m.VScale, (m, v) => m.VScale = v), + CreateGeneral(ControlNames.Alpha, m => m.Alpha, (m, v) => m.Alpha = v), + CreateGeneral(ControlNames.AlphaBlendMode, m => m.AlphaBlendMode, (m, v) => m.AlphaBlendMode = v), + CreateGeneral(ControlNames.AlphaTestReference, m => m.AlphaTestRef, (m, v) => m.AlphaTestRef = v), + CreateGeneral(ControlNames.AlphaTest, m => m.AlphaTest, (m, v) => m.AlphaTest = v), + CreateGeneral(ControlNames.ZBufferWrite, m => m.ZBufferWrite, (m, v) => m.ZBufferWrite = v), + CreateGeneral(ControlNames.ZBufferTest, m => m.ZBufferTest, (m, v) => m.ZBufferTest = v), + CreateGeneral(ControlNames.ScreenSpaceReflections, m => m.ScreenSpaceReflections, (m, v) => m.ScreenSpaceReflections = v), + CreateGeneral(ControlNames.WetnessControlSSR, m => m.WetnessControlScreenSpaceReflections, (m, v) => m.WetnessControlScreenSpaceReflections = v), + CreateGeneral(ControlNames.Decal, m => m.Decal, (m, v) => m.Decal = v), + CreateGeneral(ControlNames.TwoSided, m => m.TwoSided, (m, v) => m.TwoSided = v), + CreateGeneral(ControlNames.DecalNoFade, m => m.DecalNoFade, (m, v) => m.DecalNoFade = v), + CreateGeneral(ControlNames.NonOccluder, m => m.NonOccluder, (m, v) => m.NonOccluder = v), + CreateGeneral(ControlNames.Refraction, m => m.Refraction, (m, v) => m.Refraction = v), + CreateGeneral(ControlNames.RefractionFalloff, m => m.RefractionFalloff, (m, v) => m.RefractionFalloff = v, m => m.Refraction), + CreateGeneral(ControlNames.RefractionPower, m => m.RefractionPower, (m, v) => m.RefractionPower = v, m => m.Refraction), + CreateGeneral(ControlNames.EnvironmentMapping, m => m.EnvironmentMapping, (m, v) => m.EnvironmentMapping = v, m => m.Version < 10), + CreateGeneral(ControlNames.EnvironmentMaskScale, m => m.EnvironmentMappingMaskScale, (m, v) => m.EnvironmentMappingMaskScale = v, m => m.Version < 10 && m.EnvironmentMapping), + CreateGeneral(ControlNames.DepthBias, m => m.DepthBias, (m, v) => m.DepthBias = v, m => m.Version >= 10), + CreateGeneral(ControlNames.GrayscaleToPaletteColor, m => m.GrayscaleToPaletteColor, (m, v) => m.GrayscaleToPaletteColor = v), + CreateGeneral(ControlNames.MaskWrites, m => m.MaskWrites, (m, v) => m.MaskWrites = v, m => m.Version >= 6), + }; + + private static readonly IReadOnlyList MaterialFields = new MaterialFieldDescriptor[] { + CreateBGSM(ControlNames.Diffuse, m => m.DiffuseTexture, (m, v) => m.DiffuseTexture = v), + CreateBGSM(ControlNames.Normal, m => m.NormalTexture, (m, v) => m.NormalTexture = v), + CreateBGSM(ControlNames.SmoothSpec, m => m.SmoothSpecTexture, (m, v) => m.SmoothSpecTexture = v), + CreateBGSM(ControlNames.Greyscale, m => m.GreyscaleTexture, (m, v) => m.GreyscaleTexture = v), + CreateBGSM(ControlNames.Environment, m => m.EnvmapTexture, (m, v) => m.EnvmapTexture = v, m => m.Version <= 2), + CreateBGSM(ControlNames.Glow, m => m.GlowTexture, (m, v) => m.GlowTexture = v), + CreateBGSM(ControlNames.InnerLayer, m => m.InnerLayerTexture, (m, v) => m.InnerLayerTexture = v, m => m.Version <= 2), + CreateBGSM(ControlNames.Wrinkles, m => m.WrinklesTexture, (m, v) => m.WrinklesTexture = v), + CreateBGSM(ControlNames.Displacement, m => m.DisplacementTexture, (m, v) => m.DisplacementTexture = v, m => m.Version <= 2), + CreateBGSM(ControlNames.Specular, m => m.SpecularTexture, (m, v) => m.SpecularTexture = v, m => m.Version > 2), + CreateBGSM(ControlNames.Lighting, m => m.LightingTexture, (m, v) => m.LightingTexture = v, m => m.Version > 2), + CreateBGSM(ControlNames.Flow, m => m.FlowTexture, (m, v) => m.FlowTexture = v, m => m.Version > 2), + CreateBGSM(ControlNames.DistanceFieldAlpha, m => m.DistanceFieldAlphaTexture, (m, v) => m.DistanceFieldAlphaTexture = v, m => m.Version > 2), + CreateBGSM(ControlNames.EnableEditorAlphaRef, m => m.EnableEditorAlphaRef, (m, v) => m.EnableEditorAlphaRef = v), + CreateBGSM(ControlNames.RimLighting, m => m.RimLighting, (m, v) => m.RimLighting = v, m => m.Version < 8), + CreateBGSM(ControlNames.RimPower, m => m.RimPower, (m, v) => m.RimPower = v, m => m.Version < 8 && m.RimLighting), + CreateBGSM(ControlNames.BacklightPower, m => m.BackLightPower, (m, v) => m.BackLightPower = v, m => m.Version < 8), + CreateBGSM(ControlNames.SubsurfaceLighting, m => m.SubsurfaceLighting, (m, v) => m.SubsurfaceLighting = v, m => m.Version < 8), + CreateBGSM(ControlNames.SubsurfaceLightingRolloff, m => m.SubsurfaceLightingRolloff, (m, v) => m.SubsurfaceLightingRolloff = v, m => m.Version < 8 && m.SubsurfaceLighting), + CreateBGSM(ControlNames.Translucency, m => m.Translucency, (m, v) => m.Translucency = v, m => m.Version >= 8), + CreateBGSM(ControlNames.TranslucencyThickObject, m => m.TranslucencyThickObject, (m, v) => m.TranslucencyThickObject = v, m => m.Version >= 8), + CreateBGSM(ControlNames.TranslucencyAlbSubsurfColor, m => m.TranslucencyMixAlbedoWithSubsurfaceColor, (m, v) => m.TranslucencyMixAlbedoWithSubsurfaceColor = v, m => m.Version >= 8), + CreateBGSM(ControlNames.TranslucencySubsurfaceColor, m => m.TranslucencySubsurfaceColor, (m, v) => m.TranslucencySubsurfaceColor = v, m => m.Version >= 8), + CreateBGSM(ControlNames.TranslucencyTransmissiveScale, m => m.TranslucencyTransmissiveScale, (m, v) => m.TranslucencyTransmissiveScale = v, m => m.Version >= 8), + CreateBGSM(ControlNames.TranslucencyTurbulence, m => m.TranslucencyTurbulence, (m, v) => m.TranslucencyTurbulence = v, m => m.Version >= 8), + CreateBGSM(ControlNames.SpecularEnabled, m => m.SpecularEnabled, (m, v) => m.SpecularEnabled = v), + CreateBGSM(ControlNames.SpecularColor, m => m.SpecularColor, (m, v) => m.SpecularColor = v), + CreateBGSM(ControlNames.SpecularMultiplier, m => m.SpecularMult, (m, v) => m.SpecularMult = v), + CreateBGSM(ControlNames.Smoothness, m => m.Smoothness, (m, v) => m.Smoothness = v), + CreateBGSM(ControlNames.FresnelPower, m => m.FresnelPower, (m, v) => m.FresnelPower = v), + CreateBGSM(ControlNames.WetSpecScale, m => m.WetnessControlSpecScale, (m, v) => m.WetnessControlSpecScale = v), + CreateBGSM(ControlNames.WetSpecPowerScale, m => m.WetnessControlSpecPowerScale, (m, v) => m.WetnessControlSpecPowerScale = v), + CreateBGSM(ControlNames.WetSpecMinVar, m => m.WetnessControlSpecMinvar, (m, v) => m.WetnessControlSpecMinvar = v), + CreateBGSM(ControlNames.WetEnvMapScale, m => m.WetnessControlEnvMapScale, (m, v) => m.WetnessControlEnvMapScale = v, m => m.Version < 10), + CreateBGSM(ControlNames.WetFresnelPower, m => m.WetnessControlFresnelPower, (m, v) => m.WetnessControlFresnelPower = v), + CreateBGSM(ControlNames.WetMetalness, m => m.WetnessControlMetalness, (m, v) => m.WetnessControlMetalness = v), + CreateBGSM(ControlNames.PBR, m => m.PBR, (m, v) => m.PBR = v, m => m.Version > 2), + CreateBGSM(ControlNames.CustomPorosity, m => m.CustomPorosity, (m, v) => m.CustomPorosity = v, m => m.Version >= 9), + CreateBGSM(ControlNames.PorosityValue, m => m.PorosityValue, (m, v) => m.PorosityValue = v, m => m.Version >= 9), + CreateBGSM(ControlNames.RootMaterialPath, m => m.RootMaterialPath, (m, v) => m.RootMaterialPath = v), + CreateBGSM(ControlNames.AnisoLighting, m => m.AnisoLighting, (m, v) => m.AnisoLighting = v), + CreateBGSM(ControlNames.EmittanceEnabled, m => m.EmitEnabled, (m, v) => m.EmitEnabled = v), + CreateBGSM(ControlNames.EmittanceColor, m => m.EmittanceColor, (m, v) => m.EmittanceColor = v), + CreateBGSM(ControlNames.EmittanceMultiplier, m => m.EmittanceMult, (m, v) => m.EmittanceMult = v), + CreateBGSM(ControlNames.ModelSpaceNormals, m => m.ModelSpaceNormals, (m, v) => m.ModelSpaceNormals = v), + CreateBGSM(ControlNames.ExternalEmittance, m => m.ExternalEmittance, (m, v) => m.ExternalEmittance = v), + CreateBGSM(ControlNames.LumEmittance, m => m.LumEmittance, (m, v) => m.LumEmittance = v, m => m.Version >= 12), + CreateBGSM(ControlNames.AdaptativeEmissive, m => m.UseAdaptativeEmissive, (m, v) => m.UseAdaptativeEmissive = v, m => m.Version >= 13), + CreateBGSM(ControlNames.AdaptEmissiveExposureOffset, m => m.AdaptativeEmissive_ExposureOffset, (m, v) => m.AdaptativeEmissive_ExposureOffset = v, m => m.Version >= 13 && m.UseAdaptativeEmissive), + CreateBGSM(ControlNames.AdaptEmissiveFinalExposureMin, m => m.AdaptativeEmissive_FinalExposureMin, (m, v) => m.AdaptativeEmissive_FinalExposureMin = v, m => m.Version >= 13 && m.UseAdaptativeEmissive), + CreateBGSM(ControlNames.AdaptEmissiveFinalExposureMax, m => m.AdaptativeEmissive_FinalExposureMax, (m, v) => m.AdaptativeEmissive_FinalExposureMax = v, m => m.Version >= 13 && m.UseAdaptativeEmissive), + CreateBGSM(ControlNames.BackLighting, m => m.BackLighting, (m, v) => m.BackLighting = v, m => m.Version < 8), + CreateBGSM(ControlNames.ReceiveShadows, m => m.ReceiveShadows, (m, v) => m.ReceiveShadows = v), + CreateBGSM(ControlNames.HideSecret, m => m.HideSecret, (m, v) => m.HideSecret = v), + CreateBGSM(ControlNames.CastShadows, m => m.CastShadows, (m, v) => m.CastShadows = v), + CreateBGSM(ControlNames.DissolveFade, m => m.DissolveFade, (m, v) => m.DissolveFade = v), + CreateBGSM(ControlNames.AssumeShadowmask, m => m.AssumeShadowmask, (m, v) => m.AssumeShadowmask = v), + CreateBGSM(ControlNames.Glowmap, m => m.Glowmap, (m, v) => m.Glowmap = v), + CreateBGSM(ControlNames.EnvironmentMapWindow, m => m.EnvironmentMappingWindow, (m, v) => m.EnvironmentMappingWindow = v, m => m.Version < 7), + CreateBGSM(ControlNames.EnvironmentMapEye, m => m.EnvironmentMappingEye, (m, v) => m.EnvironmentMappingEye = v, m => m.Version < 7), + CreateBGSM(ControlNames.Hair, m => m.Hair, (m, v) => m.Hair = v), + CreateBGSM(ControlNames.HairTintColor, m => m.HairTintColor, (m, v) => m.HairTintColor = v), + CreateBGSM(ControlNames.Tree, m => m.Tree, (m, v) => m.Tree = v), + CreateBGSM(ControlNames.Facegen, m => m.Facegen, (m, v) => m.Facegen = v), + CreateBGSM(ControlNames.SkinTint, m => m.SkinTint, (m, v) => m.SkinTint = v), + CreateBGSM(ControlNames.Tessellate, m => m.Tessellate, (m, v) => m.Tessellate = v), + CreateBGSM(ControlNames.DisplacementTexBias, m => m.DisplacementTextureBias, (m, v) => m.DisplacementTextureBias = v, m => m.Version < 3 && m.Tessellate), + CreateBGSM(ControlNames.DisplacementTexScale, m => m.DisplacementTextureScale, (m, v) => m.DisplacementTextureScale = v, m => m.Version < 3 && m.Tessellate), + CreateBGSM(ControlNames.TessellationPNScale, m => m.TessellationPnScale, (m, v) => m.TessellationPnScale = v, m => m.Version < 3 && m.Tessellate), + CreateBGSM(ControlNames.TessellationBaseFactor, m => m.TessellationBaseFactor, (m, v) => m.TessellationBaseFactor = v, m => m.Version < 3 && m.Tessellate), + CreateBGSM(ControlNames.TessellationFadeDistance, m => m.TessellationFadeDistance, (m, v) => m.TessellationFadeDistance = v, m => m.Version < 3 && m.Tessellate), + CreateBGSM(ControlNames.GrayscaleToPaletteScale, m => m.GrayscaleToPaletteScale, (m, v) => m.GrayscaleToPaletteScale = v), + CreateBGSM(ControlNames.SkewSpecularAlpha, m => m.SkewSpecularAlpha, (m, v) => m.SkewSpecularAlpha = v, m => m.Version >= 1), + CreateBGSM(ControlNames.Terrain, m => m.Terrain, (m, v) => m.Terrain = v, m => m.Version >= 3), + CreateBGSM(ControlNames.UnkInt1BGSM, m => m.UnkInt1, (m, v) => m.UnkInt1 = v, m => m.Version == 3 && m.Terrain), + CreateBGSM(ControlNames.TerrainThresholdFalloff, m => m.TerrainThresholdFalloff, (m, v) => m.TerrainThresholdFalloff = v, m => m.Version >= 3 && m.Terrain), + CreateBGSM(ControlNames.TerrainTilingDistance, m => m.TerrainTilingDistance, (m, v) => m.TerrainTilingDistance = v, m => m.Version >= 3 && m.Terrain), + CreateBGSM(ControlNames.TerrainRotationAngle, m => m.TerrainRotationAngle, (m, v) => m.TerrainRotationAngle = v, m => m.Version >= 3 && m.Terrain), + }; + + private static readonly IReadOnlyList EffectFields = new MaterialFieldDescriptor[] { + CreateBGEM(ControlNames.BaseTexture, m => m.BaseTexture, (m, v) => m.BaseTexture = v), + CreateBGEM(ControlNames.GrayscaleTexture, m => m.GrayscaleTexture, (m, v) => m.GrayscaleTexture = v), + CreateBGEM(ControlNames.EnvmapTexture, m => m.EnvmapTexture, (m, v) => m.EnvmapTexture = v), + CreateBGEM(ControlNames.NormalTexture, m => m.NormalTexture, (m, v) => m.NormalTexture = v), + CreateBGEM(ControlNames.EnvmapMaskTexture, m => m.EnvmapMaskTexture, (m, v) => m.EnvmapMaskTexture = v), + CreateBGEM(ControlNames.SpecularTexture, m => m.SpecularTexture, (m, v) => m.SpecularTexture = v, m => m.Version >= 11), + CreateBGEM(ControlNames.LightingTexture, m => m.LightingTexture, (m, v) => m.LightingTexture = v, m => m.Version >= 11), + CreateBGEM(ControlNames.GlowTexture, m => m.GlowTexture, (m, v) => m.GlowTexture = v, m => m.Version >= 11), + CreateBGEM(ControlNames.GlassRoughnessScratch, m => m.GlassRoughnessScratch, (m, v) => m.GlassRoughnessScratch = v, m => m.Version >= 21), + CreateBGEM(ControlNames.GlassDirtOverlay, m => m.GlassDirtOverlay, (m, v) => m.GlassDirtOverlay = v, m => m.Version >= 21), + CreateBGEM(ControlNames.GlassEnabled, m => m.GlassEnabled, (m, v) => m.GlassEnabled = v, m => m.Version >= 21), + CreateBGEM(ControlNames.GlassFresnelColor, m => m.GlassFresnelColor, (m, v) => m.GlassFresnelColor = v, m => m.Version >= 21 && m.GlassEnabled), + CreateBGEM(ControlNames.GlassBlurScaleBase, m => m.GlassBlurScaleBase, (m, v) => m.GlassBlurScaleBase = v, m => m.Version >= 21 && m.GlassEnabled), + CreateBGEM(ControlNames.GlassBlurScaleFactor, m => m.GlassBlurScaleFactor, (m, v) => m.GlassBlurScaleFactor = v, m => m.Version >= 22 && m.GlassEnabled), + CreateBGEM(ControlNames.GlassRefractionScaleBase, m => m.GlassRefractionScaleBase, (m, v) => m.GlassRefractionScaleBase = v, m => m.Version >= 21 && m.GlassEnabled), + CreateBGEM(ControlNames.EnvMapping, m => m.EnvironmentMapping, (m, v) => m.EnvironmentMapping = v, m => m.Version >= 10), + CreateBGEM(ControlNames.EnvMappingMaskScale, m => m.EnvironmentMappingMaskScale, (m, v) => m.EnvironmentMappingMaskScale = v, m => m.Version >= 10), + CreateBGEM(ControlNames.BloodEnabled, m => m.BloodEnabled, (m, v) => m.BloodEnabled = v), + CreateBGEM(ControlNames.EffectLightingEnabled, m => m.EffectLightingEnabled, (m, v) => m.EffectLightingEnabled = v), + CreateBGEM(ControlNames.FalloffEnabled, m => m.FalloffEnabled, (m, v) => m.FalloffEnabled = v), + CreateBGEM(ControlNames.FalloffColorEnabled, m => m.FalloffColorEnabled, (m, v) => m.FalloffColorEnabled = v), + CreateBGEM(ControlNames.GrayscaleToPaletteAlpha, m => m.GrayscaleToPaletteAlpha, (m, v) => m.GrayscaleToPaletteAlpha = v), + CreateBGEM(ControlNames.SoftEnabled, m => m.SoftEnabled, (m, v) => m.SoftEnabled = v), + CreateBGEM(ControlNames.BaseColor, m => m.BaseColor, (m, v) => m.BaseColor = v), + CreateBGEM(ControlNames.BaseColorScale, m => m.BaseColorScale, (m, v) => m.BaseColorScale = v), + CreateBGEM(ControlNames.FalloffStartAngle, m => m.FalloffStartAngle, (m, v) => m.FalloffStartAngle = v, m => m.FalloffEnabled), + CreateBGEM(ControlNames.FalloffStopAngle, m => m.FalloffStopAngle, (m, v) => m.FalloffStopAngle = v, m => m.FalloffEnabled), + CreateBGEM(ControlNames.FalloffStartOpacity, m => m.FalloffStartOpacity, (m, v) => m.FalloffStartOpacity = v, m => m.FalloffEnabled), + CreateBGEM(ControlNames.FalloffStopOpacity, m => m.FalloffStopOpacity, (m, v) => m.FalloffStopOpacity = v, m => m.FalloffEnabled), + CreateBGEM(ControlNames.LightingInfluence, m => m.LightingInfluence, (m, v) => m.LightingInfluence = v), + CreateBGEM(ControlNames.EnvmapMinLOD, m => m.EnvmapMinLOD, (m, v) => m.EnvmapMinLOD = v), + CreateBGEM(ControlNames.SoftDepth, m => m.SoftDepth, (m, v) => m.SoftDepth = v, m => m.SoftEnabled), + CreateBGEM(ControlNames.EmitColor, m => m.EmittanceColor, (m, v) => m.EmittanceColor = v, m => m.Version >= 11), + CreateBGEM(ControlNames.AdaptativeEmissiveExposureOffset, m => m.AdaptativeEmissive_ExposureOffset, (m, v) => m.AdaptativeEmissive_ExposureOffset = v, m => m.Version >= 15), + CreateBGEM(ControlNames.AdaptativeEmissiveFinalExposureMin, m => m.AdaptativeEmissive_FinalExposureMin, (m, v) => m.AdaptativeEmissive_FinalExposureMin = v, m => m.Version >= 15), + CreateBGEM(ControlNames.AdaptativeEmissiveFinalExposureMax, m => m.AdaptativeEmissive_FinalExposureMax, (m, v) => m.AdaptativeEmissive_FinalExposureMax = v, m => m.Version >= 15), + CreateBGEM(ControlNames.EffectGlowmap, m => m.Glowmap, (m, v) => m.Glowmap = v, m => m.Version >= 16), + CreateBGEM(ControlNames.EffectPBRSpecular, m => m.EffectPbrSpecular, (m, v) => m.EffectPbrSpecular = v, m => m.Version >= 20), + }; + + private static readonly IReadOnlyList AllFields = GeneralFields + .Concat(MaterialFields) + .Concat(EffectFields) + .ToList(); + + private static MaterialFieldDescriptor CreateGeneral(string label, Func getter, Action setter, Func supports = null) + { + return new MaterialFieldDescriptor(label, FieldCategory.General, m => getter(m), (m, v) => setter(m, (T)v), supports); + } + + private static MaterialFieldDescriptor CreateBGSM(string label, Func getter, Action setter, Func supports = null) + { + return new MaterialFieldDescriptor(label, FieldCategory.Material, m => getter((BGSM)m), (m, v) => setter((BGSM)m, (T)v), m => m is BGSM bgsm && (supports?.Invoke(bgsm) ?? true)); + } + + private static MaterialFieldDescriptor CreateBGEM(string label, Func getter, Action setter, Func supports = null) + { + return new MaterialFieldDescriptor(label, FieldCategory.Effect, m => getter((BGEM)m), (m, v) => setter((BGEM)m, (T)v), m => m is BGEM bgem && (supports?.Invoke(bgem) ?? true)); + } + } +} diff --git a/OverwriteSummaryDialog.cs b/OverwriteSummaryDialog.cs new file mode 100644 index 0000000..1cab4d7 --- /dev/null +++ b/OverwriteSummaryDialog.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace Material_Editor +{ + internal sealed class OverwriteSummaryDialog : Form + { + private readonly ListView listView; + private readonly Label summaryLabel; + private readonly Button okButton; + + public OverwriteSummaryDialog(IReadOnlyList results, ThemePalette palette) + { + Text = "Overwrite Summary"; + FormBorderStyle = FormBorderStyle.FixedDialog; + StartPosition = FormStartPosition.CenterParent; + ClientSize = new Size(660, 520); + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + + var listView = new ListView + { + Bounds = new Rectangle(12, 12, 636, 420), + View = View.Details, + FullRowSelect = true, + HeaderStyle = ColumnHeaderStyle.Nonclickable + }; + listView.Columns.Add("Status", 120); + listView.Columns.Add("File", 360); + listView.Columns.Add("Details", 140); + listView.BorderStyle = BorderStyle.FixedSingle; + + foreach (var result in results) + { + var item = new ListViewItem(result.Status.ToString()) + { + ForeColor = result.Status switch + { + FieldCopyStatus.Success => Color.Green, + FieldCopyStatus.Skipped => Color.DarkOrange, + FieldCopyStatus.Failed => Color.Red, + _ => Color.Black, + } + }; + item.SubItems.Add(result.TargetPath); + item.SubItems.Add(result.Message); + listView.Items.Add(item); + } + + listView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); + Controls.Add(listView); + + summaryLabel = new Label + { + Bounds = new Rectangle(12, 440, 636, 28), + Text = $"{results.Count(r => r.Status == FieldCopyStatus.Success)} succeeded, {results.Count(r => r.Status == FieldCopyStatus.Skipped)} skipped, {results.Count(r => r.Status == FieldCopyStatus.Failed)} failed." + }; + Controls.Add(summaryLabel); + + var okButton = new Button + { + Text = "Close", + Bounds = new Rectangle(558, 470, 90, 30), + DialogResult = DialogResult.OK + }; + Controls.Add(okButton); + + AcceptButton = okButton; + + this.listView = listView; + this.okButton = okButton; + + ApplyPalette(palette); + } + + private void ApplyPalette(ThemePalette palette) + { + BackColor = palette.FormBackground; + ForeColor = palette.Foreground; + + listView.BackColor = palette.PanelBackground; + listView.ForeColor = palette.Foreground; + listView.GridLines = false; + + summaryLabel.BackColor = palette.FormBackground; + summaryLabel.ForeColor = palette.Foreground; + + okButton.BackColor = palette.ControlBackground; + okButton.ForeColor = palette.Foreground; + } + } +} diff --git a/Program.cs b/Program.cs index 39d419b..86fd617 100644 --- a/Program.cs +++ b/Program.cs @@ -13,8 +13,18 @@ static void Main() { // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. - ApplicationConfiguration.Initialize(); - Application.Run(new Main()); + try + { + ApplicationConfiguration.Initialize(); + var config = Material_Editor.Main.LoadConfig(); + if (config.Font != null) + Application.SetDefaultFont(config.Font); + Application.Run(new Material_Editor.Main(config)); + } + catch (Exception ex) + { + MessageBox.Show($"Unhandled startup exception:{Environment.NewLine}{ex}", "Startup Failure", MessageBoxButtons.OK, MessageBoxIcon.Error); + } } } -} \ No newline at end of file +} diff --git a/TargetFileSelectionDialog.cs b/TargetFileSelectionDialog.cs new file mode 100644 index 0000000..477c37b --- /dev/null +++ b/TargetFileSelectionDialog.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms; + +namespace Material_Editor +{ + internal sealed class TargetFileSelectionDialog : Form + { + private readonly ListView targetList; + private readonly Button addFilesButton; + private readonly Button addFolderButton; + private readonly Button removeButton; + private readonly Button okButton; + private readonly CheckBox backupCheckBox; + private readonly HashSet filePaths = new(StringComparer.OrdinalIgnoreCase); + private readonly Label introLabel; + private readonly Button cancelButton; + + public IReadOnlyList TargetFiles => targetList.Items + .Cast() + .Select(item => item.Tag as string) + .ToList(); + + public bool BackupBeforeWrite => backupCheckBox.Checked; + + public TargetFileSelectionDialog(ThemePalette palette) + { + Text = "Select Target Files"; + FormBorderStyle = FormBorderStyle.FixedDialog; + StartPosition = FormStartPosition.CenterParent; + ClientSize = new Size(600, 520); + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + + introLabel = new Label + { + Text = "Add material (.bgsm/.bgem) files or folders to apply the selected fields to.", + Bounds = new Rectangle(12, 12, 576, 24), + AutoSize = false + }; + Controls.Add(introLabel); + + targetList = new ListView + { + Bounds = new Rectangle(12, 42, 576, 360), + View = View.Details, + FullRowSelect = true, + HeaderStyle = ColumnHeaderStyle.Nonclickable + }; + targetList.Columns.Add("Target File", 560); + targetList.SelectedIndexChanged += (s, e) => UpdateRemoveButton(); + Controls.Add(targetList); + + addFilesButton = new Button + { + Text = "Add Files...", + Bounds = new Rectangle(12, 410, 100, 28) + }; + addFilesButton.Click += (s, e) => AddFiles(); + Controls.Add(addFilesButton); + + addFolderButton = new Button + { + Text = "Add Folder...", + Bounds = new Rectangle(122, 410, 100, 28) + }; + addFolderButton.Click += (s, e) => AddFolder(); + Controls.Add(addFolderButton); + + removeButton = new Button + { + Text = "Remove", + Bounds = new Rectangle(232, 410, 100, 28), + Enabled = false + }; + removeButton.Click += (s, e) => RemoveSelected(); + Controls.Add(removeButton); + + backupCheckBox = new CheckBox + { + Text = "Create .bak backup for each overwritten file", + Bounds = new Rectangle(12, 450, 400, 24), + Checked = true + }; + Controls.Add(backupCheckBox); + + okButton = new Button + { + Text = "OK", + Bounds = new Rectangle(422, 460, 80, 28), + DialogResult = DialogResult.OK, + Enabled = false + }; + okButton.Click += (s, e) => UpdateOkState(); + Controls.Add(okButton); + + cancelButton = new Button + { + Text = "Cancel", + Bounds = new Rectangle(514, 460, 74, 28), + DialogResult = DialogResult.Cancel + }; + Controls.Add(cancelButton); + + AcceptButton = okButton; + CancelButton = cancelButton; + + ApplyPalette(palette); + } + + private void AddFiles() + { + using var dialog = new OpenFileDialog + { + Filter = "Material Files (.bgsm;.bgem)|*.bgsm;*.bgem", + Multiselect = true, + Title = "Select material files..." + }; + + if (dialog.ShowDialog() != DialogResult.OK) + return; + + AddPaths(dialog.FileNames); + } + + private void AddFolder() + { + using var dialog = new FolderBrowserDialog + { + Description = "Select a folder to scan for materials..." + }; + + if (dialog.ShowDialog() != DialogResult.OK) + return; + + try + { + var matches = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.AllDirectories) + .Where(p => p.EndsWith(".bgsm", StringComparison.OrdinalIgnoreCase) || p.EndsWith(".bgem", StringComparison.OrdinalIgnoreCase)); + + AddPaths(matches); + } + catch (Exception) + { + MessageBox.Show("Unable to scan the selected folder for material files.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } + + private void AddPaths(IEnumerable paths) + { + foreach (var path in paths) + { + try + { + var fullPath = Path.GetFullPath(path); + if (filePaths.Add(fullPath)) + { + var item = new ListViewItem(fullPath) { Tag = fullPath }; + targetList.Items.Add(item); + } + } + catch (Exception) + { + // Ignore invalid paths. + } + } + + UpdateOkState(); + } + + private void RemoveSelected() + { + foreach (ListViewItem item in targetList.SelectedItems) + { + filePaths.Remove(item.Tag as string); + targetList.Items.Remove(item); + } + + UpdateOkState(); + } + + private void UpdateRemoveButton() + { + removeButton.Enabled = targetList.SelectedItems.Count > 0; + } + + private void UpdateOkState() + { + okButton.Enabled = targetList.Items.Count > 0; + } + + private void ApplyPalette(ThemePalette palette) + { + BackColor = palette.FormBackground; + ForeColor = palette.Foreground; + + introLabel.BackColor = palette.FormBackground; + introLabel.ForeColor = palette.Foreground; + + targetList.BackColor = palette.PanelBackground; + targetList.ForeColor = palette.Foreground; + + addFilesButton.BackColor = palette.ControlBackground; + addFolderButton.BackColor = palette.ControlBackground; + removeButton.BackColor = palette.ControlBackground; + okButton.BackColor = palette.ControlBackground; + cancelButton.BackColor = palette.ControlBackground; + backupCheckBox.BackColor = palette.FormBackground; + backupCheckBox.ForeColor = palette.Foreground; + } + } +} diff --git a/ThemePalette.cs b/ThemePalette.cs new file mode 100644 index 0000000..c1f795c --- /dev/null +++ b/ThemePalette.cs @@ -0,0 +1,58 @@ +using System.Drawing; + +namespace Material_Editor +{ + public enum UITheme + { + Default, + PipBoy3000 + } + + public readonly struct ThemePalette + { + public Color FormBackground { get; } + public Color ControlBackground { get; } + public Color PanelBackground { get; } + public Color MenuBackground { get; } + public Color Foreground { get; } + public Color Accent { get; } + + public ThemePalette(Color formBackground, Color controlBackground, Color panelBackground, Color menuBackground, Color foreground, Color accent) + { + FormBackground = formBackground; + ControlBackground = controlBackground; + PanelBackground = panelBackground; + MenuBackground = menuBackground; + Foreground = foreground; + Accent = accent; + } + } + + internal static class ThemeManager + { + private static readonly ThemePalette DefaultPalette = new( + SystemColors.Control, + SystemColors.Control, + Color.WhiteSmoke, + SystemColors.Control, + SystemColors.ControlText, + Color.Empty); + + private static readonly ThemePalette DarkPalette = new( + Color.FromArgb(10, 12, 5), + Color.FromArgb(33, 37, 17), + Color.FromArgb(24, 32, 14), + Color.FromArgb(12, 15, 8), + Color.FromArgb(159, 255, 101), + Color.FromArgb(88, 178, 71)); + + public static ThemePalette GetPalette(UITheme theme) + { + return theme switch + { + UITheme.PipBoy3000 => DarkPalette, + _ => DefaultPalette + }; + } + } +} diff --git a/ThemedTabControl.cs b/ThemedTabControl.cs new file mode 100644 index 0000000..e644c06 --- /dev/null +++ b/ThemedTabControl.cs @@ -0,0 +1,93 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Material_Editor +{ + internal sealed class ThemedTabControl : TabControl + { + public Func PaletteProvider { get; set; } + public Func ShouldPaintBody { get; set; } = () => true; + + [DllImport("uxtheme.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] + private static extern int SetWindowTheme(IntPtr hWnd, string pszSubAppName, string pszSubIdList); + + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + SetWindowTheme(Handle, "", ""); + SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true); + } + + protected override void OnPaint(PaintEventArgs e) + { + var palette = PaletteProvider?.Invoke(); + if (!palette.HasValue) + { + base.OnPaint(e); + return; + } + base.OnPaint(e); + + var headerBottom = Enumerable.Range(0, TabCount) + .Select(GetTabRect) + .DefaultIfEmpty(new Rectangle(0, 0, 0, 0)) + .Max(rect => rect.Bottom); + + var display = DisplayRectangle; + var bodyTop = Math.Max(display.Y, headerBottom); + var bodyHeight = ClientRectangle.Height - bodyTop; + + var paintBody = ShouldPaintBody?.Invoke() ?? true; + if (!paintBody) + return; + + if (bodyHeight > 0) + { + var bodyRect = new Rectangle(ClientRectangle.X, bodyTop, ClientRectangle.Width, bodyHeight); + using var brush = new SolidBrush(palette.Value.PanelBackground); + e.Graphics.FillRectangle(brush, bodyRect); + } + } + + protected override void WndProc(ref Message m) + { + const int WM_PAINT = 0x000F; + base.WndProc(ref m); + + if (m.Msg != WM_PAINT) + return; + + var palette = PaletteProvider?.Invoke(); + if (!palette.HasValue) + return; + + using var graphics = Graphics.FromHwnd(Handle); + var accent = palette.Value.Accent.IsEmpty ? palette.Value.MenuBackground : palette.Value.Accent; + var unselected = ControlPaint.Dark(palette.Value.PanelBackground, 0.1f); + var border = palette.Value.MenuBackground; + var textColor = palette.Value.Foreground; + + for (int i = 0; i < TabCount; i++) + { + var rect = GetTabRect(i); + var fill = i == SelectedIndex ? accent : unselected; + + using (var brush = new SolidBrush(fill)) + { + graphics.FillRectangle(brush, rect); + } + + using (var pen = new Pen(border)) + { + pen.Alignment = System.Drawing.Drawing2D.PenAlignment.Inset; + graphics.DrawRectangle(pen, rect); + } + + TextRenderer.DrawText(graphics, TabPages[i].Text, Font, rect, textColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); + } + } + } +}