mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-06 01:52:10 -04:00
fix(leadin): add close button to Place Lead-in side panel
The docked side panel could only be closed with Escape, which was unreliable: MainForm consumes Escape before ActionLeadIn's KeyDown handler, so with a part selected ActionManager called the action's empty CancelAction and nothing happened; with nothing selected the panel closed but was stored as the previous action, so the next Escape reopened it. - EditNestForm side panel gets a header with the panel title and an always-visible close button that ends the action. - ActionLeadIn.CancelAction now steps back (unlock contour, then deselect part), replacing the dead KeyDown handler. - Actions can opt out of Escape-resume (ResumeOnEscape); the lead-in action does, so a closed panel stays closed.
This commit is contained in:
1 parent
36de55f10f
commit
2b8a3ce7db
6 files changed
+149
-29
No files matched your search
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using OpenNest.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.WinForms.Tests.Forms;
|
||||||
|
|
||||||
|
public class LeadInSidePanelTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void PanelClosesWithoutEscapeReopeningIt(bool useCloseButton) => RunSta(() =>
|
||||||
|
{
|
||||||
|
using var form = new EditNestForm(new Nest("lead-in panel"));
|
||||||
|
form.Show();
|
||||||
|
|
||||||
|
form.PlaceLeadIn_Click(null!, EventArgs.Empty);
|
||||||
|
Assert.True(form.IsSidePanelVisible);
|
||||||
|
Assert.Equal("Place Lead-in", form.PlateView.Status);
|
||||||
|
|
||||||
|
if (useCloseButton)
|
||||||
|
{
|
||||||
|
var close = Assert.IsType<Button>(
|
||||||
|
Assert.Single(form.Controls.Find("sidePanelCloseButton", true))
|
||||||
|
);
|
||||||
|
close.PerformClick();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
form.PlateView.ProcessEscapeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(form.IsSidePanelVisible);
|
||||||
|
Assert.Equal("Select", form.PlateView.Status);
|
||||||
|
|
||||||
|
// Escape from Select normally resumes the previous action; it must not
|
||||||
|
// bring back a panel the user just closed.
|
||||||
|
form.PlateView.ProcessEscapeKey();
|
||||||
|
Assert.False(form.IsSidePanelVisible);
|
||||||
|
Assert.Equal("Select", form.PlateView.Status);
|
||||||
|
});
|
||||||
|
|
||||||
|
private static void RunSta(System.Action action)
|
||||||
|
{
|
||||||
|
Exception? failure = null;
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure = ex;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{ IsBackground = true };
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
Assert.True(thread.Join(TimeSpan.FromSeconds(60)), "The STA test did not complete.");
|
||||||
|
if (failure != null)
|
||||||
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,11 @@ namespace OpenNest.Actions
|
|||||||
|
|
||||||
public virtual bool SurvivesPlateChange => false;
|
public virtual bool SurvivesPlateChange => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether Escape from Select may bring this action back after it ends.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool ResumeOnEscape => true;
|
||||||
|
|
||||||
public virtual void OnPlateChanged() { }
|
public virtual void OnPlateChanged() { }
|
||||||
|
|
||||||
public virtual void ConnectEvents() { }
|
public virtual void ConnectEvents() { }
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ namespace OpenNest.Actions
|
|||||||
{
|
{
|
||||||
plateView.MouseMove += OnMouseMove;
|
plateView.MouseMove += OnMouseMove;
|
||||||
plateView.MouseDown += OnMouseDown;
|
plateView.MouseDown += OnMouseDown;
|
||||||
plateView.KeyDown += OnKeyDown;
|
|
||||||
plateView.Paint += OnPaint;
|
plateView.Paint += OnPaint;
|
||||||
ShowSidePanel();
|
ShowSidePanel();
|
||||||
}
|
}
|
||||||
@@ -64,7 +63,6 @@ namespace OpenNest.Actions
|
|||||||
{
|
{
|
||||||
plateView.MouseMove -= OnMouseMove;
|
plateView.MouseMove -= OnMouseMove;
|
||||||
plateView.MouseDown -= OnMouseDown;
|
plateView.MouseDown -= OnMouseDown;
|
||||||
plateView.KeyDown -= OnKeyDown;
|
|
||||||
plateView.Paint -= OnPaint;
|
plateView.Paint -= OnPaint;
|
||||||
|
|
||||||
HideSidePanel();
|
HideSidePanel();
|
||||||
@@ -72,23 +70,25 @@ namespace OpenNest.Actions
|
|||||||
contextMenu?.Dispose();
|
contextMenu?.Dispose();
|
||||||
contextMenu = null;
|
contextMenu = null;
|
||||||
|
|
||||||
if (selectedLayoutPart != null)
|
DeselectPart();
|
||||||
selectedLayoutPart.IsSelected = false;
|
|
||||||
|
|
||||||
selectedLayoutPart = null;
|
|
||||||
selectedPart = null;
|
|
||||||
profile = null;
|
|
||||||
contours = null;
|
|
||||||
hasSnap = false;
|
|
||||||
activeSnapType = SnapType.None;
|
|
||||||
hoveredContour = null;
|
|
||||||
plateView.Invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void CancelAction() { }
|
// Escape reaches here through ActionManager.ProcessEscapeKey while IsBusy:
|
||||||
|
// step back one level (unlock the contour, then release the part). Once
|
||||||
|
// nothing is selected, Escape ends the action and closes the side panel.
|
||||||
|
public override void CancelAction()
|
||||||
|
{
|
||||||
|
if (lockedContour != null)
|
||||||
|
UnlockContour();
|
||||||
|
else
|
||||||
|
DeselectPart();
|
||||||
|
}
|
||||||
|
|
||||||
public override bool IsBusy() => selectedPart != null;
|
public override bool IsBusy() => selectedPart != null;
|
||||||
|
|
||||||
|
// Escape closes the side panel; a second Escape must not reopen it.
|
||||||
|
public override bool ResumeOnEscape => false;
|
||||||
|
|
||||||
private void ShowSidePanel()
|
private void ShowSidePanel()
|
||||||
{
|
{
|
||||||
var form = plateView.FindForm() as EditNestForm;
|
var form = plateView.FindForm() as EditNestForm;
|
||||||
@@ -119,7 +119,7 @@ namespace OpenNest.Actions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form.ShowSidePanel(cuttingPanel);
|
form.ShowSidePanel(cuttingPanel, "Place Lead-in");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HideSidePanel()
|
private void HideSidePanel()
|
||||||
@@ -249,19 +249,6 @@ namespace OpenNest.Actions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnKeyDown(object sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.KeyCode == Keys.Escape)
|
|
||||||
{
|
|
||||||
if (lockedContour != null)
|
|
||||||
UnlockContour();
|
|
||||||
else if (selectedPart != null)
|
|
||||||
DeselectPart();
|
|
||||||
else
|
|
||||||
plateView.SetAction(typeof(ActionSelect));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPaint(object sender, PaintEventArgs e)
|
private void OnPaint(object sender, PaintEventArgs e)
|
||||||
{
|
{
|
||||||
var g = e.Graphics;
|
var g = e.Graphics;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace OpenNest.Controls
|
|||||||
if (
|
if (
|
||||||
type == typeof(Actions.ActionSelect)
|
type == typeof(Actions.ActionSelect)
|
||||||
&& !(currentAction is Actions.ActionSelect)
|
&& !(currentAction is Actions.ActionSelect)
|
||||||
|
&& currentAction.ResumeOnEscape
|
||||||
)
|
)
|
||||||
previousAction = currentAction;
|
previousAction = currentAction;
|
||||||
else
|
else
|
||||||
@@ -75,6 +76,17 @@ namespace OpenNest.Controls
|
|||||||
view.SetAction(typeof(Actions.ActionSelect));
|
view.SetAction(typeof(Actions.ActionSelect));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends the current action at the user's explicit request (for example a
|
||||||
|
/// side panel's Close button). Unlike Escape, the ended action is not kept
|
||||||
|
/// for a later Escape to restore.
|
||||||
|
/// </summary>
|
||||||
|
public void EndAction()
|
||||||
|
{
|
||||||
|
SetAction(typeof(Actions.ActionSelect));
|
||||||
|
previousAction = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void RestorePreviousAction()
|
public void RestorePreviousAction()
|
||||||
{
|
{
|
||||||
var action = previousAction;
|
var action = previousAction;
|
||||||
|
|||||||
@@ -487,6 +487,8 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
public void ProcessEscapeKey() => actionManager.ProcessEscapeKey();
|
public void ProcessEscapeKey() => actionManager.ProcessEscapeKey();
|
||||||
|
|
||||||
|
public void EndAction() => actionManager.EndAction();
|
||||||
|
|
||||||
protected override bool ProcessDialogKey(Keys keyData)
|
protected override bool ProcessDialogKey(Keys keyData)
|
||||||
{
|
{
|
||||||
// Only handle TAB, RETURN, ESC, and ARROW KEYS here.
|
// Only handle TAB, RETURN, ESC, and ARROW KEYS here.
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
private SplitContainer viewSplitContainer;
|
private SplitContainer viewSplitContainer;
|
||||||
private Panel sidePanel;
|
private Panel sidePanel;
|
||||||
|
private Label sidePanelTitle;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to distinguish between single/double click on drawing within drawinglistbox.
|
/// Used to distinguish between single/double click on drawing within drawinglistbox.
|
||||||
@@ -163,6 +164,40 @@ namespace OpenNest.Forms
|
|||||||
BackColor = Color.White,
|
BackColor = Color.White,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The header sits outside the scrolling content so Close stays visible.
|
||||||
|
sidePanelTitle = new Label
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
AutoEllipsis = true,
|
||||||
|
TextAlign = ContentAlignment.MiddleLeft,
|
||||||
|
Font = new Font("Segoe UI", 9f, FontStyle.Bold),
|
||||||
|
};
|
||||||
|
|
||||||
|
var closeButton = new Button
|
||||||
|
{
|
||||||
|
Name = "sidePanelCloseButton",
|
||||||
|
Text = "\u00D7",
|
||||||
|
AccessibleName = "Close panel",
|
||||||
|
Dock = DockStyle.Right,
|
||||||
|
Width = 30,
|
||||||
|
Font = new Font("Segoe UI", 12f),
|
||||||
|
FlatStyle = FlatStyle.Flat,
|
||||||
|
FlatAppearance = { BorderSize = 0 },
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
};
|
||||||
|
closeButton.Click += (s, e) => CloseSidePanel();
|
||||||
|
new ToolTip(components).SetToolTip(closeButton, "Close (Esc)");
|
||||||
|
|
||||||
|
var sidePanelHeader = new Panel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
Height = 30,
|
||||||
|
BackColor = Color.FromArgb(240, 240, 240),
|
||||||
|
Padding = new Padding(8, 0, 0, 0),
|
||||||
|
};
|
||||||
|
sidePanelHeader.Controls.Add(sidePanelTitle);
|
||||||
|
sidePanelHeader.Controls.Add(closeButton);
|
||||||
|
|
||||||
viewSplitContainer = new SplitContainer
|
viewSplitContainer = new SplitContainer
|
||||||
{
|
{
|
||||||
Dock = DockStyle.Fill,
|
Dock = DockStyle.Fill,
|
||||||
@@ -173,14 +208,18 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
viewSplitContainer.Panel1.Controls.Add(PlateView);
|
viewSplitContainer.Panel1.Controls.Add(PlateView);
|
||||||
viewSplitContainer.Panel2.Controls.Add(sidePanel);
|
viewSplitContainer.Panel2.Controls.Add(sidePanel);
|
||||||
|
viewSplitContainer.Panel2.Controls.Add(sidePanelHeader);
|
||||||
viewSplitContainer.Panel2Collapsed = true;
|
viewSplitContainer.Panel2Collapsed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowSidePanel(Control content, int width = 390)
|
public bool IsSidePanelVisible => !viewSplitContainer.Panel2Collapsed;
|
||||||
|
|
||||||
|
public void ShowSidePanel(Control content, string title, int width = 390)
|
||||||
{
|
{
|
||||||
sidePanel.Controls.Clear();
|
sidePanel.Controls.Clear();
|
||||||
content.Dock = DockStyle.Fill;
|
content.Dock = DockStyle.Fill;
|
||||||
sidePanel.Controls.Add(content);
|
sidePanel.Controls.Add(content);
|
||||||
|
sidePanelTitle.Text = title;
|
||||||
viewSplitContainer.SplitterDistance = viewSplitContainer.Width - width;
|
viewSplitContainer.SplitterDistance = viewSplitContainer.Width - width;
|
||||||
viewSplitContainer.Panel2Collapsed = false;
|
viewSplitContainer.Panel2Collapsed = false;
|
||||||
}
|
}
|
||||||
@@ -189,6 +228,17 @@ namespace OpenNest.Forms
|
|||||||
{
|
{
|
||||||
viewSplitContainer.Panel2Collapsed = true;
|
viewSplitContainer.Panel2Collapsed = true;
|
||||||
sidePanel.Controls.Clear();
|
sidePanel.Controls.Clear();
|
||||||
|
sidePanelTitle.Text = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseSidePanel()
|
||||||
|
{
|
||||||
|
// Side panels belong to the plate action that opened them; ending the
|
||||||
|
// action hides its panel. Hide directly as well so Close always closes.
|
||||||
|
PlateView.EndAction();
|
||||||
|
if (IsSidePanelVisible)
|
||||||
|
HideSidePanel();
|
||||||
|
PlateView.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Button CreateNavButton(System.Drawing.Image image)
|
private static Button CreateNavButton(System.Drawing.Image image)
|
||||||
|
|||||||
Reference in new issue
Block a user