From 9ef1d6a015518e22ae289c194521f036fbfe8c24 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:22:58 +0100 Subject: [PATCH 1/5] Add Ctrl+F and Ctrl+G search shortcuts to image tabs Hues also gets F3 for the next name match. --- .../UserControls/HuesControl.cs | 26 +++++++++++++++++++ .../UserControls/ItemsControl.cs | 14 ++++++++++ .../UserControls/LandTilesControl.cs | 14 ++++++++++ .../UserControls/TexturesControl.cs | 7 +++++ 4 files changed, 61 insertions(+) diff --git a/UoFiddler.Controls/UserControls/HuesControl.cs b/UoFiddler.Controls/UserControls/HuesControl.cs index d29fc46b..6687cf25 100644 --- a/UoFiddler.Controls/UserControls/HuesControl.cs +++ b/UoFiddler.Controls/UserControls/HuesControl.cs @@ -344,6 +344,32 @@ private void HueIndexToolStripTextBox_KeyUp(object sender, KeyEventArgs e) vScrollBar.Value = scrollPosition; } + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + if (keyData == (Keys.Control | Keys.F)) + { + HueNameToolStripTextBox.Focus(); + HueNameToolStripTextBox.SelectAll(); + return true; + } + + if (keyData == (Keys.Control | Keys.G)) + { + HueIndexToolStripTextBox.Focus(); + HueIndexToolStripTextBox.SelectAll(); + return true; + } + + if (keyData == Keys.F3 && !HueNameToolStripTextBox.TextBox.Focused && + !string.IsNullOrEmpty(HueNameToolStripTextBox.Text)) + { + FindByName(Selected); + return true; + } + + return base.ProcessCmdKey(ref msg, keyData); + } + private void SearchNameToolStripButton_Click(object sender, EventArgs e) { FindByName(Selected); diff --git a/UoFiddler.Controls/UserControls/ItemsControl.cs b/UoFiddler.Controls/UserControls/ItemsControl.cs index 3f9b1cf7..13eec260 100644 --- a/UoFiddler.Controls/UserControls/ItemsControl.cs +++ b/UoFiddler.Controls/UserControls/ItemsControl.cs @@ -1604,6 +1604,20 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) return true; } + if (keyData == (Keys.Control | Keys.F)) + { + searchByNameToolStripTextBox.Focus(); + searchByNameToolStripTextBox.SelectAll(); + return true; + } + + if (keyData == (Keys.Control | Keys.G)) + { + searchByIdToolStripTextBox.Focus(); + searchByIdToolStripTextBox.SelectAll(); + return true; + } + if (keyData == Keys.F3 || keyData == (Keys.F3 | Keys.Shift)) { if (searchByNameToolStripTextBox.TextBox.Focused) diff --git a/UoFiddler.Controls/UserControls/LandTilesControl.cs b/UoFiddler.Controls/UserControls/LandTilesControl.cs index db81f395..0e11bfb9 100644 --- a/UoFiddler.Controls/UserControls/LandTilesControl.cs +++ b/UoFiddler.Controls/UserControls/LandTilesControl.cs @@ -1311,6 +1311,20 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) return true; } + if (keyData == (Keys.Control | Keys.F)) + { + searchByNameToolStripTextBox.Focus(); + searchByNameToolStripTextBox.SelectAll(); + return true; + } + + if (keyData == (Keys.Control | Keys.G)) + { + searchByIdToolStripTextBox.Focus(); + searchByIdToolStripTextBox.SelectAll(); + return true; + } + if (keyData == Keys.F3 || keyData == (Keys.F3 | Keys.Shift)) { if (searchByNameToolStripTextBox.TextBox.Focused) diff --git a/UoFiddler.Controls/UserControls/TexturesControl.cs b/UoFiddler.Controls/UserControls/TexturesControl.cs index c02ba781..20bfd309 100644 --- a/UoFiddler.Controls/UserControls/TexturesControl.cs +++ b/UoFiddler.Controls/UserControls/TexturesControl.cs @@ -1041,6 +1041,13 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) return true; } + if (keyData == (Keys.Control | Keys.F) || keyData == (Keys.Control | Keys.G)) + { + searchByIdToolStripTextBox.Focus(); + searchByIdToolStripTextBox.SelectAll(); + return true; + } + return base.ProcessCmdKey(ref msg, keyData); } From aef151847c1ff3dfed635954702818e0ebe0084d Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:24:15 +0100 Subject: [PATCH 2/5] Add copy, log folder and GitHub report buttons to the exception dialog --- UoFiddler/Forms/ExceptionForm.Designer.cs | 119 ++++++++++++++++------ UoFiddler/Forms/ExceptionForm.cs | 75 +++++++++++++- 2 files changed, 158 insertions(+), 36 deletions(-) diff --git a/UoFiddler/Forms/ExceptionForm.Designer.cs b/UoFiddler/Forms/ExceptionForm.Designer.cs index 4d328c9b..f9d51914 100644 --- a/UoFiddler/Forms/ExceptionForm.Designer.cs +++ b/UoFiddler/Forms/ExceptionForm.Designer.cs @@ -39,52 +39,105 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { - this.OK = new System.Windows.Forms.Button(); - this.richTextBox = new System.Windows.Forms.RichTextBox(); - this.SuspendLayout(); + OK = new System.Windows.Forms.Button(); + richTextBox = new System.Windows.Forms.RichTextBox(); + buttonPanel = new System.Windows.Forms.FlowLayoutPanel(); + CopyButton = new System.Windows.Forms.Button(); + OpenLogFolderButton = new System.Windows.Forms.Button(); + ReportButton = new System.Windows.Forms.Button(); + buttonPanel.SuspendLayout(); + SuspendLayout(); + // + // buttonPanel + // + buttonPanel.Controls.Add(OK); + buttonPanel.Controls.Add(ReportButton); + buttonPanel.Controls.Add(OpenLogFolderButton); + buttonPanel.Controls.Add(CopyButton); + buttonPanel.Dock = System.Windows.Forms.DockStyle.Bottom; + buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; + buttonPanel.Location = new System.Drawing.Point(0, 256); + buttonPanel.Name = "buttonPanel"; + buttonPanel.Padding = new System.Windows.Forms.Padding(2); + buttonPanel.Size = new System.Drawing.Size(674, 35); + buttonPanel.TabIndex = 2; // // OK // - this.OK.Dock = System.Windows.Forms.DockStyle.Bottom; - this.OK.Location = new System.Drawing.Point(0, 264); - this.OK.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - this.OK.Name = "OK"; - this.OK.Size = new System.Drawing.Size(674, 27); - this.OK.TabIndex = 0; - this.OK.Text = "OK"; - this.OK.UseVisualStyleBackColor = true; - this.OK.Click += new System.EventHandler(this.OnClick); + OK.Location = new System.Drawing.Point(567, 5); + OK.Name = "OK"; + OK.Size = new System.Drawing.Size(100, 25); + OK.TabIndex = 0; + OK.Text = "Close"; + OK.UseVisualStyleBackColor = true; + OK.Click += OnClick; + // + // ReportButton + // + ReportButton.Location = new System.Drawing.Point(431, 5); + ReportButton.Name = "ReportButton"; + ReportButton.Size = new System.Drawing.Size(130, 25); + ReportButton.TabIndex = 3; + ReportButton.Text = "Report on GitHub..."; + ReportButton.UseVisualStyleBackColor = true; + ReportButton.Click += OnClickReport; + // + // OpenLogFolderButton + // + OpenLogFolderButton.Location = new System.Drawing.Point(295, 5); + OpenLogFolderButton.Name = "OpenLogFolderButton"; + OpenLogFolderButton.Size = new System.Drawing.Size(130, 25); + OpenLogFolderButton.TabIndex = 2; + OpenLogFolderButton.Text = "Open Log Folder"; + OpenLogFolderButton.UseVisualStyleBackColor = true; + OpenLogFolderButton.Click += OnClickOpenLogFolder; + // + // CopyButton + // + CopyButton.Location = new System.Drawing.Point(159, 5); + CopyButton.Name = "CopyButton"; + CopyButton.Size = new System.Drawing.Size(130, 25); + CopyButton.TabIndex = 1; + CopyButton.Text = "Copy Details"; + CopyButton.UseVisualStyleBackColor = true; + CopyButton.Click += OnClickCopy; // // richTextBox // - this.richTextBox.Dock = System.Windows.Forms.DockStyle.Fill; - this.richTextBox.Location = new System.Drawing.Point(0, 0); - this.richTextBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - this.richTextBox.Name = "richTextBox"; - this.richTextBox.ReadOnly = true; - this.richTextBox.Size = new System.Drawing.Size(674, 264); - this.richTextBox.TabIndex = 1; - this.richTextBox.Text = ""; + richTextBox.Dock = System.Windows.Forms.DockStyle.Fill; + richTextBox.Location = new System.Drawing.Point(0, 0); + richTextBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + richTextBox.Name = "richTextBox"; + richTextBox.ReadOnly = true; + richTextBox.Size = new System.Drawing.Size(674, 256); + richTextBox.TabIndex = 1; + richTextBox.Text = ""; // // ExceptionForm // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(674, 291); - this.Controls.Add(this.richTextBox); - this.Controls.Add(this.OK); - this.DoubleBuffered = true; - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; - this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); - this.Name = "ExceptionForm"; - this.Text = "Exception"; - this.ResumeLayout(false); - + AcceptButton = OK; + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + CancelButton = OK; + ClientSize = new System.Drawing.Size(674, 291); + Controls.Add(richTextBox); + Controls.Add(buttonPanel); + DoubleBuffered = true; + FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; + Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + Name = "ExceptionForm"; + Text = "Exception"; + buttonPanel.ResumeLayout(false); + ResumeLayout(false); } #endregion private System.Windows.Forms.Button OK; private System.Windows.Forms.RichTextBox richTextBox; + private System.Windows.Forms.FlowLayoutPanel buttonPanel; + private System.Windows.Forms.Button CopyButton; + private System.Windows.Forms.Button OpenLogFolderButton; + private System.Windows.Forms.Button ReportButton; } -} \ No newline at end of file +} diff --git a/UoFiddler/Forms/ExceptionForm.cs b/UoFiddler/Forms/ExceptionForm.cs index dc9a3ec6..73d17c0f 100644 --- a/UoFiddler/Forms/ExceptionForm.cs +++ b/UoFiddler/Forms/ExceptionForm.cs @@ -1,27 +1,35 @@ /*************************************************************************** * * $Author: Turley - * + * * "THE BEER-WARE LICENSE" - * As long as you retain this notice you can do whatever you want with + * As long as you retain this notice you can do whatever you want with * this stuff. If we meet some day, and you think this stuff is worth it, * you can buy me a beer in return. * ***************************************************************************/ using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; using System.Windows.Forms; +using UoFiddler.Classes; using UoFiddler.Controls.Classes; namespace UoFiddler.Forms { public partial class ExceptionForm : Form { + private readonly Exception _error; + public ExceptionForm(Exception err) { InitializeComponent(); Icon = Options.GetFiddlerIcon(); + _error = err; + richTextBox.Text = err.InnerException != null ? $"{err.InnerException.Message}\n{err.InnerException.GetType()}" : string.Empty; @@ -33,5 +41,66 @@ private void OnClick(object sender, EventArgs e) { Close(); } + + private string ReportText => $"UOFiddler {FiddlerOptions.AppVersion}{Environment.NewLine}{_error}"; + + private bool TryCopyReport() + { + try + { + Clipboard.SetText(ReportText); + return true; + } + catch (ExternalException) + { + // The clipboard can be held by another process; copying is a convenience only. + return false; + } + } + + private void OnClickCopy(object sender, EventArgs e) + { + CopyButton.Text = TryCopyReport() ? "Copied" : "Copy failed"; + } + + private void OnClickOpenLogFolder(object sender, EventArgs e) + { + string logFolder = Path.Combine(Options.AppDataPath, "log"); + + try + { + Directory.CreateDirectory(logFolder); + Process.Start(new ProcessStartInfo { FileName = logFolder, UseShellExecute = true }); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.ComponentModel.Win32Exception) + { + MessageBox.Show(this, ex.Message, "Open Log Folder", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } + + /// + /// Opens a new GitHub issue titled after the error. The full details go to the clipboard to be + /// pasted in, since a stack trace easily exceeds what a URL can carry. + /// + private void OnClickReport(object sender, EventArgs e) + { + bool copied = TryCopyReport(); + + string title = Uri.EscapeDataString($"Crash: {_error.GetType().Name}: {_error.Message}"); + string body = Uri.EscapeDataString( + "**Steps to reproduce:**\n\n\n**Exception details:**\n" + + (copied ? "(paste from clipboard)\n" : "(use Copy Details in the error window)\n")); + + string url = $"https://github.com/{FiddlerOptions.RepositoryOwner}/{FiddlerOptions.RepositoryName}/issues/new?title={title}&body={body}"; + + try + { + Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); + } + catch (System.ComponentModel.Win32Exception ex) + { + MessageBox.Show(this, ex.Message, "Report on GitHub", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } } -} \ No newline at end of file +} From 08c8066ac3825196d9efaa94168d4b9380c748e4 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:25:59 +0100 Subject: [PATCH 3/5] Add reload progress, unsaved-tab markers and tab shortcuts to the main window Ctrl+1..9 switch tabs, Ctrl+R reloads files, F1 opens help. --- UoFiddler/Forms/MainForm.Designer.cs | 6 + UoFiddler/Forms/MainForm.cs | 232 ++++++++++++++++++--------- 2 files changed, 165 insertions(+), 73 deletions(-) diff --git a/UoFiddler/Forms/MainForm.Designer.cs b/UoFiddler/Forms/MainForm.Designer.cs index 84de6687..240882ba 100644 --- a/UoFiddler/Forms/MainForm.Designer.cs +++ b/UoFiddler/Forms/MainForm.Designer.cs @@ -739,6 +739,7 @@ private void InitializeComponent() SettingsMenu.Name = "SettingsMenu"; SettingsMenu.Size = new System.Drawing.Size(62, 22); SettingsMenu.Text = "Settings"; + SettingsMenu.ToolTipText = "Options, client paths and reloading files"; // // AlwaysOnTopMenuitem // @@ -781,6 +782,7 @@ private void InitializeComponent() reloadFilesMenuItem.Name = "reloadFilesMenuItem"; reloadFilesMenuItem.Size = new System.Drawing.Size(212, 22); reloadFilesMenuItem.Text = "Reload Files"; + reloadFilesMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.R; reloadFilesMenuItem.Click += ReloadFiles; // // toolStripDropDownButtonView @@ -791,6 +793,7 @@ private void InitializeComponent() toolStripDropDownButtonView.Name = "toolStripDropDownButtonView"; toolStripDropDownButtonView.Size = new System.Drawing.Size(45, 22); toolStripDropDownButtonView.Text = "View"; + toolStripDropDownButtonView.ToolTipText = "Show or hide tabs (Ctrl+1..9 switch tabs)"; // // ToggleViewStart // @@ -1030,6 +1033,7 @@ private void InitializeComponent() ExternToolsDropDown.Name = "ExternToolsDropDown"; ExternToolsDropDown.Size = new System.Drawing.Size(83, 22); ExternToolsDropDown.Text = "Extern Tools"; + ExternToolsDropDown.ToolTipText = "Run configured external tools"; // // manageToolStripMenuItem // @@ -1046,6 +1050,7 @@ private void InitializeComponent() toolStripDropDownButtonPlugins.Name = "toolStripDropDownButtonPlugins"; toolStripDropDownButtonPlugins.Size = new System.Drawing.Size(59, 22); toolStripDropDownButtonPlugins.Text = "Plugins"; + toolStripDropDownButtonPlugins.ToolTipText = "Plugin features and plugin management"; // // manageToolStripMenuItem1 // @@ -1074,6 +1079,7 @@ private void InitializeComponent() toolStripMenuItemHelp.Name = "toolStripMenuItemHelp"; toolStripMenuItemHelp.Size = new System.Drawing.Size(107, 22); toolStripMenuItemHelp.Text = "Help"; + toolStripMenuItemHelp.ShortcutKeys = System.Windows.Forms.Keys.F1; toolStripMenuItemHelp.Click += ToolStripMenuItemHelp_Click; // // toolStripSeparator1 diff --git a/UoFiddler/Forms/MainForm.cs b/UoFiddler/Forms/MainForm.cs index f1a9d6d1..4a1d5292 100644 --- a/UoFiddler/Forms/MainForm.cs +++ b/UoFiddler/Forms/MainForm.cs @@ -10,6 +10,7 @@ ***************************************************************************/ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Linq; @@ -45,12 +46,19 @@ private struct NativeRect { public int Left, Top, Right, Bottom; } private readonly ILogger _log; + private StatusStrip _statusStrip; + private ToolStripStatusLabel _statusLabel; + private ToolStripProgressBar _statusProgress; + private readonly Dictionary _tabTitles = new(); + public MainForm() : this(AppLog.For()) { } public MainForm(ILogger logger) { _log = logger; InitializeComponent(); + InitializeStatusStrip(); + InitializeUnsavedTabMarkers(); darkModeMenuItem.Checked = AppSettings.DarkMode; @@ -147,96 +155,92 @@ private void OnClickDarkMode(object sender, EventArgs e) private void ReloadFiles(object sender, EventArgs e) { - using (new WaitCursorScope(this)) + // Order matters: art has to be reloaded before tiledata for the latter to load properly. + var steps = new (string Label, bool Run, Action Reload)[] { - Verdata.Initialize(); - - if (Options.LoadedUltimaClass["Art"] || Options.LoadedUltimaClass["TileData"]) + ("Verdata", true, Verdata.Initialize), + ("Art and TileData", Options.LoadedUltimaClass["Art"] || Options.LoadedUltimaClass["TileData"], () => { - // Looks like we have to reload art first to have proper tiledata loading - // and order here is important Art.Reload(); TileData.Initialize(); - } - - if (Options.LoadedUltimaClass["Hues"]) - { - Hues.Initialize(); - } - - if (Options.LoadedUltimaClass["ASCIIFont"]) - { - AsciiText.Initialize(); - } - - if (Options.LoadedUltimaClass["UnicodeFont"]) - { - UnicodeFonts.Initialize(); - } - - if (Options.LoadedUltimaClass["Animdata"]) - { - Animdata.Initialize(); - } - - if (Options.LoadedUltimaClass["Light"]) + }), + ("Hues", Options.LoadedUltimaClass["Hues"], Hues.Initialize), + ("ASCII fonts", Options.LoadedUltimaClass["ASCIIFont"], AsciiText.Initialize), + ("Unicode fonts", Options.LoadedUltimaClass["UnicodeFont"], UnicodeFonts.Initialize), + ("Animdata", Options.LoadedUltimaClass["Animdata"], Animdata.Initialize), + ("Light", Options.LoadedUltimaClass["Light"], Light.Reload), + ("Skills", Options.LoadedUltimaClass["Skills"], Skills.Reload), + ("Sounds", Options.LoadedUltimaClass["Sound"], Sounds.Initialize), + ("Textures", Options.LoadedUltimaClass["Texture"], Textures.Reload), + ("Gumps", Options.LoadedUltimaClass["Gumps"], Gumps.Reload), + ("Animations", Options.LoadedUltimaClass["Animations"], Animations.Reload), + ("Radar colors", Options.LoadedUltimaClass["RadarColor"], RadarCol.Initialize), + ("Map", Options.LoadedUltimaClass["Map"], () => { - Light.Reload(); - } + MapHelper.CheckForNewMapSize(); + Map.Reload(); + }), + ("Multis", Options.LoadedUltimaClass["Multis"], Multis.Reload), + ("Speech", Options.LoadedUltimaClass["Speech"], SpeechList.Initialize), + ("Animation editor", Options.LoadedUltimaClass["AnimationEdit"], AnimationEdit.Reload), + }; - if (Options.LoadedUltimaClass["Skills"]) - { - Skills.Reload(); - } + var toRun = steps.Where(step => step.Run).ToList(); - if (Options.LoadedUltimaClass["Sound"]) - { - Sounds.Initialize(); - } - - if (Options.LoadedUltimaClass["Texture"]) - { - Textures.Reload(); - } + using (new WaitCursorScope(this)) + { + _statusProgress.Maximum = toRun.Count + 1; + _statusProgress.Value = 0; + _statusProgress.Visible = true; - if (Options.LoadedUltimaClass["Gumps"]) + foreach (var step in toRun) { - Gumps.Reload(); + SetStatus($"Reloading {step.Label}..."); + step.Reload(); + _statusProgress.Value++; } - if (Options.LoadedUltimaClass["Animations"]) - { - Animations.Reload(); - } + SetStatus("Refreshing tabs..."); + ControlEvents.FireFilePathChangeEvent(); - if (Options.LoadedUltimaClass["RadarColor"]) - { - RadarCol.Initialize(); - } + _statusProgress.Visible = false; + SetStatus("Files reloaded."); + } + } - if (Options.LoadedUltimaClass["Map"]) - { - MapHelper.CheckForNewMapSize(); - Map.Reload(); - } + private void InitializeStatusStrip() + { + _statusLabel = new ToolStripStatusLabel + { + Spring = true, + TextAlign = ContentAlignment.MiddleLeft + }; - if (Options.LoadedUltimaClass["Multis"]) - { - Multis.Reload(); - } + _statusProgress = new ToolStripProgressBar + { + Visible = false, + Size = new Size(160, 16) + }; - if (Options.LoadedUltimaClass["Speech"]) - { - SpeechList.Initialize(); - } + _statusStrip = new StatusStrip + { + SizingGrip = false + }; + _statusStrip.Items.Add(_statusLabel); + _statusStrip.Items.Add(_statusProgress); - if (Options.LoadedUltimaClass["AnimationEdit"]) - { - AnimationEdit.Reload(); - } + // Added last so it docks before the tab panel fills the rest of the window. + Controls.Add(_statusStrip); + } - ControlEvents.FireFilePathChangeEvent(); - } + /// + /// Shows a message in the status bar and repaints it straight away, since the callers keep + /// the UI thread busy. + /// + private void SetStatus(string message) + { + _statusLabel.Text = message; + _statusStrip.Refresh(); } /// @@ -732,5 +736,87 @@ private void ToolStripMenuItemAbout_Click(object sender, EventArgs e) aboutBoxForm.ShowDialog(this); } } + + /// + /// The keys whose unsaved changes a tab shows, + /// by tab tag. + /// + private static readonly Dictionary _tabChangeKeys = new() + { + { 1, new[] { "Multis" } }, + { 2, new[] { "Animations" } }, + { 3, new[] { "Art" } }, + { 4, new[] { "Art" } }, + { 5, new[] { "Texture" } }, + { 6, new[] { "Gumps" } }, + { 7, new[] { "Sound" } }, + { 8, new[] { "Hues" } }, + { 9, new[] { "ASCIIFont", "UnicodeFont" } }, + { 10, new[] { "CliLoc" } }, + { 11, new[] { "Map" } }, + { 12, new[] { "Light" } }, + { 13, new[] { "Speech" } }, + { 14, new[] { "Skills" } }, + { 15, new[] { "Animdata" } }, + { 18, new[] { "TileData" } }, + { 19, new[] { "RadarCol" } }, + { 20, new[] { "SkillGrp" } }, + { 22, new[] { "TileGroups" } }, + }; + + /// + /// Prefixes a tab's title with "* " while the data it edits has unsaved changes. The change + /// flags are plain dictionary entries set all over the controls, so they are polled. + /// + private void InitializeUnsavedTabMarkers() + { + var timer = new Timer(components) { Interval = 500 }; + timer.Tick += (_, _) => UpdateUnsavedTabMarkers(); + timer.Start(); + } + + private void UpdateUnsavedTabMarkers() + { + foreach (TabPage page in TabPanel.TabPages) + { + if (page.Tag is not int tag || !_tabChangeKeys.TryGetValue(tag, out string[] keys)) + { + continue; + } + + if (!_tabTitles.TryGetValue(page, out string title)) + { + title = page.Text; + _tabTitles[page] = title; + } + + bool unsaved = keys.Any(key => Options.ChangedUltimaClass.TryGetValue(key, out bool changed) && changed); + string text = unsaved ? "* " + title : title; + if (page.Text != text) + { + page.Text = text; + } + } + } + + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + // Ctrl+1..Ctrl+9 select the first nine visible tabs. + Keys key = keyData & Keys.KeyCode; + if ((keyData & Keys.Modifiers) == Keys.Control) + { + int index = key is >= Keys.D1 and <= Keys.D9 ? key - Keys.D1 + : key is >= Keys.NumPad1 and <= Keys.NumPad9 ? key - Keys.NumPad1 + : -1; + + if (index >= 0 && index < TabPanel.TabCount) + { + TabPanel.SelectedIndex = index; + return true; + } + } + + return base.ProcessCmdKey(ref msg, keyData); + } } } \ No newline at end of file From a177d8876335c75ec6c062a4715d3a8fb2745342 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:26:30 +0100 Subject: [PATCH 4/5] Flag a missing output folder and confirm Apply in Options --- UoFiddler/Forms/OptionsForm.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/UoFiddler/Forms/OptionsForm.cs b/UoFiddler/Forms/OptionsForm.cs index 44a68aa2..2308cbf1 100644 --- a/UoFiddler/Forms/OptionsForm.cs +++ b/UoFiddler/Forms/OptionsForm.cs @@ -27,6 +27,10 @@ public partial class OptionsForm : Form private readonly Action _updateItemsTabAction; private readonly Action _updateSoundTabAction; + private readonly ErrorProvider _outputPathError; + private readonly Timer _savedTimer; + private readonly string _title; + public OptionsForm(Action updateAllTileViewsAction, Action updateItemsTabAction, Action updateSoundTabAction, @@ -41,6 +45,15 @@ public OptionsForm(Action updateAllTileViewsAction, Icon = Options.GetFiddlerIcon(); + _title = Text; + _outputPathError = new ErrorProvider(components) { BlinkStyle = ErrorBlinkStyle.NeverBlink }; + _savedTimer = new Timer(components) { Interval = 2000 }; + _savedTimer.Tick += (_, _) => + { + _savedTimer.Stop(); + Text = _title; + }; + _updateAllTileViewsAction = updateAllTileViewsAction; _updateItemsTabAction = updateItemsTabAction; _updateSoundTabAction = updateSoundTabAction; @@ -90,6 +103,8 @@ public OptionsForm(Action updateAllTileViewsAction, cmdtext.Text = Options.MapCmd; argstext.Text = Options.MapArgs; textBoxOutputPath.Text = Options.OutputPath; + textBoxOutputPath.TextChanged += (_, _) => ValidateOutputPath(); + ValidateOutputPath(); foreach (ClientFileSaveFormat format in ClientFileSaveFormats.All) { @@ -214,6 +229,21 @@ private void OnClickApply(object sender, EventArgs e) Options.ExportFilenameDecimalPadded = newPad; AppSettings.Save(); } + + Text = $"{_title} - settings saved"; + _savedTimer.Stop(); + _savedTimer.Start(); + } + + /// + /// Apply keeps the old output path when the new one does not exist, so say so next to the box. + /// + private void ValidateOutputPath() + { + string path = textBoxOutputPath.Text; + bool valid = string.IsNullOrWhiteSpace(path) || Directory.Exists(path); + _outputPathError.SetError(textBoxOutputPath, + valid ? string.Empty : "This folder does not exist, so Apply keeps the current output path."); } private void OnExportFilenameFormatChanged(object sender, EventArgs e) From 9a1038fa6cc06617aefdba31900fc00264e5c184 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:48:35 +0100 Subject: [PATCH 5/5] Confirm Reload Files when there are unsaved changes Clear the unsaved flags after a reload, since every tab re-reads its data. --- UoFiddler/Forms/MainForm.cs | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/UoFiddler/Forms/MainForm.cs b/UoFiddler/Forms/MainForm.cs index 4a1d5292..5aeb9300 100644 --- a/UoFiddler/Forms/MainForm.cs +++ b/UoFiddler/Forms/MainForm.cs @@ -155,6 +155,19 @@ private void OnClickDarkMode(object sender, EventArgs e) private void ReloadFiles(object sender, EventArgs e) { + string unsavedFiles = GetUnsavedFiles(); + if (unsavedFiles.Length > 0) + { + DialogResult result = + MessageBox.Show($"Reload files and discard unsaved changes?\r\n\r\nThere are unsaved files:\r\n{unsavedFiles}", + "Unsaved Changes", MessageBoxButtons.YesNo, MessageBoxIcon.Question, + MessageBoxDefaultButton.Button2); + if (result == DialogResult.No) + { + return; + } + } + // Order matters: art has to be reloaded before tiledata for the latter to load properly. var steps = new (string Label, bool Run, Action Reload)[] { @@ -203,6 +216,13 @@ private void ReloadFiles(object sender, EventArgs e) SetStatus("Refreshing tabs..."); ControlEvents.FireFilePathChangeEvent(); + // Every tab has re-read its data from disk, so nothing is unsaved any more. + foreach (string key in Options.ChangedUltimaClass.Keys.ToList()) + { + Options.ChangedUltimaClass[key] = false; + } + UpdateUnsavedTabMarkers(); + _statusProgress.Visible = false; SetStatus("Files reloaded."); } @@ -485,12 +505,20 @@ private void OnClickManagePlugins(object sender, EventArgs e) _pluginsFormForm.Show(); } + /// + /// Lists the files with unsaved changes, one "- name" line each, or returns an empty string. + /// + private static string GetUnsavedFiles() + { + return Options.ChangedUltimaClass + .Where(key => key.Value) + .Aggregate(string.Empty, (current, key) => current + $"- {key.Key} \r\n"); + } + private void OnClosing(object sender, FormClosingEventArgs e) { _log.LogInformation("MainForm - OnClosing - start"); - string files = Options.ChangedUltimaClass - .Where(key => key.Value) - .Aggregate(string.Empty, (current, key) => current + $"- {key.Key} \r\n"); + string files = GetUnsavedFiles(); if (files.Length > 0) {