diff --git a/ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/toollibrary/ToolEditorPanel.java b/ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/toollibrary/ToolEditorPanel.java index 1b3cd394a6..f20d1aa303 100644 --- a/ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/toollibrary/ToolEditorPanel.java +++ b/ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/toollibrary/ToolEditorPanel.java @@ -32,9 +32,9 @@ This file is part of Universal Gcode Sender (UGS). import javax.swing.JSeparator; import javax.swing.JTextField; import javax.swing.SwingConstants; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; import java.awt.Dimension; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; import java.util.function.Consumer; /** @@ -88,12 +88,18 @@ public void setChangeListener(Consumer listener) { private void initComponents() { setLayout(new MigLayout("fillx, wrap 2", "[pref!][grow,fill]")); - setPreferredSize(new Dimension(360, 420)); - setMinimumSize(new Dimension(360, 420)); + setPreferredSize(new Dimension(420, 420)); + setMinimumSize(new Dimension(420, 420)); add(new JLabel("Name")); nameField = new JTextField(); - nameField.getDocument().addDocumentListener(simpleDocListener(this::fireChange)); + nameField.addActionListener(e -> { if (!suppressEvents) fireChange(); }); + nameField.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + if (!suppressEvents) fireChange(); + } + }); add(nameField, "growx"); add(new JLabel("Shape")); @@ -328,11 +334,4 @@ private void validateAndReportError() { errorLabel.setText(error.isEmpty() ? " " : error); } - private DocumentListener simpleDocListener(Runnable onChange) { - return new DocumentListener() { - @Override public void insertUpdate(DocumentEvent e) { if (!suppressEvents) onChange.run(); } - @Override public void removeUpdate(DocumentEvent e) { if (!suppressEvents) onChange.run(); } - @Override public void changedUpdate(DocumentEvent e) { if (!suppressEvents) onChange.run(); } - }; - } } diff --git a/ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/toollibrary/ToolLibraryDialog.java b/ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/toollibrary/ToolLibraryDialog.java index 324fac2669..df31c7103a 100644 --- a/ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/toollibrary/ToolLibraryDialog.java +++ b/ugs-designer/src/main/java/com/willwinder/ugs/designer/gui/toollibrary/ToolLibraryDialog.java @@ -75,7 +75,7 @@ public ToolLibraryDialog(Window owner, ToolLibraryService service, UnitUtils.Uni private void initComponents() { setLayout(new MigLayout("fill", "[250!][grow]", "[grow][]")); - setPreferredSize(new Dimension(720, 520)); + setPreferredSize(new Dimension(780, 520)); toolList = new JList<>(listModel); toolList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); @@ -134,7 +134,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i JComponent.WHEN_IN_FOCUSED_WINDOW); setSize(getPreferredSize()); - setMinimumSize(new Dimension(560, 400)); + setMinimumSize(new Dimension(620, 400)); setLocationRelativeTo(getOwner()); } diff --git a/ugs-designer/src/test/java/com/willwinder/ugs/designer/gui/toollibrary/ToolEditorPanelNameCommitTest.java b/ugs-designer/src/test/java/com/willwinder/ugs/designer/gui/toollibrary/ToolEditorPanelNameCommitTest.java new file mode 100644 index 0000000000..3a4384f4f6 --- /dev/null +++ b/ugs-designer/src/test/java/com/willwinder/ugs/designer/gui/toollibrary/ToolEditorPanelNameCommitTest.java @@ -0,0 +1,121 @@ +/* + Copyright 2026 Damian Nikodem + + This file is part of Universal Gcode Sender (UGS). + + UGS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UGS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UGS. If not, see . + */ +package com.willwinder.ugs.designer.gui.toollibrary; + +import com.willwinder.ugs.designer.model.toollibrary.EndmillShape; +import com.willwinder.ugs.designer.model.toollibrary.ToolDefinition; +import com.willwinder.universalgcodesender.model.UnitUtils; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import javax.swing.JTextField; +import java.awt.GraphicsEnvironment; +import java.awt.event.ActionEvent; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Regression guard for the Tool Library name-field focus-loss bug. Typing into the Name field used + * to commit on every keystroke via a DocumentListener, which triggered a refresh cycle that + * disabled the field mid-edit and kicked focus to the Shape combo. The fix moves commit to + * Enter / focus-lost so the field stays focused while the user types. + */ +public class ToolEditorPanelNameCommitTest { + + private ToolEditorPanel panel; + private JTextField nameField; + private AtomicInteger fireCount; + private AtomicReference lastEdit; + + @Before + public void setUp() throws Exception { + Assume.assumeFalse(GraphicsEnvironment.isHeadless()); + panel = new ToolEditorPanel(UnitUtils.Units.MM); + nameField = readField("nameField"); + + fireCount = new AtomicInteger(); + lastEdit = new AtomicReference<>(); + panel.setChangeListener(t -> { + fireCount.incrementAndGet(); + lastEdit.set(t); + }); + + ToolDefinition tool = new ToolDefinition(); + tool.setName("Initial"); + tool.setShape(EndmillShape.UPCUT); + tool.setDiameter(3.0); + tool.setDiameterUnit(UnitUtils.Units.MM); + tool.setFeedSpeed(900); + tool.setPlungeSpeed(300); + tool.setDepthPerPass(1.0); + tool.setStepOverPercent(0.4); + tool.setMaxSpindleSpeed(18000); + panel.setTool(tool, false); + fireCount.set(0); + lastEdit.set(null); + } + + @Test + public void typingDoesNotFireChange() throws Exception { + nameField.getDocument().insertString(0, "A", null); + nameField.getDocument().insertString(1, "B", null); + nameField.getDocument().insertString(2, "C", null); + assertEquals("Typing into the name field must not commit per keystroke.", + 0, fireCount.get()); + } + + @Test + public void enterCommitsName() { + nameField.setText("Renamed"); + nameField.getActionListeners()[0].actionPerformed( + new ActionEvent(nameField, ActionEvent.ACTION_PERFORMED, "")); + assertEquals(1, fireCount.get()); + assertNotNull(lastEdit.get()); + assertEquals("Renamed", lastEdit.get().getName()); + } + + @Test + public void focusLostCommitsName() { + nameField.setText("AfterTab"); + FocusEvent event = new FocusEvent(nameField, FocusEvent.FOCUS_LOST); + for (FocusListener listener : nameField.getFocusListeners()) { + if (listener instanceof FocusAdapter) { + listener.focusLost(event); + } + } + assertEquals(1, fireCount.get()); + assertNotNull(lastEdit.get()); + assertEquals("AfterTab", lastEdit.get().getName()); + } + + @SuppressWarnings("unchecked") + private T readField(String name) throws Exception { + Field field = ToolEditorPanel.class.getDeclaredField(name); + field.setAccessible(true); + return (T) field.get(panel); + } +}