From 6db8ab21f44f3739c7cdb00daaa09533ef654b19 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 1 Feb 2026 16:20:59 -0500 Subject: [PATCH] feat: Improve document management with Save/Save As and keyboard shortcuts - Track file path after save/load so Save doesn't prompt again - Add Save As (Ctrl+Shift+S) to always prompt for location - Update window title to show current filename - Generate incremental default filenames (CutList_1.json, etc.) - Add keyboard shortcuts: Ctrl+S (Save), Ctrl+O (Open), Ctrl+N (New) - Enter key in items grid moves to Length column on next row Co-Authored-By: Claude Opus 4.5 --- CutList/Forms/MainForm.cs | 68 +++++++++++++++++++++++++ CutList/Presenters/IMainView.cs | 6 +++ CutList/Presenters/MainFormPresenter.cs | 50 ++++++++++++++++-- CutList/Services/DocumentService.cs | 1 + 4 files changed, 122 insertions(+), 3 deletions(-) diff --git a/CutList/Forms/MainForm.cs b/CutList/Forms/MainForm.cs index 95824b2..5ed3ee4 100644 --- a/CutList/Forms/MainForm.cs +++ b/CutList/Forms/MainForm.cs @@ -9,6 +9,7 @@ namespace CutList.Forms public partial class MainForm : Form, IMainView { private static readonly Random random = new Random(); + private const string BaseTitle = "Cut List"; private BindingList parts; private BindingList bins; @@ -32,9 +33,13 @@ namespace CutList.Forms binInputItemBindingSource.DataSource = bins; binInputItemBindingSource.ListChanged += BinInputItemBindingSource_ListChanged; + toolbox = new Toolbox(); cutMethodComboBox.DataSource = toolbox.Tools; + // Enable keyboard shortcuts + KeyPreview = true; + #if DEBUG loadExampleDataButton.Visible = true; #else @@ -140,6 +145,13 @@ namespace CutList.Forms LoadDocumentData(new List(), new List()); } + public void UpdateWindowTitle(string? fileName) + { + Text = string.IsNullOrEmpty(fileName) + ? BaseTitle + : $"{fileName} - {BaseTitle}"; + } + // Event handler delegates to presenter private void Open() { @@ -164,6 +176,61 @@ namespace CutList.Forms presenter.SaveDocument(); } + private void SaveAs() + { + FlushPendingEdits(); + presenter.SaveDocumentAs(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Control && e.Shift && e.KeyCode == Keys.S) + { + SaveAs(); + e.Handled = true; + } + else if (e.Control && e.KeyCode == Keys.S) + { + Save(); + e.Handled = true; + } + else if (e.Control && e.KeyCode == Keys.O) + { + Open(); + e.Handled = true; + } + else if (e.Control && e.KeyCode == Keys.N) + { + presenter.NewDocument(); + e.Handled = true; + } + } + + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + // Handle Enter key in items DataGridView to move to Length column + if (keyData == Keys.Enter && dataGridView1.CurrentCell != null && + (dataGridView1.ContainsFocus || dataGridView1.IsCurrentCellInEditMode)) + { + dataGridView1.EndEdit(); + + int currentRow = dataGridView1.CurrentCell.RowIndex; + int nextRow = currentRow + 1; + int lengthColumnIndex = lengthDataGridViewTextBoxColumn.Index; + + if (nextRow < dataGridView1.RowCount) + { + dataGridView1.CurrentCell = dataGridView1[lengthColumnIndex, nextRow]; + dataGridView1.BeginEdit(true); + return true; // Handled + } + } + + return base.ProcessCmdKey(ref msg, keyData); + } + private void Run() { FlushPendingEdits(); @@ -281,6 +348,7 @@ namespace CutList.Forms dataGridView1.Refresh(); } + private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e) { dataGridView1.Rows[e.RowIndex].ErrorText = e.Exception.InnerException?.Message; diff --git a/CutList/Presenters/IMainView.cs b/CutList/Presenters/IMainView.cs index 93a1882..c9ce957 100644 --- a/CutList/Presenters/IMainView.cs +++ b/CutList/Presenters/IMainView.cs @@ -88,5 +88,11 @@ namespace CutList.Presenters /// Clears all data in the view. /// void ClearData(); + + /// + /// Updates the window title to reflect the current document state. + /// + /// The file name to display, or null for a new document + void UpdateWindowTitle(string? fileName); } } diff --git a/CutList/Presenters/MainFormPresenter.cs b/CutList/Presenters/MainFormPresenter.cs index 8cf6982..1313a28 100644 --- a/CutList/Presenters/MainFormPresenter.cs +++ b/CutList/Presenters/MainFormPresenter.cs @@ -13,6 +13,7 @@ namespace CutList.Presenters private readonly CutListService _cutListService; private readonly DocumentService _documentService; private Document _currentDocument; + private int _documentCounter = 0; public MainFormPresenter(IMainView view, CutListService cutListService, DocumentService documentService) { @@ -40,11 +41,13 @@ namespace CutList.Presenters _currentDocument = loadResult.Value; _view.LoadDocumentData(_currentDocument.PartsToNest, _currentDocument.StockBins); + _view.UpdateWindowTitle(Path.GetFileName(filePath)); UpdateRunButtonState(); } /// - /// Handles the "Save" operation to save the current document to file. + /// Handles the "Save" operation. If the document has a known path, saves directly. + /// Otherwise, prompts for a file location (same as Save As). /// public void SaveDocument() { @@ -57,19 +60,59 @@ namespace CutList.Presenters return; } - var defaultFileName = _currentDocument.LastFilePath == null - ? "NewDocument.json" + // If we have a known path, save directly without prompting + if (!string.IsNullOrEmpty(_currentDocument.LastFilePath)) + { + SaveToPath(_currentDocument.LastFilePath); + return; + } + + // No known path - prompt for location (same as Save As) + SaveDocumentAs(); + } + + /// + /// Handles the "Save As" operation. Always prompts for a file location. + /// + public void SaveDocumentAs() + { + SyncDocumentFromView(); + + var validationResult = _documentService.Validate(_currentDocument); + if (validationResult.IsFailure) + { + _view.ShowWarning(validationResult.Error); + return; + } + + var defaultFileName = string.IsNullOrEmpty(_currentDocument.LastFilePath) + ? GenerateDefaultFileName() : Path.GetFileName(_currentDocument.LastFilePath); if (!_view.PromptSaveFile("Json File|*.json", defaultFileName, out string filePath)) return; + SaveToPath(filePath); + } + + private void SaveToPath(string filePath) + { var saveResult = _documentService.Save(_currentDocument, filePath); if (saveResult.IsFailure) { _view.ShowError(saveResult.Error); + return; } + + _currentDocument.LastFilePath = filePath; + _view.UpdateWindowTitle(Path.GetFileName(filePath)); + } + + private string GenerateDefaultFileName() + { + _documentCounter++; + return $"CutList_{_documentCounter}.json"; } /// @@ -100,6 +143,7 @@ namespace CutList.Presenters { _currentDocument = new Document(); _view.ClearData(); + _view.UpdateWindowTitle(null); UpdateRunButtonState(); } diff --git a/CutList/Services/DocumentService.cs b/CutList/Services/DocumentService.cs index de3df66..f35983e 100644 --- a/CutList/Services/DocumentService.cs +++ b/CutList/Services/DocumentService.cs @@ -41,6 +41,7 @@ namespace CutList.Services { var json = File.ReadAllText(filePath); var document = JsonConvert.DeserializeObject(json); + document.LastFilePath = filePath; return Result.Success(document); } catch (Exception ex)