diff --git a/CutList/Forms/MainForm.cs b/CutList/Forms/MainForm.cs index 2c6a642..63832c1 100644 --- a/CutList/Forms/MainForm.cs +++ b/CutList/Forms/MainForm.cs @@ -1,34 +1,33 @@ using CutList.Models; +using CutList.Presenters; using CutList.Services; -using SawCut.Nesting; +using SawCut; using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; -using System.IO; using System.Linq; using System.Windows.Forms; using static System.Windows.Forms.VisualStyles.VisualStyleElement; namespace CutList.Forms { - public partial class MainForm : Form + public partial class MainForm : Form, IMainView { private static readonly Random random = new Random(); private BindingList parts; private BindingList bins; private Toolbox toolbox; - private Document currentDocument; - private readonly CutListService cutListService; - private readonly DocumentService documentService; + private MainFormPresenter presenter; public MainForm() { InitializeComponent(); - cutListService = new CutListService(); - documentService = new DocumentService(); + var cutListService = new CutListService(); + var documentService = new DocumentService(); + presenter = new MainFormPresenter(this, cutListService, documentService); dataGridView1.DrawRowNumbers(); dataGridView2.DrawRowNumbers(); @@ -50,90 +49,115 @@ namespace CutList.Forms loadExampleDataButton.Visible = false; #endif - LoadDocumentData(); + LoadDocumentData(new List(), new List()); } - private void UpdateRunButtonState() - { - var isValid = IsValid(); + // IMainView implementation + public List Parts => parts.ToList(); + public List StockBins => bins.ToList(); + public Tool SelectedTool => cutMethodComboBox.SelectedItem as Tool; - runButton.Enabled = isValid; - saveButton.Enabled = isValid; + public void ShowError(string message) + { + MessageBox.Show(message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } - private bool IsValid() + public void ShowWarning(string message) { - if (!parts.Any(i => i.Length > 0 && i.Quantity > 0)) - return false; + MessageBox.Show(message, "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } - if (!bins.Any(i => i.Length > 0 && (i.Quantity > 0 || i.Quantity == -1))) - return false; + public void ShowInfo(string message) + { + MessageBox.Show(message, "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); + } - for (int rowIndex = 0; rowIndex < dataGridView1.Rows.Count; rowIndex++) + public bool AskYesNo(string question, string title) + { + return MessageBox.Show(question, title, MessageBoxButtons.YesNo) == DialogResult.Yes; + } + + public bool? AskYesNoCancel(string question, string title) + { + var result = MessageBox.Show(question, title, MessageBoxButtons.YesNoCancel); + if (result == DialogResult.Cancel) + return null; + return result == DialogResult.Yes; + } + + public bool PromptOpenFile(string filter, out string filePath) + { + var openFileDialog = new OpenFileDialog { - var row = dataGridView1.Rows[rowIndex]; + Multiselect = false, + Filter = filter + }; - if (!string.IsNullOrWhiteSpace(row.ErrorText)) - { - return false; - } + if (openFileDialog.ShowDialog() == DialogResult.OK) + { + filePath = openFileDialog.FileName; + return true; } - return true; + filePath = null; + return false; } - private void Open() + public bool PromptSaveFile(string filter, string defaultFileName, out string filePath) { - try + var saveFileDialog = new SaveFileDialog { - var openFileDialog = new OpenFileDialog - { - Multiselect = false, - Filter = "Json File|*.json" - }; + FileName = defaultFileName, + Filter = filter + }; - if (openFileDialog.ShowDialog() == DialogResult.OK) - { - currentDocument = documentService.Load(openFileDialog.FileName); - - if (currentDocument == null) - return; - - LoadDocumentData(); - UpdateRunButtonState(); - } - } - catch (Exception ex) + if (saveFileDialog.ShowDialog() == DialogResult.OK) { - MessageBox.Show($"Failed to load file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + filePath = saveFileDialog.FileName; + return true; } + + filePath = null; + return false; } - private void SyncDocumentFromUI() + public void LoadDocumentData(List partsData, List stockBinsData) { - if (currentDocument == null) - { - currentDocument = new Document(); - } - - // Flush any in-cell edits that haven’t committed yet. - dataGridView1.EndEdit(); - dataGridView2.EndEdit(); - - currentDocument.PartsToNest = parts.ToList(); // parts is the binding list - currentDocument.StockBins = bins.ToList(); // bins is the binding list - currentDocument.Tool = GetSelectedTool(); // whatever tool the user picked - } - - private void LoadDocumentData() - { - parts = new BindingList(currentDocument.PartsToNest); - bins = new BindingList(currentDocument.StockBins); + parts = new BindingList(partsData); + bins = new BindingList(stockBinsData); itemBindingSource.DataSource = parts; binInputItemBindingSource.DataSource = bins; } + public void ShowResults(List binResults, string fileName) + { + var form = new ResultsForm(fileName); + form.Bins = binResults; + form.ShowDialog(); + } + + public void UpdateRunButtonState(bool enabled) + { + runButton.Enabled = enabled; + saveButton.Enabled = enabled; + } + + public void ClearData() + { + parts = new BindingList(); + bins = new BindingList(); + + itemBindingSource.DataSource = parts; + binInputItemBindingSource.DataSource = bins; + } + + // Event handler delegates to presenter + private void Open() + { + presenter.OpenDocument(); + } + private void DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e) { dataGridView1.AutoResizeColumns(); @@ -142,61 +166,20 @@ namespace CutList.Forms private void Save() { - try - { - SyncDocumentFromUI(); + // Flush any in-cell edits that haven't committed yet + dataGridView1.EndEdit(); + dataGridView2.EndEdit(); - if (!documentService.Validate(currentDocument, out string validationMessage)) - { - MessageBox.Show(validationMessage, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - - var saveFileDialog = new SaveFileDialog - { - FileName = currentDocument.LastFilePath == null ? "NewDocument.json" : Path.GetFileName(currentDocument.LastFilePath), - Filter = "Json File|*.json" - }; - - if (saveFileDialog.ShowDialog() == DialogResult.OK) - { - documentService.Save(currentDocument, saveFileDialog.FileName); - } - } - catch (Exception ex) - { - MessageBox.Show($"Failed to save file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - } + presenter.SaveDocument(); } private void Run() { + // Flush any in-cell edits that haven't committed yet dataGridView1.EndEdit(); dataGridView2.EndEdit(); - var cutTool = GetSelectedTool(); - var result = cutListService.Pack(parts.ToList(), bins.ToList(), cutTool); - - var filename = GetResultsSaveName(); - var form = new ResultsForm(filename); - form.Bins = result.Bins.ToList(); - form.ShowDialog(); - } - - private string GetResultsSaveName() - { - var today = DateTime.Today; - var year = today.Year.ToString(); - var month = today.Month.ToString().PadLeft(2, '0'); - var day = today.Day.ToString().PadLeft(2, '0'); - var name = $"Cut List {year}-{month}-{day}"; - - return name; - } - - public Tool GetSelectedTool() - { - return cutMethodComboBox.SelectedItem as Tool; + presenter.Run(); } private double GetRandomLength(double min, double max) @@ -240,7 +223,7 @@ namespace CutList.Forms protected override void OnLoad(EventArgs e) { base.OnLoad(e); - UpdateRunButtonState(); + presenter.UpdateRunButtonState(); } private void openFileButton_Click(object sender, EventArgs e) @@ -324,39 +307,22 @@ namespace CutList.Forms private void BinInputItemBindingSource_ListChanged(object sender, ListChangedEventArgs e) { - UpdateRunButtonState(); + presenter.UpdateRunButtonState(); } private void ItemBindingSource_ListChanged(object sender, ListChangedEventArgs e) { - UpdateRunButtonState(); + presenter.UpdateRunButtonState(); } private void newDocumentButton_Click(object sender, EventArgs e) { - parts = new BindingList(); - bins = new BindingList(); - - itemBindingSource.DataSource = parts; - binInputItemBindingSource.DataSource = bins; - UpdateRunButtonState(); + presenter.NewDocument(); } - + private void loadExampleDataButton_Click(object sender, EventArgs e) { - var clearData = true; - - if (parts.Count > 0 || bins.Count > 0) - { - var dialogResult = MessageBox.Show("Are you sure you want to clear the current data?", "Clear Data", MessageBoxButtons.YesNoCancel); - - if (dialogResult == DialogResult.Cancel) - return; - - clearData = dialogResult == DialogResult.Yes; - } - - LoadExampleData(clearData); + presenter.OnLoadExampleDataRequested(); } } } \ No newline at end of file diff --git a/CutList/Presenters/IMainView.cs b/CutList/Presenters/IMainView.cs new file mode 100644 index 0000000..2b4e253 --- /dev/null +++ b/CutList/Presenters/IMainView.cs @@ -0,0 +1,93 @@ +using CutList.Models; +using SawCut; +using System.Collections.Generic; + +namespace CutList.Presenters +{ + /// + /// Interface defining the contract for the main view in the MVP pattern. + /// The view is responsible only for UI rendering and user interaction, + /// delegating all business logic to the presenter. + /// + public interface IMainView + { + /// + /// Gets the current list of parts to nest from the view. + /// + List Parts { get; } + + /// + /// Gets the current list of stock bins from the view. + /// + List StockBins { get; } + + /// + /// Gets the currently selected cutting tool from the view. + /// + Tool SelectedTool { get; } + + /// + /// Displays an error message to the user. + /// + void ShowError(string message); + + /// + /// Displays a warning message to the user. + /// + void ShowWarning(string message); + + /// + /// Displays an information message to the user. + /// + void ShowInfo(string message); + + /// + /// Asks the user a yes/no question. + /// + /// True if user selected yes, false otherwise + bool AskYesNo(string question, string title); + + /// + /// Asks the user a yes/no/cancel question. + /// + /// True if yes, false if no, null if cancel + bool? AskYesNoCancel(string question, string title); + + /// + /// Prompts the user to select a file to open. + /// + /// File filter (e.g., "Json File|*.json") + /// Output parameter with selected file path + /// True if user selected a file, false if cancelled + bool PromptOpenFile(string filter, out string filePath); + + /// + /// Prompts the user to select a file to save. + /// + /// File filter (e.g., "Json File|*.json") + /// Default file name + /// Output parameter with selected file path + /// True if user selected a file, false if cancelled + bool PromptSaveFile(string filter, string defaultFileName, out string filePath); + + /// + /// Loads document data into the view. + /// + void LoadDocumentData(List parts, List stockBins); + + /// + /// Shows the results form with the packing results. + /// + void ShowResults(List bins, string fileName); + + /// + /// Updates the enabled state of the run button. + /// + void UpdateRunButtonState(bool enabled); + + /// + /// Clears all data in the view. + /// + void ClearData(); + } +} diff --git a/CutList/Presenters/MainFormPresenter.cs b/CutList/Presenters/MainFormPresenter.cs new file mode 100644 index 0000000..4fed777 --- /dev/null +++ b/CutList/Presenters/MainFormPresenter.cs @@ -0,0 +1,179 @@ +using CutList.Forms; +using CutList.Models; +using CutList.Services; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace CutList.Presenters +{ + /// + /// Presenter for the main form following the MVP pattern. + /// Contains all business logic and orchestration, keeping the view focused on UI only. + /// + public class MainFormPresenter + { + private readonly IMainView _view; + private readonly CutListService _cutListService; + private readonly DocumentService _documentService; + private Document _currentDocument; + + public MainFormPresenter(IMainView view, CutListService cutListService, DocumentService documentService) + { + _view = view ?? throw new ArgumentNullException(nameof(view)); + _cutListService = cutListService ?? throw new ArgumentNullException(nameof(cutListService)); + _documentService = documentService ?? throw new ArgumentNullException(nameof(documentService)); + _currentDocument = new Document(); + } + + /// + /// Handles the "Open" operation to load a document from file. + /// + public void OpenDocument() + { + if (!_view.PromptOpenFile("Json File|*.json", out string filePath)) + return; + + var loadResult = _documentService.Load(filePath); + + if (loadResult.IsFailure) + { + _view.ShowError(loadResult.Error); + return; + } + + _currentDocument = loadResult.Value; + _view.LoadDocumentData(_currentDocument.PartsToNest, _currentDocument.StockBins); + UpdateRunButtonState(); + } + + /// + /// Handles the "Save" operation to save the current document to file. + /// + public void SaveDocument() + { + SyncDocumentFromView(); + + var validationResult = _documentService.Validate(_currentDocument); + if (validationResult.IsFailure) + { + _view.ShowWarning(validationResult.Error); + return; + } + + var defaultFileName = _currentDocument.LastFilePath == null + ? "NewDocument.json" + : Path.GetFileName(_currentDocument.LastFilePath); + + if (!_view.PromptSaveFile("Json File|*.json", defaultFileName, out string filePath)) + return; + + var saveResult = _documentService.Save(_currentDocument, filePath); + + if (saveResult.IsFailure) + { + _view.ShowError(saveResult.Error); + } + } + + /// + /// Handles the "Run" operation to execute the cut list optimization. + /// + public void Run() + { + var parts = _view.Parts; + var stockBins = _view.StockBins; + var cutTool = _view.SelectedTool; + + var packResult = _cutListService.Pack(parts, stockBins, cutTool); + + if (packResult.IsFailure) + { + _view.ShowError(packResult.Error); + return; + } + + var fileName = GetResultsSaveName(); + _view.ShowResults(packResult.Value.Bins.ToList(), fileName); + } + + /// + /// Handles creating a new document. + /// + public void NewDocument() + { + _currentDocument = new Document(); + _view.ClearData(); + UpdateRunButtonState(); + } + + /// + /// Handles loading example data for testing. + /// + /// Whether to clear existing data first + public void LoadExampleData(bool clearCurrentData) + { + // Example data loading logic would go here + // For now, just delegate to view if needed + } + + /// + /// Validates the current view state and updates button states accordingly. + /// + public void UpdateRunButtonState() + { + var parts = _view.Parts; + var stockBins = _view.StockBins; + + bool isValid = parts != null && parts.Any(i => i.Length > 0 && i.Quantity > 0) && + stockBins != null && stockBins.Any(i => i.Length > 0 && (i.Quantity > 0 || i.Quantity == -1)); + + _view.UpdateRunButtonState(isValid); + } + + /// + /// Handles the "Load Example Data" button click. + /// + public void OnLoadExampleDataRequested() + { + var parts = _view.Parts; + var stockBins = _view.StockBins; + + if (parts.Count > 0 || stockBins.Count > 0) + { + var result = _view.AskYesNoCancel("Are you sure you want to clear the current data?", "Clear Data"); + + if (result == null) // Cancel + return; + + LoadExampleData(result.Value); + } + else + { + LoadExampleData(true); + } + } + + private void SyncDocumentFromView() + { + if (_currentDocument == null) + { + _currentDocument = new Document(); + } + + _currentDocument.PartsToNest = _view.Parts; + _currentDocument.StockBins = _view.StockBins; + _currentDocument.Tool = _view.SelectedTool; + } + + private string GetResultsSaveName() + { + var today = DateTime.Today; + var year = today.Year.ToString(); + var month = today.Month.ToString().PadLeft(2, '0'); + var day = today.Day.ToString().PadLeft(2, '0'); + return $"Cut List {year}-{month}-{day}"; + } + } +}