Bugfixes from zOS testing
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 37s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m16s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m52s

This commit is contained in:
2026-09-17 23:09:43 +00:00
parent 30b1a77b61
commit ef70ea0f5a
25 changed files with 2686 additions and 335 deletions
@@ -104,6 +104,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
}
});
// Ensure clicking anywhere inside the frame brings focus to window and terminalPanel
Toolkit.getDefaultToolkit().addAWTEventListener(event -> {
if (event instanceof MouseEvent && event.getID() == MouseEvent.MOUSE_PRESSED) {
MouseEvent me = (MouseEvent) event;
Object src = me.getSource();
if (src instanceof Component) {
Window win = SwingUtilities.getWindowAncestor((Component) src);
if (win == J3270App.this && !isAnyChildDialogActive()) {
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
J3270App.this.toFront();
J3270App.this.requestFocus();
}
boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu)
|| SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null
|| SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null;
if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
}
}
}
}, AWTEvent.MOUSE_EVENT_MASK);
}
private boolean isAnyChildDialogActive() {
@@ -694,6 +717,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
dialog.setVisible(true);
if (dialog.isConfirmed()) {
@@ -730,6 +754,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
if (config.getGraphicsMode() != null) {
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
}
disconnect();
@@ -234,7 +234,6 @@ public class HostStorage {
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
if (map.containsKey("graphicsMode")) {
String gm = String.valueOf(map.get("graphicsMode"));
if ("PROGRAMMED_SYMBOLS".equalsIgnoreCase(gm)) gm = "BOTH";
h.setGraphicsMode(gm);
}
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
@@ -6,6 +6,7 @@ import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.util.List;
@@ -226,6 +227,24 @@ public class ConnectDialog extends JDialog {
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(graphicsCombo);
graphicsCombo.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setBorder(new EmptyBorder(4, 8, 4, 8));
if (value instanceof haus.nightmare.lib3270j.graphics.GraphicsMode) {
setText(((haus.nightmare.lib3270j.graphics.GraphicsMode) value).getDescription());
}
if (isSelected) {
setBackground(ThemeManager.getSelectionBg());
setForeground(ThemeManager.getSelectionFg());
} else {
setBackground(ThemeManager.getBgComponent());
setForeground(ThemeManager.getFgMain());
}
return this;
}
});
contentPanel.add(graphicsCombo, gbc);
gridy++;
@@ -664,7 +683,7 @@ public class ConnectDialog extends JDialog {
String gfx = "BOTH";
if (graphicsCombo.getSelectedItem() != null) {
gfx = graphicsCombo.getSelectedItem().toString();
gfx = ((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()).name();
}
String cp = "037";
@@ -731,7 +750,9 @@ public class ConnectDialog extends JDialog {
result.setLuName(lu);
}
if (graphicsCombo.getSelectedItem() != null) {
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
haus.nightmare.lib3270j.graphics.GraphicsMode gm = (haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem();
result.setGraphicsMode(gm);
haus.nightmare.j3270.config.Settings.setGraphicsMode(gm);
}
Object cpSelection = codePageCombo.getSelectedItem();
@@ -776,6 +797,14 @@ public class ConnectDialog extends JDialog {
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
}
if (selectedProfileId != null) {
SavedHost existing = HostStorage.findById(selectedProfileId);
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
? existing.getName() : host;
SavedHost updated = buildCurrentSavedHost(pName);
HostStorage.saveHost(updated);
}
confirmed = true;
dispose();
}
@@ -810,6 +839,12 @@ public class ConnectDialog extends JDialog {
tn3270eCheckBox.setSelected(tn3270e);
}
public void setInitialGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
if (mode != null && selectedProfileId == null && graphicsCombo != null) {
graphicsCombo.setSelectedItem(mode);
}
}
public void setInitialCodePage(String cp) {
if (cp != null && codePageCombo != null) {
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
@@ -25,8 +25,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
// Font and cell dimensions
private Font terminalFont;
private Font boldTerminalFont;
private int cellWidth;
private int cellHeight;
int cellWidth;
int cellHeight;
private int fontAscent;
private int fontDescent;
private int padding = 4;
@@ -74,6 +74,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private int selectionStartRow = -1, selectionStartCol = -1;
private int selectionEndRow = -1, selectionEndCol = -1;
private boolean isDragging = false;
private boolean isActivationClick = false;
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend
// ========== Search Highlight state ==========
@@ -117,7 +118,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
/**
* Compute the horizontal render offset to center the grid within the panel.
*/
private int getRenderOffsetX() {
int getRenderOffsetX() {
int termCols = 80;
if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
int gridW = termCols * cellWidth;
@@ -128,7 +129,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
/**
* Compute the vertical render offset to center the grid within the panel.
*/
private int getRenderOffsetY() {
int getRenderOffsetY() {
int termRows = 24;
if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
int gridH = termRows * cellHeight;
@@ -186,7 +187,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
MouseAdapter mouseHandler = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
requestFocusInWindow();
Window win = SwingUtilities.getWindowAncestor(TerminalPanel.this);
boolean wasWindowInactive = (win != null && (!win.isActive() || !win.isFocused()));
boolean wasPanelUnfocused = !hasFocus();
isActivationClick = wasWindowInactive || wasPanelUnfocused;
if (wasWindowInactive) {
win.toFront();
win.requestFocus();
}
requestFocus();
// Right-click context menu
if (SwingUtilities.isRightMouseButton(e)) {
@@ -206,16 +216,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
row = Math.max(0, Math.min(row, displayRows - 1));
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
boolean isSelectableField = false;
if (sb.isFormatted()) {
int faPos = sb.findFieldAttribute(row * displayCols + col);
if (faPos >= 0) {
int fa = sb.getCell(faPos).fa & 0xFF;
isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa);
}
}
if (isGraphic || lightPenMode || isSelectableField) {
if (isGraphic || lightPenMode) {
clearSelection();
} else {
selectionStartRow = row;
@@ -292,7 +293,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
row = Math.max(0, Math.min(row, displayRows - 1));
int clickAddr = row * displayCols + col;
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null;
boolean isGraphic = client.getGocaDecoder() != null &&
(client.getGocaDecoder().isGraphicsCursorActive() ||
(client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()));
if (isGraphic) {
clearSelection();
@@ -307,8 +310,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
int gx = client.getGocaDecoder().getGraphicCursorX();
int gy = client.getGocaDecoder().getGraphicCursorY();
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
client.getInputProcessor().sendGraphicMouseAid(
gx, gy,
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
button,
e.isShiftDown(),
@@ -318,25 +324,57 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
return;
}
// Automatic Light-Pen / Selectable Field detection on mouse click:
boolean wasDrag = (selectionStartRow != -1 && selectionEndRow != -1 &&
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol));
boolean activation = isActivationClick;
isActivationClick = false;
if (wasDrag) {
// Preserve drag/block selection so user can interact with menus or press Ctrl+C
repaint();
return;
}
// Single click (not a drag):
clearSelection();
sb.setCursorAddress(clickAddr);
// If this was an activation click (e.g. clicking to resume focus),
// do not fire field actions or send AIDs
if (activation) {
refreshScreen();
return;
}
// Automatic Light-Pen / Selectable Field detection on mouse click (HoD parity):
if (sb.isFormatted()) {
int faPos = sb.findFieldAttribute(clickAddr);
if (faPos >= 0) {
int fa = sb.getCell(faPos).fa & 0xFF;
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
clearSelection();
if (client.lightPenSelect(clickAddr)) {
refreshScreen();
return;
int size = displayRows * displayCols;
int desPos = (faPos + 1) % size;
char desAscii = (char) sb.getCell(desPos).ucs4;
int desEbcdic = sb.getCell(desPos).ec & 0xFF;
boolean isToggleField = (desEbcdic == 0x6F || desAscii == '?' || desAscii == '\uFF1F' ||
desEbcdic == 0x6E || desAscii == '>' || desAscii == '\uFF1E');
boolean isEnterField = (desEbcdic == 0x50 || desAscii == '&' || desAscii == '\uFF06');
boolean isProtected = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa);
// Only auto-select toggle fields (?/>), enter fields (&), or unprotected space fields
if (lightPenMode || isToggleField || isEnterField || !isProtected) {
if (client.lightPenSelect(clickAddr)) {
refreshScreen();
return;
}
}
}
}
}
if (lightPenMode) {
clearSelection();
sb.setCursorAddress(clickAddr);
boolean result = client.lightPenSelect(clickAddr);
if (result) {
refreshScreen();
@@ -346,12 +384,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
sb.setCursorAddress(clickAddr);
clearSelection();
refreshScreen();
return;
}
refreshScreen();
return;
}
repaint();
}
@@ -304,4 +304,40 @@ public class HostStorageAndProxyTest {
dialog.dispose();
}
}
@Test
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
public void testSavedHostGraphicsModeSerializationRoundtrip() {
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
SavedHost host = new SavedHost(
UUID.randomUUID().toString(),
"Gfx " + gm.name(),
"test.host.com",
23,
4,
24,
80,
"",
false,
"TSO",
false,
false,
true,
gm.name(),
"037",
"NONE",
"",
0,
"",
""
);
String json = HostStorage.serializeHostsJson(List.of(host));
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
assertEquals(1, parsed.size());
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
"ConnectionConfig must reflect GraphicsMode." + gm.name());
}
}
}
@@ -0,0 +1,101 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.HeadlessException;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TerminalPanelSelectionTest {
@Test
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
String headerText = " ISPF PRIMARY OPTION MENU";
for (int i = 0; i < headerText.length(); i++) {
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
sb.getCell(1 + i).ec = (byte) (0x40 + i);
}
panel.setClient(client);
// Cell dimensions in panel
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener[] mouseListeners = panel.getMouseListeners();
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
assertTrue(mouseListeners.length > 0);
assertTrue(motionListeners.length > 0);
MouseListener ml = mouseListeners[0];
MouseMotionListener mml = motionListeners[0];
// 1. Mouse Pressed at cell (row 0, col 2)
int startX = ox + 2 * cellW + cellW / 2;
int startY = oy + cellH / 2;
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, startX, startY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(pressEvent);
// 2. Drag to cell (row 0, col 15)
int dragX = ox + 15 * cellW + cellW / 2;
int dragY = oy + cellH / 2;
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
mml.mouseDragged(dragEvent);
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
// 3. Mouse Released at cell (row 0, col 15)
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(releaseEvent);
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
String selected = panel.getSelectedText();
assertNotNull(selected);
assertTrue(selected.length() > 0, "Selected text must not be empty");
// 4. A subsequent single click without drag should clear the selection and move the cursor
int clickX = ox + 5 * cellW + cellW / 2;
int clickY = oy + cellH / 2;
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(clickPress);
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(clickRelease);
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
}