From 9b46db4891ddfc468dd2802b3e9d0cd852c98d01 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 17 Oct 2026 21:11:37 -0400 Subject: [PATCH] refactor(ui): extract MainForm startup and settings services Moves startup and settings code out of MainForm: - PostProcessorLoader discovers posts and registers their materials. Each DLL now loads in isolation; a failing one is named in a startup warning instead of throwing out of the MainForm constructor. - MachineSettings owns the clamp-settings and nest-defaults entry points and their once-per-session warnings; BomImportForm calls it directly. - NewNestFactory creates File > New nests and their daily names; GetNestName is now a pure static the characterization test calls. - MainWindowSettings restores/saves window placement and migrates the legacy nest template. - UpdaterStatus owns the updater and its status-bar item. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Data/NestDefaults.cs | 2 +- .../Forms/MainFormCharacterizationTests.cs | 10 +- OpenNest/Forms/BomImportForm.cs | 4 +- OpenNest/Forms/MachineSettings.cs | 83 +++++++++ OpenNest/Forms/MainForm.Designer.cs | 2 +- OpenNest/Forms/MainForm.File.cs | 119 +----------- OpenNest/Forms/MainForm.Nest.cs | 2 +- OpenNest/Forms/MainForm.Tools.cs | 2 +- OpenNest/Forms/MainForm.cs | 170 +++--------------- OpenNest/Forms/MainWindowSettings.cs | 81 +++++++++ OpenNest/Forms/NewNestFactory.cs | 68 +++++++ OpenNest/Forms/UpdaterStatus.cs | 63 +++++++ OpenNest/PostProcessorLoader.cs | 53 ++++++ 13 files changed, 377 insertions(+), 282 deletions(-) create mode 100644 OpenNest/Forms/MachineSettings.cs create mode 100644 OpenNest/Forms/MainWindowSettings.cs create mode 100644 OpenNest/Forms/NewNestFactory.cs create mode 100644 OpenNest/Forms/UpdaterStatus.cs create mode 100644 OpenNest/PostProcessorLoader.cs diff --git a/OpenNest.Data/NestDefaults.cs b/OpenNest.Data/NestDefaults.cs index 7fd24e07..c94c11df 100644 --- a/OpenNest.Data/NestDefaults.cs +++ b/OpenNest.Data/NestDefaults.cs @@ -49,7 +49,7 @@ public sealed class NestDefaults /// /// The built-in defaults used when no file exists and for every field /// that is missing or invalid. Matches the historical - /// MainForm.CreateDefaultNest values (units default to Inches; callers + /// File > New values (units default to Inches; callers /// may override from their own settings). /// public static NestDefaults Fallback => new(); diff --git a/OpenNest.WinForms.Tests/Forms/MainFormCharacterizationTests.cs b/OpenNest.WinForms.Tests/Forms/MainFormCharacterizationTests.cs index 58008339..7d345a1b 100644 --- a/OpenNest.WinForms.Tests/Forms/MainFormCharacterizationTests.cs +++ b/OpenNest.WinForms.Tests/Forms/MainFormCharacterizationTests.cs @@ -18,15 +18,7 @@ public class MainFormCharacterizationTests [InlineData(900, "N26-322")] [InlineData(27000, "N26-3222")] public void NestNamesUseTwoDigitYearAndPaddedBase30Sequence(int id, string expected) => - StaTestThread.Run(() => - { - using var host = new Host(); - var name = (string)typeof(MainForm) - .GetMethod("GetNestName", BindingFlags.Instance | BindingFlags.NonPublic)! - .Invoke(host, [new DateTime(2026, 10, 17), id])!; - - Assert.Equal(expected, name); - }, TimeSpan.FromMinutes(1), "The STA test did not complete."); + Assert.Equal(expected, NewNestFactory.GetNestName(new DateTime(2026, 10, 17), id)); [Fact] public void BusyGuardsDifferInWhichOperationsTheyCount() => StaTestThread.Run(() => diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index d10b9fd4..c6166378 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -32,7 +32,7 @@ namespace OpenNest.Forms dgvParts.AutoGenerateColumns = false; dgvParts.DataSource = _parts; _groupSettings = new Dictionary(); - _defaults = MainForm.LoadSavedNestDefaults(out _); + _defaults = MachineSettings.LoadSavedNestDefaults(out _); ApplyDefaults(); } @@ -414,7 +414,7 @@ namespace OpenNest.Forms var nestsCreated = 0; var importErrors = new List(); // Read when the nests are created, warning (once per session) like New does. - var clamps = MainForm.LoadClampSettings(this); + var clamps = MachineSettings.LoadClampSettings(this); foreach (var group in groups) { diff --git a/OpenNest/Forms/MachineSettings.cs b/OpenNest/Forms/MachineSettings.cs new file mode 100644 index 00000000..28a7e6f6 --- /dev/null +++ b/OpenNest/Forms/MachineSettings.cs @@ -0,0 +1,83 @@ +using System.Windows.Forms; +using OpenNest.Data; + +namespace OpenNest.Forms +{ + /// + /// The desktop entry points for the persisted machine clamp settings and nest defaults. + /// The warning variants report an unusable file once per session. + /// + internal static class MachineSettings + { + private static bool clampSettingsWarned; + private static bool defaultsWarned; + + /// + /// Loads the machine clamp settings for creating new nests (New, BOM import); an + /// unreadable or partly invalid file warns once per session, because the new nest's + /// clamps may not match the machine. + /// + public static ClampSettings LoadClampSettings(IWin32Window owner) + { + var settings = LoadSavedClampSettings(out var status); + + if ( + status is ClampSettingsStatus.Invalid or ClampSettingsStatus.Partial + && !clampSettingsWarned + ) + { + clampSettingsWarned = true; + var problem = + status == ClampSettingsStatus.Invalid + ? "could not be read, so new nests have no clamps" + : "has invalid values that were ignored, so new nests may be missing clamps"; + MessageBox.Show( + owner, + $"The clamp settings file {problem}:\n{ClampSettings.DefaultPath}\n\nCheck the clamps in Nest > Clamps and use Save as Machine Defaults to rewrite the file.", + "Clamps", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + } + return settings; + } + + /// The single desktop entry point for the persisted machine clamp settings. + public static ClampSettings LoadSavedClampSettings(out ClampSettingsStatus status) => + ClampSettings.Load(ClampSettings.DefaultPath, out status); + + /// + /// Loads the persisted nest defaults (see ); + /// a corrupt or unreadable file warns once per session. + /// + public static NestDefaults LoadNestDefaults() + { + var defaults = LoadSavedNestDefaults(out var status); + + if (status == NestDefaultsStatus.Invalid && !defaultsWarned) + { + defaultsWarned = true; + MessageBox.Show( + $"The nest defaults file could not be read:\n{NestDefaults.DefaultPath}\n\nBuilt-in defaults will be used. Re-save your defaults from Tools > Nest Defaults.", + "Nest Defaults", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + } + return defaults; + } + + /// + /// The single desktop entry point for persisted nest defaults. Units + /// fall back to the legacy DefaultUnit setting whenever the file is + /// missing or unusable or has no valid unit, so New, BOM import and + /// the defaults dialog always agree. + /// + public static NestDefaults LoadSavedNestDefaults(out NestDefaultsStatus status) => + NestDefaults.Load( + NestDefaults.DefaultPath, + Properties.Settings.Default.DefaultUnit, + out status + ); + } +} diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs index 7155706f..c3aa3fe3 100644 --- a/OpenNest/Forms/MainForm.Designer.cs +++ b/OpenNest/Forms/MainForm.Designer.cs @@ -21,7 +21,7 @@ namespace OpenNest.Forms components.Dispose(); } if (disposing) - updater?.Dispose(); + updaterStatus?.Dispose(); base.Dispose(disposing); } diff --git a/OpenNest/Forms/MainForm.File.cs b/OpenNest/Forms/MainForm.File.cs index 96a877da..3681c68d 100644 --- a/OpenNest/Forms/MainForm.File.cs +++ b/OpenNest/Forms/MainForm.File.cs @@ -6,116 +6,11 @@ using OpenNest.Data; using OpenNest.Engine.BestFit; using OpenNest.Forms.Commands; using OpenNest.IO; -using OpenNest.Properties; namespace OpenNest.Forms { public partial class MainForm { - private Nest CreateDefaultNest() - { - var nest = new Nest(); - LoadNestDefaults().ApplyTo(nest); - // After the defaults so the rail is converted to the new nest's units. - LoadClampSettings(this).ApplyToNewNest(nest); - return nest; - } - - /// - /// Loads the machine clamp settings for creating new nests (New, BOM import); an - /// unreadable or partly invalid file warns once per session, because the new nest's - /// clamps may not match the machine. - /// - internal static ClampSettings LoadClampSettings(IWin32Window owner) - { - var settings = LoadSavedClampSettings(out var status); - - if ( - status is ClampSettingsStatus.Invalid or ClampSettingsStatus.Partial - && !clampSettingsWarned - ) - { - clampSettingsWarned = true; - var problem = - status == ClampSettingsStatus.Invalid - ? "could not be read, so new nests have no clamps" - : "has invalid values that were ignored, so new nests may be missing clamps"; - MessageBox.Show( - owner, - $"The clamp settings file {problem}:\n{ClampSettings.DefaultPath}\n\nCheck the clamps in Nest > Clamps and use Save as Machine Defaults to rewrite the file.", - "Clamps", - MessageBoxButtons.OK, - MessageBoxIcon.Warning - ); - } - return settings; - } - - private static bool clampSettingsWarned; - - /// The single desktop entry point for the persisted machine clamp settings. - internal static ClampSettings LoadSavedClampSettings(out ClampSettingsStatus status) => - ClampSettings.Load(ClampSettings.DefaultPath, out status); - - /// - /// Loads the persisted nest defaults (see ); - /// a corrupt or unreadable file warns once per session. - /// - private NestDefaults LoadNestDefaults() - { - var defaults = LoadSavedNestDefaults(out var status); - - if (status == NestDefaultsStatus.Invalid && !defaultsWarned) - { - defaultsWarned = true; - MessageBox.Show( - $"The nest defaults file could not be read:\n{NestDefaults.DefaultPath}\n\nBuilt-in defaults will be used. Re-save your defaults from Tools > Nest Defaults.", - "Nest Defaults", - MessageBoxButtons.OK, - MessageBoxIcon.Warning - ); - } - return defaults; - } - - private bool defaultsWarned; - - /// - /// The single desktop entry point for persisted nest defaults. Units - /// fall back to the legacy DefaultUnit setting whenever the file is - /// missing or unusable or has no valid unit, so New, BOM import and - /// the defaults dialog always agree. - /// - internal static NestDefaults LoadSavedNestDefaults(out NestDefaultsStatus status) => - NestDefaults.Load( - NestDefaults.DefaultPath, - Properties.Settings.Default.DefaultUnit, - out status - ); - - private string GetNestName(DateTime date, int id) - { - var year = (date.Year % 100).ToString("D2"); - var seq = ToBase36(id).PadLeft(3, '2'); - - return $"N{year}-{seq}"; - } - - private static string ToBase36(int value) - { - const string chars = "2345679ACDEFGHJKLMNPQRSTUVWXYZ"; - if (value == 0) - return chars[0].ToString(); - - var result = ""; - while (value > 0) - { - result = chars[value % chars.Length] + result; - value /= chars.Length; - } - return result; - } - private void LoadNest(Nest nest, FormWindowState windowState = FormWindowState.Maximized, Guid remoteId = default, string serverUrl = null, int remoteRevision = 0) { @@ -142,19 +37,7 @@ namespace OpenNest.Forms var windowState = ActiveMdiChild != null ? ActiveMdiChild.WindowState : FormWindowState.Maximized; - var nest = CreateDefaultNest(); - - nest.DateCreated = DateTime.Now; - nest.DateLastModified = DateTime.Now; - - if (DateTime.Now.Date != Settings.Default.LastNestCreatedDate.Date) - { - Settings.Default.NestNumber = 1; - Settings.Default.LastNestCreatedDate = DateTime.Now.Date; - } - - nest.Name = GetNestName(DateTime.Now, Settings.Default.NestNumber++); - LoadNest(nest, windowState); + LoadNest(NewNestFactory.Create(this), windowState); } private async void Open_Click(object sender, EventArgs e) diff --git a/OpenNest/Forms/MainForm.Nest.cs b/OpenNest/Forms/MainForm.Nest.cs index f723d2a5..0af7145b 100644 --- a/OpenNest/Forms/MainForm.Nest.cs +++ b/OpenNest/Forms/MainForm.Nest.cs @@ -77,7 +77,7 @@ namespace OpenNest.Forms return; } - var defaults = LoadSavedClampSettings(out _); + var defaults = MachineSettings.LoadSavedClampSettings(out _); using var form = new ClampsForm(nest.PlateDefaults.ClampRail, nest.Units, defaults); if (form.ShowDialog(this) != DialogResult.OK) return; diff --git a/OpenNest/Forms/MainForm.Tools.cs b/OpenNest/Forms/MainForm.Tools.cs index 8972fc8d..7f09b75f 100644 --- a/OpenNest/Forms/MainForm.Tools.cs +++ b/OpenNest/Forms/MainForm.Tools.cs @@ -177,7 +177,7 @@ namespace OpenNest.Forms private void NestDefaults_Click(object sender, EventArgs e) { - using (var form = new NestDefaultsForm(LoadSavedNestDefaults(out _))) + using (var form = new NestDefaultsForm(MachineSettings.LoadSavedNestDefaults(out _))) { if (form.ShowDialog(this) == DialogResult.OK) form.GetDefaults().Save(NestDefaults.DefaultPath); diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index c56457f8..2cfefe12 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; -using System.Reflection; using System.Threading; using System.Windows.Forms; -using OpenNest.Data; using OpenNest.Engine.BestFit; using OpenNest.Engine.Jobs; using OpenNest.Forms.Commands; @@ -110,7 +108,7 @@ namespace OpenNest.Forms mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExport), exportNestMenu); mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExit), storageModeMenu); UpdateOverlapMenu(); - LoadSettings(); + MainWindowSettings.Load(this); var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar); menuStrip1.Renderer = renderer; @@ -124,7 +122,7 @@ namespace OpenNest.Forms this.SetBevel(false); AllowDrop = true; - LoadPosts(); + LoadPosts(PostProcessorLoader.Load(PostProcessorLoader.DefaultDirectory)); EnableCheck(); UpdateStatus(); @@ -146,84 +144,11 @@ namespace OpenNest.Forms engineComboBox.SelectedItem = EngineSelection.EngineName; engineComboBox.SelectedIndexChanged += EngineComboBox_SelectedIndexChanged; - InitializeUpdater(); + updaterStatus = new UpdaterStatus(this, statusStrip1, statusLabel1.Font); + updaterStatus.Start(); } - private global::OpenNest.UpdaterService updater; - private ToolStripStatusLabel updateStatusLabel; - - /// - /// Starts background update checks against the per-machine feed URL - /// (Options dialog; blank disables checks). Never restarts on its - /// own -- shows a clickable status item once an update is ready and - /// restarts only when the user clicks it. - /// - private void InitializeUpdater() - { - updateStatusLabel = new ToolStripStatusLabel - { - Visible = false, - ForeColor = Color.DarkGreen, - Font = new Font(statusLabel1.Font, FontStyle.Bold), - }; - updateStatusLabel.Click += (_, _) => - { - if (!updater.HasPendingUpdate) - return; - - var result = MessageBox.Show( - this, - "An OpenNest update has been downloaded. Restart now to apply it? Any unsaved nest will be lost.", - "Update ready", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question - ); - if (result == DialogResult.Yes) - updater.ApplyAndRestart(); - }; - statusStrip1.Items.Add(updateStatusLabel); - - updater = new global::OpenNest.UpdaterService(); - updater.UpdateReady += version => - { - if (IsHandleCreated) - BeginInvoke(new System.Action(() => ShowUpdateReady(version))); - }; - updater.Start(); - } - - private void ShowUpdateReady(string version) - { - updateStatusLabel.Text = $"Update {version} ready - click to restart"; - updateStatusLabel.Visible = true; - } - - - private void LoadSettings() - { - var screen = Screen.PrimaryScreen.Bounds; - - if (screen.Contains(Settings.Default.MainWindowLocation)) - Location = Settings.Default.MainWindowLocation; - - if ( - Settings.Default.MainWindowSize.Width <= screen.Width - && Settings.Default.MainWindowSize.Height <= screen.Height - ) - { - Size = Settings.Default.MainWindowSize; - } - - WindowState = Settings.Default.MainWindowState; - } - - private void SaveSettings() - { - Settings.Default.MainWindowLocation = Location; - Settings.Default.MainWindowSize = Size; - Settings.Default.MainWindowState = WindowState; - Settings.Default.Save(); - } + private UpdaterStatus updaterStatus; private static ToolStripButton CreateActionButton(string name, string text, Image image, EventHandler click) @@ -242,40 +167,27 @@ namespace OpenNest.Forms return button; } - private void LoadPosts() + private void LoadPosts(PostProcessorLoader.Result posts) { - var exepath = Assembly.GetEntryAssembly().Location; - var postprocessordir = Path.Combine(Path.GetDirectoryName(exepath), "Posts"); - - if (Directory.Exists(postprocessordir)) + foreach (var postProcessor in posts.PostProcessors) { - foreach (var file in Directory.GetFiles(postprocessordir, "*.dll")) - { - var types = Assembly.LoadFile(file).GetTypes(); + var postProcessorMenuItem = new ToolStripMenuItem(postProcessor.Name); + postProcessorMenuItem.Tag = postProcessor; + postProcessorMenuItem.Click += PostProcessor_Click; + mnuNestPost.DropDownItems.Add(postProcessorMenuItem); - foreach (var type in types) - { - if (type.GetInterfaces().Contains(typeof(IPostProcessor)) == false) - continue; - - var postProcessor = Activator.CreateInstance(type) as IPostProcessor; - var postProcessorMenuItem = new ToolStripMenuItem(postProcessor.Name); - postProcessorMenuItem.Tag = postProcessor; - postProcessorMenuItem.Click += PostProcessor_Click; - mnuNestPost.DropDownItems.Add(postProcessorMenuItem); - - var toolbarPost = new ToolStripMenuItem(postProcessor.Name) { Tag = postProcessor }; - toolbarPost.Click += PostProcessor_Click; - postButton.DropDownItems.Add(toolbarPost); - - if (postProcessor is IMaterialProvidingPostProcessor materialProvider) - PostProcessorMaterials.AddFrom(materialProvider); - } - } + var toolbarPost = new ToolStripMenuItem(postProcessor.Name) { Tag = postProcessor }; + toolbarPost.Click += PostProcessor_Click; + postButton.DropDownItems.Add(toolbarPost); } mnuNestPost.Visible = mnuNestPost.DropDownItems.Count > 0; postButton.Visible = postButton.DropDownItems.Count > 0; + + if (posts.Failures.Count > 0) + Shown += (_, _) => MessageBox.Show(this, + "These post processors could not be loaded:\n\n" + string.Join("\n", posts.Failures), + "Post Processors", MessageBoxButtons.OK, MessageBoxIcon.Warning); } #region Overrides @@ -344,52 +256,12 @@ namespace OpenNest.Forms { base.OnLoad(e); - MigrateNestTemplate(); + MainWindowSettings.MigrateNestTemplate(); if (Settings.Default.CreateNewNestOnOpen) New_Click(this, new EventArgs()); } - /// - /// One-time upgrade: converts a legacy .nstdot nest template - /// (NestTemplatePath setting) into defaults.json, then clears the - /// setting so the template mechanism is never consulted again. - /// - private void MigrateNestTemplate() - { - var templatePath = Settings.Default.NestTemplatePath; - if (string.IsNullOrWhiteSpace(templatePath)) - return; - - // A failed conversion keeps the setting populated so the user - // can still find their template file. - var converted = true; - if (File.Exists(templatePath) && !File.Exists(NestDefaults.DefaultPath)) - { - try - { - var nest = new NestReader(templatePath) { BestFitStore = BestFitCacheStore.Shared }.Read(); - NestDefaults.FromNest(nest).Save(NestDefaults.DefaultPath); - } - catch (Exception ex) - { - converted = false; - MessageBox.Show( - $"The nest template could not be converted to the new defaults file:\n{templatePath}\n\n{ex.Message}\n\nIt will no longer be loaded automatically. Set defaults under Tools > Nest Defaults.", - "Nest Template", - MessageBoxButtons.OK, - MessageBoxIcon.Warning - ); - } - } - - if (!converted) - return; - - Settings.Default.NestTemplatePath = ""; - Settings.Default.Save(); - } - protected override void OnDragEnter(DragEventArgs e) { base.OnDragEnter(e); @@ -430,7 +302,7 @@ namespace OpenNest.Forms protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { base.OnClosing(e); - SaveSettings(); + MainWindowSettings.Save(this); } #endregion diff --git a/OpenNest/Forms/MainWindowSettings.cs b/OpenNest/Forms/MainWindowSettings.cs new file mode 100644 index 00000000..10efcd1f --- /dev/null +++ b/OpenNest/Forms/MainWindowSettings.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Windows.Forms; +using OpenNest.Data; +using OpenNest.Engine.BestFit; +using OpenNest.IO; +using OpenNest.Properties; + +namespace OpenNest.Forms +{ + /// The main window's persisted placement and its one-time settings upgrades. + internal static class MainWindowSettings + { + /// Restores the saved placement when it still fits the primary screen. + public static void Load(Form form) + { + var screen = Screen.PrimaryScreen.Bounds; + + if (screen.Contains(Settings.Default.MainWindowLocation)) + form.Location = Settings.Default.MainWindowLocation; + + if ( + Settings.Default.MainWindowSize.Width <= screen.Width + && Settings.Default.MainWindowSize.Height <= screen.Height + ) + { + form.Size = Settings.Default.MainWindowSize; + } + + form.WindowState = Settings.Default.MainWindowState; + } + + public static void Save(Form form) + { + Settings.Default.MainWindowLocation = form.Location; + Settings.Default.MainWindowSize = form.Size; + Settings.Default.MainWindowState = form.WindowState; + Settings.Default.Save(); + } + + /// + /// One-time upgrade: converts a legacy .nstdot nest template + /// (NestTemplatePath setting) into defaults.json, then clears the + /// setting so the template mechanism is never consulted again. + /// + public static void MigrateNestTemplate() + { + var templatePath = Settings.Default.NestTemplatePath; + if (string.IsNullOrWhiteSpace(templatePath)) + return; + + // A failed conversion keeps the setting populated so the user + // can still find their template file. + var converted = true; + if (File.Exists(templatePath) && !File.Exists(NestDefaults.DefaultPath)) + { + try + { + var nest = new NestReader(templatePath) { BestFitStore = BestFitCacheStore.Shared }.Read(); + NestDefaults.FromNest(nest).Save(NestDefaults.DefaultPath); + } + catch (Exception ex) + { + converted = false; + MessageBox.Show( + $"The nest template could not be converted to the new defaults file:\n{templatePath}\n\n{ex.Message}\n\nIt will no longer be loaded automatically. Set defaults under Tools > Nest Defaults.", + "Nest Template", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + } + } + + if (!converted) + return; + + Settings.Default.NestTemplatePath = ""; + Settings.Default.Save(); + } + } +} diff --git a/OpenNest/Forms/NewNestFactory.cs b/OpenNest/Forms/NewNestFactory.cs new file mode 100644 index 00000000..e42e08f4 --- /dev/null +++ b/OpenNest/Forms/NewNestFactory.cs @@ -0,0 +1,68 @@ +using System; +using System.Windows.Forms; +using OpenNest.Properties; + +namespace OpenNest.Forms +{ + /// + /// Creates File > New nests: machine defaults and clamps, then the next name in the + /// day's sequence ( restarts at 1 each day). + /// + internal static class NewNestFactory + { + private const string Base30Digits = "2345679ACDEFGHJKLMNPQRSTUVWXYZ"; + + public static Nest Create(IWin32Window owner) + { + var nest = CreateDefaultNest(owner); + var now = DateTime.Now; + + nest.DateCreated = now; + nest.DateLastModified = now; + + if (now.Date != Settings.Default.LastNestCreatedDate.Date) + { + Settings.Default.NestNumber = 1; + Settings.Default.LastNestCreatedDate = now.Date; + } + + nest.Name = GetNestName(now, Settings.Default.NestNumber++); + return nest; + } + + private static Nest CreateDefaultNest(IWin32Window owner) + { + var nest = new Nest(); + MachineSettings.LoadNestDefaults().ApplyTo(nest); + // After the defaults so the rail is converted to the new nest's units. + MachineSettings.LoadClampSettings(owner).ApplyToNewNest(nest); + return nest; + } + + /// + /// "N" + two-digit year + the sequence in an unambiguous base-30 alphabet, padded to + /// three digits with its zero digit ('2'). + /// + public static string GetNestName(DateTime date, int id) + { + var year = (date.Year % 100).ToString("D2"); + var seq = ToBase30(id).PadLeft(3, Base30Digits[0]); + + return $"N{year}-{seq}"; + } + + private static string ToBase30(int value) + { + if (value == 0) + return Base30Digits[0].ToString(); + + var result = ""; + while (value > 0) + { + result = Base30Digits[value % Base30Digits.Length] + result; + value /= Base30Digits.Length; + } + return result; + } + } +} diff --git a/OpenNest/Forms/UpdaterStatus.cs b/OpenNest/Forms/UpdaterStatus.cs new file mode 100644 index 00000000..c639d82d --- /dev/null +++ b/OpenNest/Forms/UpdaterStatus.cs @@ -0,0 +1,63 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace OpenNest.Forms +{ + /// + /// Background update checks against the per-machine feed URL (Options dialog; blank + /// disables checks) and the status-bar item that announces a downloaded update. Never + /// restarts on its own: it restarts only when the user clicks the item and confirms. + /// + internal sealed class UpdaterStatus : IDisposable + { + private readonly Form owner; + private readonly UpdaterService updater = new(); + private readonly ToolStripStatusLabel label; + + public UpdaterStatus(Form owner, StatusStrip statusStrip, Font baseFont) + { + this.owner = owner; + label = new ToolStripStatusLabel + { + Visible = false, + ForeColor = Color.DarkGreen, + Font = new Font(baseFont, FontStyle.Bold), + }; + label.Click += Label_Click; + statusStrip.Items.Add(label); + + updater.UpdateReady += version => + { + if (owner.IsHandleCreated) + owner.BeginInvoke(new Action(() => ShowUpdateReady(version))); + }; + } + + public void Start() => updater.Start(); + + public void Dispose() => updater.Dispose(); + + private void Label_Click(object sender, EventArgs e) + { + if (!updater.HasPendingUpdate) + return; + + var result = MessageBox.Show( + owner, + "An OpenNest update has been downloaded. Restart now to apply it? Any unsaved nest will be lost.", + "Update ready", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question + ); + if (result == DialogResult.Yes) + updater.ApplyAndRestart(); + } + + private void ShowUpdateReady(string version) + { + label.Text = $"Update {version} ready - click to restart"; + label.Visible = true; + } + } +} diff --git a/OpenNest/PostProcessorLoader.cs b/OpenNest/PostProcessorLoader.cs new file mode 100644 index 00000000..dc9a0144 --- /dev/null +++ b/OpenNest/PostProcessorLoader.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace OpenNest +{ + /// + /// Discovers post processors in the desktop's Posts/ directory and registers the + /// materials they provide. Each DLL loads in isolation: one that fails is reported in + /// instead of stopping the others or startup. + /// + public static class PostProcessorLoader + { + public sealed record Result(IReadOnlyList PostProcessors, IReadOnlyList Failures); + + public static string DefaultDirectory => + Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Posts"); + + public static Result Load(string directory) + { + var postProcessors = new List(); + var failures = new List(); + if (!Directory.Exists(directory)) + return new Result(postProcessors, failures); + + foreach (var file in Directory.GetFiles(directory, "*.dll")) + { + try + { + // Instantiate the whole DLL before registering any of it. + var loaded = Assembly.LoadFile(file).GetTypes() + .Where(type => type.GetInterfaces().Contains(typeof(IPostProcessor))) + .Select(type => (IPostProcessor)Activator.CreateInstance(type)) + .ToList(); + foreach (var postProcessor in loaded) + { + postProcessors.Add(postProcessor); + if (postProcessor is IMaterialProvidingPostProcessor materialProvider) + PostProcessorMaterials.AddFrom(materialProvider); + } + } + catch (Exception ex) + { + var reason = ex is TargetInvocationException { InnerException: { } inner } ? inner : ex; + failures.Add($"{Path.GetFileName(file)}: {reason.Message}"); + } + } + return new Result(postProcessors, failures); + } + } +}