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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<PartInputItem> parts;
|
||||
private BindingList<BinInputItem> 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<PartInputItem>(), new List<BinInputItem>());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -88,5 +88,11 @@ namespace CutList.Presenters
|
||||
/// Clears all data in the view.
|
||||
/// </summary>
|
||||
void ClearData();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the window title to reflect the current document state.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file name to display, or null for a new document</param>
|
||||
void UpdateWindowTitle(string? fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the "Save As" operation. Always prompts for a file location.
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,6 +143,7 @@ namespace CutList.Presenters
|
||||
{
|
||||
_currentDocument = new Document();
|
||||
_view.ClearData();
|
||||
_view.UpdateWindowTitle(null);
|
||||
UpdateRunButtonState();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace CutList.Services
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var document = JsonConvert.DeserializeObject<Document>(json);
|
||||
document.LastFilePath = filePath;
|
||||
return Result<Document>.Success(document);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user