fix(desktop): pick up a drawing on mouse down in the parts list

Choosing a part from the editor's drawing list armed the pickup only in
Click. Moving the mouse more than 3 px before release started a
drag-and-drop, which takes mouse capture, so the list never raised Click
and nothing attached to the cursor on the plate.

The list now raises DrawingPickedUp on a single left press over a row
and the editor arms the pickup there. Dragging onto the plate still
drops a part at the release point; a completed drop clears the pickup so
a second copy does not follow the cursor. Presses below the last row,
right presses and the second press of a double-click do not pick up.
This commit is contained in:
aj committed 2026-10-14 16:11:23 -04:00
1 parent 720c3d9b7a
commit 8834abc854
4 files changed
+208 -6

No files matched your search

@@ -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<ActionClone>(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<ActionSelect>(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<Drawing>();
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<ActionSelect>(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<Part>)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);
}
}
+49 -4
View File
@@ -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<Drawing> DeleteRequested;
/// <summary>
/// Raised when the left button is pressed on a drawing's row.
/// </summary>
public event EventHandler<Drawing> DrawingPickedUp;
/// <summary>
/// Raised when a drawing dragged out of the list was accepted by a drop target.
/// </summary>
public event EventHandler<Drawing> 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);
}
/// <summary>
/// The drawing whose row contains <paramref name="location"/>, or null
/// when the point is in the empty area below the last row.
/// </summary>
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)
-1
View File
@@ -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
+11 -1
View File
@@ -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)