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)
{