diff --git a/OpenNest.WinForms.Tests/Controls/DrawingListPickupTests.cs b/OpenNest.WinForms.Tests/Controls/DrawingListPickupTests.cs new file mode 100644 index 00000000..15473745 --- /dev/null +++ b/OpenNest.WinForms.Tests/Controls/DrawingListPickupTests.cs @@ -0,0 +1,148 @@ +using System.Drawing; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using OpenNest.Actions; +using OpenNest.CNC; +using OpenNest.Controls; +using OpenNest.Forms; + +namespace OpenNest.WinForms.Tests.Controls; + +[Collection("Fill operation lifetime")] +public class DrawingListPickupTests +{ + private const int WM_LBUTTONDOWN = 0x0201; + private const int WM_LBUTTONUP = 0x0202; + private const int MK_LBUTTON = 0x0001; + + [Fact] + public void PressingADrawingRowPicksItUpWithoutWaitingForClick() => RunSta(() => + { + var (form, list, second) = CreateEditor(); + using (form) + { + var center = RowCenter(list, list.Items.IndexOf(second)); + + // Only the press: a mouse that moves before release never produces Click. + SendMessage(list.Handle, WM_LBUTTONDOWN, MK_LBUTTON, MakeLParam(center)); + try + { + EnterPlateView(form); + + var clone = Assert.IsType(form.PlateView.Actions.CurrentAction); + Assert.Same(second, ClonedDrawing(clone)); + } + finally + { + SendMessage(list.Handle, WM_LBUTTONUP, 0, MakeLParam(center)); + } + } + }); + + [Fact] + public void CompletedDragDropDoesNotAttachASecondCopy() => RunSta(() => + { + var (form, list, second) = CreateEditor(); + using (form) + { + var center = RowCenter(list, list.Items.IndexOf(second)); + SendMessage(list.Handle, WM_LBUTTONDOWN, MK_LBUTTON, MakeLParam(center)); + SendMessage(list.Handle, WM_LBUTTONUP, 0, MakeLParam(center)); + + typeof(EditNestForm) + .GetMethod("drawingListBox1_DrawingDropped", BindingFlags.Instance | BindingFlags.NonPublic)! + .Invoke(form, [list, second]); + EnterPlateView(form); + + Assert.IsType(form.PlateView.Actions.CurrentAction); + } + }); + + [Theory] + [InlineData(MouseButtons.Right, 1, false)] + [InlineData(MouseButtons.Left, 2, false)] + [InlineData(MouseButtons.Left, 1, true)] + public void OnlyASingleLeftPressOnARowPicksUp(MouseButtons button, int clicks, bool belowRows) => + RunSta(() => + { + using var list = new PressableDrawingListBox { Size = new Size(260, 400) }; + var drawing = new Drawing("only", Square()); + list.Items.Add(drawing); + _ = list.Handle; + var picked = new List(); + list.DrawingPickedUp += (_, d) => picked.Add(d); + + var point = belowRows + ? new Point(20, list.GetItemRectangle(0).Bottom + 20) + : RowCenter(list, 0); + list.Press(new MouseEventArgs(button, clicks, point.X, point.Y, 0)); + + Assert.Empty(picked); + + list.Press(new MouseEventArgs(MouseButtons.Left, 1, RowCenter(list, 0).X, RowCenter(list, 0).Y, 0)); + Assert.Same(drawing, Assert.Single(picked)); + }); + + private static (EditNestForm Form, DrawingListBox List, Drawing Second) CreateEditor() + { + var nest = new Nest("pickup"); + nest.Drawings.Add(new Drawing("a", Square())); + var second = new Drawing("b", Square()); + nest.Drawings.Add(second); + var form = new EditNestForm(nest) { Width = 1100, Height = 700 }; + form.PlateView.SetOverlapAutoCheck(null); + form.Show(); + Application.DoEvents(); + + var list = (DrawingListBox)typeof(EditNestForm) + .GetField("drawingListBox1", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(form)!; + Assert.Equal(2, list.Items.Count); + Assert.IsType(form.PlateView.Actions.CurrentAction); + return (form, list, second); + } + + private static void EnterPlateView(EditNestForm form) => + typeof(EditNestForm) + .GetMethod("PlateView_Enter", BindingFlags.Instance | BindingFlags.NonPublic)! + .Invoke(form, [form.PlateView, EventArgs.Empty]); + + private static Drawing ClonedDrawing(ActionClone clone) + { + var parts = (List)typeof(ActionClone) + .GetField("sourceParts", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(clone)!; + return Assert.Single(parts).BaseDrawing; + } + + private static Point RowCenter(ListBox list, int index) + { + var row = list.GetItemRectangle(index); + return new Point(row.Left + row.Width / 2, row.Top + row.Height / 2); + } + + private static IntPtr MakeLParam(Point point) => (IntPtr)((point.Y << 16) | (point.X & 0xFFFF)); + + private static Program Square() + { + var program = new Program(); + program.MoveTo(0, 0); + program.LineTo(0, 10); + program.LineTo(10, 10); + program.LineTo(10, 0); + program.LineTo(0, 0); + return program; + } + + private static void RunSta(System.Action action) => + StaTestThread.Run(action, TimeSpan.FromMinutes(3), "The STA test did not complete."); + + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam); + + private sealed class PressableDrawingListBox : DrawingListBox + { + public void Press(MouseEventArgs e) => OnMouseDown(e); + } +} diff --git a/OpenNest/Controls/DrawingListBox.cs b/OpenNest/Controls/DrawingListBox.cs index a8bce0cf..7d1a533f 100644 --- a/OpenNest/Controls/DrawingListBox.cs +++ b/OpenNest/Controls/DrawingListBox.cs @@ -12,6 +12,7 @@ namespace OpenNest.Controls private readonly Size imageSize; private readonly Font nameFont; private Point lastClickLocation; + private Drawing pressedDrawing; public DrawingListBox() { @@ -39,6 +40,16 @@ namespace OpenNest.Controls public event EventHandler DeleteRequested; + /// + /// Raised when the left button is pressed on a drawing's row. + /// + public event EventHandler DrawingPickedUp; + + /// + /// Raised when a drawing dragged out of the list was accepted by a drop target. + /// + public event EventHandler DrawingDropped; + public Units Units { get; set; } public bool HideDepletedParts { get; set; } @@ -152,19 +163,53 @@ namespace OpenNest.Controls { base.OnMouseMove(e); - var item = SelectedItem as Drawing; + var drawing = pressedDrawing; - if (item == null) + if (drawing == null || e.Button != MouseButtons.Left) return; - if (e.Button == MouseButtons.Left && e.Location.DistanceTo(lastClickLocation) > 3) - DoDragDrop(item, DragDropEffects.Copy); + if (e.Location.DistanceTo(lastClickLocation) <= 3) + return; + + pressedDrawing = null; + + if (DoDragDrop(drawing, DragDropEffects.Copy) != DragDropEffects.None) + DrawingDropped?.Invoke(this, drawing); } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); lastClickLocation = e.Location; + + // Pick the part up on the press. Waiting for Click loses the pickup + // whenever the mouse moves a few pixels first: the drag that starts + // takes mouse capture and the list never raises Click. + pressedDrawing = + e.Button == MouseButtons.Left && e.Clicks == 1 ? DrawingAt(e.Location) : null; + + if (pressedDrawing != null) + DrawingPickedUp?.Invoke(this, pressedDrawing); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + pressedDrawing = null; + base.OnMouseUp(e); + } + + /// + /// The drawing whose row contains , or null + /// when the point is in the empty area below the last row. + /// + internal Drawing DrawingAt(Point location) + { + var index = IndexFromPoint(location); + + if (index < 0 || index >= Items.Count) + return null; + + return GetItemRectangle(index).Contains(location) ? Items[index] as Drawing : null; } protected override void WndProc(ref Message m) diff --git a/OpenNest/Forms/EditNestForm.Designer.cs b/OpenNest/Forms/EditNestForm.Designer.cs index 1ccf8634..38f04ba7 100644 --- a/OpenNest/Forms/EditNestForm.Designer.cs +++ b/OpenNest/Forms/EditNestForm.Designer.cs @@ -213,7 +213,6 @@ drawingListBox1.Size = new System.Drawing.Size(265, 394); drawingListBox1.TabIndex = 1; drawingListBox1.Units = Units.Inches; - drawingListBox1.Click += drawingListBox1_Click; drawingListBox1.DoubleClick += drawingListBox1_DoubleClick; // // toolStrip2 diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index 342dc3e3..16d79684 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -432,6 +432,8 @@ namespace OpenNest.Forms Text = Nest.Name; drawingListBox1.Units = Nest.Units; drawingListBox1.DeleteRequested += drawingListBox1_DeleteRequested; + drawingListBox1.DrawingPickedUp += drawingListBox1_DrawingPickedUp; + drawingListBox1.DrawingDropped += drawingListBox1_DrawingDropped; } public void UpdatePlateList() @@ -1423,11 +1425,19 @@ namespace OpenNest.Forms PlateView.Invalidate(); } - private void drawingListBox1_Click(object sender, EventArgs e) + private void drawingListBox1_DrawingPickedUp(object sender, Drawing drawing) { + // Armed on the press, so moving the mouse before release still + // attaches the part when the pointer reaches the plate. addPart = true; } + private void drawingListBox1_DrawingDropped(object sender, Drawing drawing) + { + // The drop already placed the part; don't attach another copy. + addPart = false; + } + private void PlateView_MouseEnter(object sender, EventArgs e) { if (!PlateView.Focused)