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
|
public partial class MainForm : Form, IMainView
|
||||||
{
|
{
|
||||||
private static readonly Random random = new Random();
|
private static readonly Random random = new Random();
|
||||||
|
private const string BaseTitle = "Cut List";
|
||||||
|
|
||||||
private BindingList<PartInputItem> parts;
|
private BindingList<PartInputItem> parts;
|
||||||
private BindingList<BinInputItem> bins;
|
private BindingList<BinInputItem> bins;
|
||||||
@@ -32,9 +33,13 @@ namespace CutList.Forms
|
|||||||
binInputItemBindingSource.DataSource = bins;
|
binInputItemBindingSource.DataSource = bins;
|
||||||
binInputItemBindingSource.ListChanged += BinInputItemBindingSource_ListChanged;
|
binInputItemBindingSource.ListChanged += BinInputItemBindingSource_ListChanged;
|
||||||
|
|
||||||
|
|
||||||
toolbox = new Toolbox();
|
toolbox = new Toolbox();
|
||||||
cutMethodComboBox.DataSource = toolbox.Tools;
|
cutMethodComboBox.DataSource = toolbox.Tools;
|
||||||
|
|
||||||
|
// Enable keyboard shortcuts
|
||||||
|
KeyPreview = true;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
loadExampleDataButton.Visible = true;
|
loadExampleDataButton.Visible = true;
|
||||||
#else
|
#else
|
||||||
@@ -140,6 +145,13 @@ namespace CutList.Forms
|
|||||||
LoadDocumentData(new List<PartInputItem>(), new List<BinInputItem>());
|
LoadDocumentData(new List<PartInputItem>(), new List<BinInputItem>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateWindowTitle(string? fileName)
|
||||||
|
{
|
||||||
|
Text = string.IsNullOrEmpty(fileName)
|
||||||
|
? BaseTitle
|
||||||
|
: $"{fileName} - {BaseTitle}";
|
||||||
|
}
|
||||||
|
|
||||||
// Event handler delegates to presenter
|
// Event handler delegates to presenter
|
||||||
private void Open()
|
private void Open()
|
||||||
{
|
{
|
||||||
@@ -164,6 +176,61 @@ namespace CutList.Forms
|
|||||||
presenter.SaveDocument();
|
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()
|
private void Run()
|
||||||
{
|
{
|
||||||
FlushPendingEdits();
|
FlushPendingEdits();
|
||||||
@@ -281,6 +348,7 @@ namespace CutList.Forms
|
|||||||
dataGridView1.Refresh();
|
dataGridView1.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
|
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
|
||||||
{
|
{
|
||||||
dataGridView1.Rows[e.RowIndex].ErrorText = e.Exception.InnerException?.Message;
|
dataGridView1.Rows[e.RowIndex].ErrorText = e.Exception.InnerException?.Message;
|
||||||
|
|||||||
@@ -88,5 +88,11 @@ namespace CutList.Presenters
|
|||||||
/// Clears all data in the view.
|
/// Clears all data in the view.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ClearData();
|
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 CutListService _cutListService;
|
||||||
private readonly DocumentService _documentService;
|
private readonly DocumentService _documentService;
|
||||||
private Document _currentDocument;
|
private Document _currentDocument;
|
||||||
|
private int _documentCounter = 0;
|
||||||
|
|
||||||
public MainFormPresenter(IMainView view, CutListService cutListService, DocumentService documentService)
|
public MainFormPresenter(IMainView view, CutListService cutListService, DocumentService documentService)
|
||||||
{
|
{
|
||||||
@@ -40,11 +41,13 @@ namespace CutList.Presenters
|
|||||||
|
|
||||||
_currentDocument = loadResult.Value;
|
_currentDocument = loadResult.Value;
|
||||||
_view.LoadDocumentData(_currentDocument.PartsToNest, _currentDocument.StockBins);
|
_view.LoadDocumentData(_currentDocument.PartsToNest, _currentDocument.StockBins);
|
||||||
|
_view.UpdateWindowTitle(Path.GetFileName(filePath));
|
||||||
UpdateRunButtonState();
|
UpdateRunButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void SaveDocument()
|
public void SaveDocument()
|
||||||
{
|
{
|
||||||
@@ -57,19 +60,59 @@ namespace CutList.Presenters
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultFileName = _currentDocument.LastFilePath == null
|
// If we have a known path, save directly without prompting
|
||||||
? "NewDocument.json"
|
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);
|
: Path.GetFileName(_currentDocument.LastFilePath);
|
||||||
|
|
||||||
if (!_view.PromptSaveFile("Json File|*.json", defaultFileName, out string filePath))
|
if (!_view.PromptSaveFile("Json File|*.json", defaultFileName, out string filePath))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
SaveToPath(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveToPath(string filePath)
|
||||||
|
{
|
||||||
var saveResult = _documentService.Save(_currentDocument, filePath);
|
var saveResult = _documentService.Save(_currentDocument, filePath);
|
||||||
|
|
||||||
if (saveResult.IsFailure)
|
if (saveResult.IsFailure)
|
||||||
{
|
{
|
||||||
_view.ShowError(saveResult.Error);
|
_view.ShowError(saveResult.Error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_currentDocument.LastFilePath = filePath;
|
||||||
|
_view.UpdateWindowTitle(Path.GetFileName(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateDefaultFileName()
|
||||||
|
{
|
||||||
|
_documentCounter++;
|
||||||
|
return $"CutList_{_documentCounter}.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -100,6 +143,7 @@ namespace CutList.Presenters
|
|||||||
{
|
{
|
||||||
_currentDocument = new Document();
|
_currentDocument = new Document();
|
||||||
_view.ClearData();
|
_view.ClearData();
|
||||||
|
_view.UpdateWindowTitle(null);
|
||||||
UpdateRunButtonState();
|
UpdateRunButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace CutList.Services
|
|||||||
{
|
{
|
||||||
var json = File.ReadAllText(filePath);
|
var json = File.ReadAllText(filePath);
|
||||||
var document = JsonConvert.DeserializeObject<Document>(json);
|
var document = JsonConvert.DeserializeObject<Document>(json);
|
||||||
|
document.LastFilePath = filePath;
|
||||||
return Result<Document>.Success(document);
|
return Result<Document>.Success(document);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user