Add bugfixes from discord
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s

This commit is contained in:
2026-09-10 09:24:39 -04:00
parent 59925c2787
commit 46a022d86b
23 changed files with 2075 additions and 202 deletions
@@ -712,6 +712,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return terminalPanel;
}
public Telnet3270Client getClient() {
return client;
}
void connect(ConnectionConfig config) {
lastHost = config.getHost();
lastPort = config.getPort();
@@ -1167,6 +1171,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
@@ -141,6 +141,14 @@ public class Settings {
flushPrefs();
}
public static boolean getAutoReconnect() {
return getAutoConnectAutoReconnect();
}
public static void setAutoReconnect(boolean autoReconnect) {
setAutoConnectAutoReconnect(autoReconnect);
}
public static int getAutoConnectReconnectMaxRetries() {
return prefs.getInt("autoConnectReconnectMaxRetries", 5);
}
@@ -150,6 +158,32 @@ public class Settings {
flushPrefs();
}
public static boolean getInputMask() {
return prefs.getBoolean("inputMask", true);
}
public static void setInputMask(boolean mask) {
prefs.putBoolean("inputMask", mask);
flushPrefs();
}
public static boolean getInputMaskEnabled() {
return getInputMask();
}
public static void setInputMaskEnabled(boolean enabled) {
setInputMask(enabled);
}
public static String getInputMaskChar() {
return prefs.get("inputMaskChar", "*");
}
public static void setInputMaskChar(String ch) {
prefs.put("inputMaskChar", (ch != null && !ch.trim().isEmpty()) ? ch.trim().substring(0, 1) : "*");
flushPrefs();
}
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
@@ -574,6 +608,26 @@ public class Settings {
setDynamicCols(Integer.parseInt(value));
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "autoReconnect":
case "auto_reconnect":
case "autoConnectAutoReconnect":
setAutoConnectAutoReconnect(Boolean.parseBoolean(value));
break;
case "reconnectMaxRetries":
case "autoConnectReconnectMaxRetries":
setAutoConnectReconnectMaxRetries(Integer.parseInt(value));
break;
case "inputMask":
case "input_mask":
case "inputMaskEnabled":
case "maskInput":
setInputMask(Boolean.parseBoolean(value));
break;
case "inputMaskChar":
case "input_mask_char":
case "maskChar":
setInputMaskChar(value);
break;
case "autoSysUnlock":
case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value));
@@ -729,6 +783,10 @@ public class Settings {
w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode());
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
w.println("inputMask = " + getInputMask());
w.println("inputMaskChar = " + getInputMaskChar());
w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
@@ -31,6 +31,9 @@ public class SettingsDialog extends JDialog {
private JPanel autoConnectPanel;
private JTextField hostField;
private JTextField portField;
private JCheckBox autoReconnectCheck;
private JCheckBox inputMaskCheck;
private JTextField inputMaskCharField;
private JCheckBox blockSelectCheck;
private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner;
@@ -268,14 +271,45 @@ public class SettingsDialog extends JDialog {
});
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
// Block select mode checkbox
// Auto-Reconnect checkbox
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridwidth = 2;
autoReconnectCheck = new JCheckBox("Auto-Reconnect on Disconnect", Settings.getAutoConnectAutoReconnect());
ThemeManager.styleCheckBox(autoReconnectCheck);
panel.add(autoReconnectCheck, gbc);
// Input Mask feature control
gbc.gridy = 3;
gbc.gridwidth = 2;
JPanel inputMaskPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
inputMaskPanel.setOpaque(false);
inputMaskCheck = new JCheckBox("Enable Input Mask (Password Masking)", Settings.getInputMask());
ThemeManager.styleCheckBox(inputMaskCheck);
inputMaskPanel.add(inputMaskCheck);
JLabel maskCharLabel = new JLabel("Mask Character:");
inputMaskCharField = new JTextField(Settings.getInputMaskChar(), 2);
ThemeManager.styleTextField(inputMaskCharField);
inputMaskCharField.setEnabled(inputMaskCheck.isSelected());
maskCharLabel.setEnabled(inputMaskCheck.isSelected());
inputMaskCheck.addActionListener(e -> {
boolean sel = inputMaskCheck.isSelected();
inputMaskCharField.setEnabled(sel);
maskCharLabel.setEnabled(sel);
});
inputMaskPanel.add(maskCharLabel);
inputMaskPanel.add(inputMaskCharField);
panel.add(inputMaskPanel, gbc);
// Block select mode checkbox
gbc.gridy = 4;
gbc.gridwidth = 2;
blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode());
panel.add(blockSelectCheck, gbc);
// Default Dynamic Screen Size
gbc.gridy = 3;
gbc.gridy = 5;
gbc.gridwidth = 1;
gbc.gridx = 0;
panel.add(new JLabel("Default Dynamic Screen:"), gbc);
@@ -296,16 +330,16 @@ public class SettingsDialog extends JDialog {
// Clipboard & Tabular Paste options
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridy = 6;
gbc.gridwidth = 2;
enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel());
panel.add(enablePasteFromExcelCheck, gbc);
gbc.gridy = 5;
gbc.gridy = 7;
pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine());
panel.add(pasteStopAtProtectedCheck, gbc);
gbc.gridy = 6;
gbc.gridy = 8;
gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc);
@@ -752,6 +786,24 @@ public class SettingsDialog extends JDialog {
}
}
// Auto-Reconnect
if (autoReconnectCheck != null) {
boolean ar = autoReconnectCheck.isSelected();
Settings.setAutoConnectAutoReconnect(ar);
if (parentApp != null && parentApp.getClient() != null && parentApp.getClient().getConfig() != null) {
parentApp.getClient().getConfig().setAutoReconnect(ar);
}
}
// Input Mask
if (inputMaskCheck != null) {
Settings.setInputMask(inputMaskCheck.isSelected());
}
if (inputMaskCharField != null) {
String charText = inputMaskCharField.getText().trim();
Settings.setInputMaskChar(charText.isEmpty() ? "*" : charText.substring(0, 1));
}
// Block select mode
Settings.setBlockSelectMode(blockSelectCheck.isSelected());
@@ -65,6 +65,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
private boolean crosshairRulerEnabled = false;
private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC)
private boolean inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
private String inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
private boolean textBlinkVisible = true;
private Image wallpaperImage = null;
private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null;
@@ -1239,12 +1241,32 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
applyModeSettings();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
revalidate();
repaint();
}
public boolean isInputMaskEnabled() {
return inputMaskEnabled;
}
public void setInputMaskEnabled(boolean enabled) {
this.inputMaskEnabled = enabled;
repaint();
}
public String getInputMaskChar() {
return inputMaskChar;
}
public void setInputMaskChar(String maskChar) {
this.inputMaskChar = maskChar;
repaint();
}
public void setFontSize(int size) {
currentFontSize = size;
haus.nightmare.j3270.config.Settings.setFontSize(size);
@@ -1527,12 +1549,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
// Password fields
if (faIsZero(currentFA & 0xFF)) {
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? boldTerminalFont : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
g2.drawString("*", x, y + fontAscent);
if (inputMaskEnabled) {
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? boldTerminalFont : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
String mask = (inputMaskChar != null && !inputMaskChar.isEmpty()) ? inputMaskChar : "*";
g2.drawString(mask, x, y + fontAscent);
}
}
if (isCellSelected(row, col)) {
@@ -0,0 +1,176 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.*;
public class BehaviorSettingsTest {
private boolean origAutoReconnect;
private int origReconnectMaxRetries;
private boolean origInputMask;
private String origInputMaskChar;
@BeforeEach
public void setUp() {
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
origInputMask = Settings.getInputMask();
origInputMaskChar = Settings.getInputMaskChar();
}
@AfterEach
public void tearDown() {
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
Settings.setInputMask(origInputMask);
Settings.setInputMaskChar(origInputMaskChar);
}
@Test
@DisplayName("Auto-reconnect settings get/set and alias consistency")
public void testAutoReconnectSettingsPersistence() {
Settings.setAutoConnectAutoReconnect(true);
assertTrue(Settings.getAutoConnectAutoReconnect());
assertTrue(Settings.getAutoReconnect());
Settings.setAutoReconnect(false);
assertFalse(Settings.getAutoConnectAutoReconnect());
assertFalse(Settings.getAutoReconnect());
Settings.setAutoConnectReconnectMaxRetries(8);
assertEquals(8, Settings.getAutoConnectReconnectMaxRetries());
}
@Test
@DisplayName("Input mask settings get/set and defaults")
public void testInputMaskSettingsPersistence() {
Settings.setInputMask(false);
assertFalse(Settings.getInputMask());
assertFalse(Settings.getInputMaskEnabled());
Settings.setInputMaskEnabled(true);
assertTrue(Settings.getInputMask());
assertTrue(Settings.getInputMaskEnabled());
Settings.setInputMaskChar("#");
assertEquals("#", Settings.getInputMaskChar());
Settings.setInputMaskChar("*");
assertEquals("*", Settings.getInputMaskChar());
// Empty string should fall back to '*'
Settings.setInputMaskChar("");
assertEquals("*", Settings.getInputMaskChar());
}
@Test
@DisplayName("INI export and load preserves autoReconnect and inputMask")
public void testIniExportAndLoad() throws Exception {
Settings.setAutoConnectAutoReconnect(true);
Settings.setAutoConnectReconnectMaxRetries(12);
Settings.setInputMask(false);
Settings.setInputMaskChar("@");
File tempFile = File.createTempFile("j3270_behavior_test", ".ini");
tempFile.deleteOnExit();
Settings.exportToIniFile(tempFile.getAbsolutePath());
// Reset to different values
Settings.setAutoConnectAutoReconnect(false);
Settings.setAutoConnectReconnectMaxRetries(3);
Settings.setInputMask(true);
Settings.setInputMaskChar("*");
// Load back from INI
Settings.loadFromIniFile(tempFile.getAbsolutePath());
assertTrue(Settings.getAutoConnectAutoReconnect());
assertEquals(12, Settings.getAutoConnectReconnectMaxRetries());
assertFalse(Settings.getInputMask());
assertEquals("@", Settings.getInputMaskChar());
tempFile.delete();
}
@Test
@DisplayName("TerminalPanel reloads input mask settings correctly")
public void testTerminalPanelInputMaskReload() {
try {
TerminalPanel panel = new TerminalPanel();
Settings.setInputMask(false);
Settings.setInputMaskChar("$");
panel.reloadSettings();
assertFalse(panel.isInputMaskEnabled());
assertEquals("$", panel.getInputMaskChar());
panel.setInputMaskEnabled(true);
assertTrue(panel.isInputMaskEnabled());
panel.setInputMaskChar("#");
assertEquals("#", panel.getInputMaskChar());
} catch (HeadlessException ignored) {
// Safe fallback for headless runner
}
}
@Test
@DisplayName("SettingsDialog contains Auto-Reconnect checkbox and Input Mask controls in Behavior panel")
public void testSettingsDialogBehaviorControls() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// In automated/headless environments, JFrame cannot be initialized
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Access private fields in SettingsDialog to verify component bindings
Field autoRecField = SettingsDialog.class.getDeclaredField("autoReconnectCheck");
autoRecField.setAccessible(true);
JCheckBox autoReconnectCheck = (JCheckBox) autoRecField.get(dialog);
assertNotNull(autoReconnectCheck, "autoReconnectCheck must exist in SettingsDialog");
assertEquals("Auto-Reconnect on Disconnect", autoReconnectCheck.getText());
assertEquals(Settings.getAutoConnectAutoReconnect(), autoReconnectCheck.isSelected());
Field inputMaskCheckField = SettingsDialog.class.getDeclaredField("inputMaskCheck");
inputMaskCheckField.setAccessible(true);
JCheckBox inputMaskCheck = (JCheckBox) inputMaskCheckField.get(dialog);
assertNotNull(inputMaskCheck, "inputMaskCheck must exist in SettingsDialog");
assertEquals(Settings.getInputMask(), inputMaskCheck.isSelected());
Field inputMaskCharField = SettingsDialog.class.getDeclaredField("inputMaskCharField");
inputMaskCharField.setAccessible(true);
JTextField maskCharField = (JTextField) inputMaskCharField.get(dialog);
assertNotNull(maskCharField, "inputMaskCharField must exist in SettingsDialog");
assertEquals(Settings.getInputMaskChar(), maskCharField.getText());
assertEquals(inputMaskCheck.isSelected(), maskCharField.isEnabled());
// Test interaction: unchecking inputMask disables character field
inputMaskCheck.setSelected(false);
for (java.awt.event.ActionListener al : inputMaskCheck.getActionListeners()) {
al.actionPerformed(new java.awt.event.ActionEvent(inputMaskCheck, java.awt.event.ActionEvent.ACTION_PERFORMED, ""));
}
assertFalse(maskCharField.isEnabled());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
}