Rearrange settings, update sample config
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m8s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m38s
Release j3270 / Build & Publish Release (push) Successful in 1m13s

This commit is contained in:
2026-09-15 17:59:26 +00:00
parent 7892229a2c
commit 754b1de7cb
22 changed files with 1530 additions and 237 deletions
+6
View File
@@ -3,6 +3,11 @@
; or: ./run.sh -c j3270.ini ; or: ./run.sh -c j3270.ini
[appearance] [appearance]
; UI font settings (menus, dialogs, status bar)
; uiFontFamily = SansSerif
; uiFontSize = 13
;
; Terminal font settings (3270 screen display)
; fontFamily = IBM 3270 ; fontFamily = IBM 3270
; fontSize = 18 ; fontSize = 18
@@ -10,6 +15,7 @@
; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING ; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING
; autoConnectHost = mainframe.example.com ; autoConnectHost = mainframe.example.com
; autoConnectPort = 23 ; autoConnectPort = 23
; blockPaste = true
[colors] [colors]
; Host colors 0-15 (hex RGB) ; Host colors 0-15 (hex RGB)
@@ -717,6 +717,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return client; return client;
} }
public StatusBar getStatusBar() {
return statusBar;
}
void connect(ConnectionConfig config) { void connect(ConnectionConfig config) {
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
@@ -946,7 +950,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(area); ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp); ThemeManager.styleScrollPane(sp);
@@ -979,7 +983,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(area); ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp); ThemeManager.styleScrollPane(sp);
@@ -45,6 +45,24 @@ public class Settings {
flushPrefs(); flushPrefs();
} }
public static String getUiFontFamily() {
return prefs.get("uiFontFamily", "SansSerif");
}
public static void setUiFontFamily(String family) {
prefs.put("uiFontFamily", (family != null && !family.trim().isEmpty()) ? family : "SansSerif");
flushPrefs();
}
public static int getUiFontSize() {
return prefs.getInt("uiFontSize", 13);
}
public static void setUiFontSize(int size) {
prefs.putInt("uiFontSize", Math.max(8, Math.min(size, 48)));
flushPrefs();
}
private static void flushPrefs() { private static void flushPrefs() {
try { try {
prefs.flush(); prefs.flush();
@@ -390,6 +408,17 @@ public class Settings {
prefs.putBoolean("blockSelectMode", block); prefs.putBoolean("blockSelectMode", block);
} }
// ========== Block paste ==========
public static boolean getBlockPaste() {
return prefs.getBoolean("blockPaste", true);
}
public static void setBlockPaste(boolean block) {
prefs.putBoolean("blockPaste", block);
flushPrefs();
}
// ========== Clipboard & Tabular Paste ========== // ========== Clipboard & Tabular Paste ==========
public static boolean getEnablePasteFromExcel() { public static boolean getEnablePasteFromExcel() {
@@ -541,6 +570,16 @@ public class Settings {
switch (key) { switch (key) {
case "fontFamily": setFontFamily(value); break; case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break; case "fontSize": setFontSize(Integer.parseInt(value)); break;
case "uiFontFamily":
case "uifontfamily":
case "uifont":
case "uiFont":
setUiFontFamily(value);
break;
case "uiFontSize":
case "uifontsize":
setUiFontSize(Integer.parseInt(value));
break;
case "javaUiTheme": case "javaUiTheme":
case "theme": case "theme":
case "uiTheme": case "uiTheme":
@@ -608,6 +647,7 @@ public class Settings {
setDynamicCols(Integer.parseInt(value)); setDynamicCols(Integer.parseInt(value));
break; break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
case "autoReconnect": case "autoReconnect":
case "auto_reconnect": case "auto_reconnect":
case "autoConnectAutoReconnect": case "autoConnectAutoReconnect":
@@ -632,6 +672,9 @@ public class Settings {
case "auto_sys_unlock": case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value)); setAutoSysUnlock(Boolean.parseBoolean(value));
break; break;
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel": case "enablePasteFromExcel":
case "pasteFromExcel": case "pasteFromExcel":
case "excelPaste": case "excelPaste":
@@ -649,6 +692,10 @@ public class Settings {
case "clipboard": case "clipboard":
switch (key) { switch (key) {
case "blockPaste":
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel": case "enablePasteFromExcel":
case "pasteFromExcel": case "pasteFromExcel":
case "excelPaste": case "excelPaste":
@@ -761,6 +808,8 @@ public class Settings {
// [appearance] // [appearance]
w.println("[appearance]"); w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name()); w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("uiFontFamily = " + getUiFontFamily());
w.println("uiFontSize = " + getUiFontSize());
w.println("fontFamily = " + getFontFamily()); w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize()); w.println("fontSize = " + getFontSize());
w.println("crosshairRuler = " + getCrosshairRuler()); w.println("crosshairRuler = " + getCrosshairRuler());
@@ -783,6 +832,7 @@ public class Settings {
w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols()); w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
w.println("blockPaste = " + getBlockPaste());
w.println("autoReconnect = " + getAutoConnectAutoReconnect()); w.println("autoReconnect = " + getAutoConnectAutoReconnect());
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries()); w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
w.println("inputMask = " + getInputMask()); w.println("inputMask = " + getInputMask());
@@ -45,7 +45,7 @@ public class ConnectDialog extends JDialog {
gbc.insets = new Insets(5, 6, 5, 6); gbc.insets = new Insets(5, 6, 5, 6);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Host // Host
gbc.gridx = 0; gbc.gridx = 0;
@@ -82,7 +82,7 @@ public class ConnectDialog extends JDialog {
gbc.weightx = 1.0; gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values()); modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4); modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); modelCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(modelCombo); ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc); mainPanel.add(modelCombo, gbc);
@@ -148,7 +148,7 @@ public class ConnectDialog extends JDialog {
gbc.weightx = 1.0; gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values()); graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode()); graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); graphicsCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(graphicsCombo); ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc); mainPanel.add(graphicsCombo, gbc);
@@ -207,7 +207,7 @@ public class ConnectDialog extends JDialog {
break; break;
} }
} }
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); codePageCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(codePageCombo); ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc); mainPanel.add(codePageCombo, gbc);
@@ -235,7 +235,7 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 8; gbc.gridy = 8;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
ThemeManager.styleCheckBox(verifyCertCheckBox); ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); verifyCertCheckBox.setFont(ThemeManager.getUiFont());
verifyCertCheckBox.setSelected(true); verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false); verifyCertCheckBox.setEnabled(false);
mainPanel.add(verifyCertCheckBox, gbc); mainPanel.add(verifyCertCheckBox, gbc);
@@ -245,7 +245,7 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 9; gbc.gridy = 9;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
ThemeManager.styleCheckBox(tn3270eCheckBox); ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); tn3270eCheckBox.setFont(ThemeManager.getUiFont());
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e()); tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
mainPanel.add(tn3270eCheckBox, gbc); mainPanel.add(tn3270eCheckBox, gbc);
@@ -254,7 +254,7 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 10; gbc.gridy = 10;
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)"); keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
ThemeManager.styleCheckBox(keepAliveCheckBox); ThemeManager.styleCheckBox(keepAliveCheckBox);
keepAliveCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); keepAliveCheckBox.setFont(ThemeManager.getUiFont());
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive()); keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
mainPanel.add(keepAliveCheckBox, gbc); mainPanel.add(keepAliveCheckBox, gbc);
@@ -263,7 +263,7 @@ public class ConnectDialog extends JDialog {
gbc.gridy = 11; gbc.gridy = 11;
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect"); autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
ThemeManager.styleCheckBox(autoReconnectCheckBox); ThemeManager.styleCheckBox(autoReconnectCheckBox);
autoReconnectCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); autoReconnectCheckBox.setFont(ThemeManager.getUiFont());
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect()); autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
mainPanel.add(autoReconnectCheckBox, gbc); mainPanel.add(autoReconnectCheckBox, gbc);
@@ -300,7 +300,7 @@ public class ConnectDialog extends JDialog {
private JTextField createField(int cols) { private JTextField createField(int cols) {
JTextField field = new JTextField(cols); JTextField field = new JTextField(cols);
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field); ThemeManager.styleTextField(field);
return field; return field;
} }
@@ -61,7 +61,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
JPanel topPanel = new JPanel(new BorderLayout()); JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false); topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen"); countLabel = new JLabel("0 fields detected on screen");
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); countLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
topPanel.add(countLabel, BorderLayout.WEST); topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh"); JButton refreshBtn = new JButton("Refresh");
@@ -84,12 +84,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
table = new JTable(tableModel); table = new JTable(tableModel);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
// Column widths // Column widths
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260}; int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
@@ -51,7 +51,7 @@ public class FindDialog extends JDialog {
gbc.insets = new Insets(4, 4, 4, 4); gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Search text // Search text
gbc.gridx = 0; gbc.gridx = 0;
@@ -64,7 +64,7 @@ public class FindDialog extends JDialog {
gbc.weightx = 1.0; gbc.weightx = 1.0;
searchField = new JTextField(20); searchField = new JTextField(20);
searchField.setText(lastSearchText); searchField.setText(lastSearchText);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); searchField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(searchField); ThemeManager.styleTextField(searchField);
formPanel.add(searchField, gbc); formPanel.add(searchField, gbc);
@@ -103,7 +103,7 @@ public class FindDialog extends JDialog {
gbc.gridwidth = 2; gbc.gridwidth = 2;
statusLabel = new JLabel(" "); statusLabel = new JLabel(" ");
statusLabel.setForeground(ThemeManager.getOiaFgAlert()); statusLabel.setForeground(ThemeManager.getOiaFgAlert());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(labelFont);
formPanel.add(statusLabel, gbc); formPanel.add(statusLabel, gbc);
mainPanel.add(formPanel, BorderLayout.CENTER); mainPanel.add(formPanel, BorderLayout.CENTER);
@@ -51,7 +51,7 @@ public class HostDirectoryDialog extends JDialog {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12)); mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font font = ThemeManager.getUiFont();
// Top Query bar // Top Query bar
JPanel topPanel = new JPanel(new GridBagLayout()); JPanel topPanel = new JPanel(new GridBagLayout());
@@ -79,7 +79,7 @@ public class HostDirectoryDialog extends JDialog {
gbc.gridx = 3; gbc.gridx = 3;
gbc.weightx = 1.0; gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); queryField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(queryField); ThemeManager.styleTextField(queryField);
topPanel.add(queryField, gbc); topPanel.add(queryField, gbc);
@@ -97,12 +97,12 @@ public class HostDirectoryDialog extends JDialog {
tableModel = new DefaultTableModel(); tableModel = new DefaultTableModel();
table = new JTable(tableModel); table = new JTable(tableModel);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
table.addMouseListener(new MouseAdapter() { table.addMouseListener(new MouseAdapter() {
@Override @Override
@@ -123,7 +123,7 @@ public class HostDirectoryDialog extends JDialog {
statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(font);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
@@ -72,7 +72,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
gbc.insets = new Insets(3, 4, 3, 4); gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); Font labelFont = ThemeManager.getUiFont();
// Host / Port // Host / Port
gbc.gridx = 0; gbc.gridy = 0; gbc.gridx = 0; gbc.gridy = 0;
@@ -206,7 +206,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool text area // Spool text area
spoolArea = new JTextArea(); spoolArea = new JTextArea();
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); spoolArea.setFont(ThemeManager.getMonospacedUiFont());
spoolArea.setEditable(false); spoolArea.setEditable(false);
ThemeManager.styleTextArea(spoolArea); ThemeManager.styleTextArea(spoolArea);
@@ -257,7 +257,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
private JTextField createField(String text, int cols) { private JTextField createField(String text, int cols) {
JTextField tf = new JTextField(text, cols); JTextField tf = new JTextField(text, cols);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); tf.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(tf); ThemeManager.styleTextField(tf);
return tf; return tf;
} }
@@ -36,7 +36,7 @@ public class ScriptDialog extends JDialog {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Header // Header
JPanel topPanel = new JPanel(new BorderLayout(5, 5)); JPanel topPanel = new JPanel(new BorderLayout(5, 5));
@@ -49,7 +49,7 @@ public class ScriptDialog extends JDialog {
// Script area // Script area
scriptArea = new JTextArea(); scriptArea = new JTextArea();
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); scriptArea.setFont(ThemeManager.getMonospacedUiFont());
scriptArea.setLineWrap(true); scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false); scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea); ThemeManager.styleTextArea(scriptArea);
@@ -75,7 +75,7 @@ public class ScriptDialog extends JDialog {
for (String token : tokens) { for (String token : tokens) {
JButton btn = new JButton(token); JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11)); btn.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -2));
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
btn.setFocusable(false); btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4)); btn.setMargin(new Insets(2, 4, 2, 4));
@@ -95,7 +95,7 @@ public class ScriptDialog extends JDialog {
statusLabel = new JLabel("Ready"); statusLabel = new JLabel("Ready");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
File diff suppressed because it is too large Load Diff
@@ -34,9 +34,9 @@ public class StatusBar extends JPanel {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(ThemeManager.getStatusBarBg()); setBackground(ThemeManager.getStatusBarBg());
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder())); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim()); connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
@@ -143,9 +143,36 @@ public class StatusBar extends JPanel {
} }
setBackground(ThemeManager.getStatusBarBg(theme)); setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme))); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
applyFontRecursively(this, oiaFont);
revalidate();
repaint();
updateStatus(); updateStatus();
} }
private void updateBarHeight(Font font) {
int fontHeight = 16;
try {
FontMetrics fm = getFontMetrics(font);
if (fm != null) fontHeight = fm.getHeight();
} catch (Exception ignored) {
fontHeight = font.getSize() + 4;
}
int barHeight = Math.max(22, fontHeight + 6);
setPreferredSize(new Dimension(800, barHeight));
}
private void applyFontRecursively(Component comp, Font font) {
if (comp instanceof JLabel) {
comp.setFont(font);
} else if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyFontRecursively(child, font);
}
}
}
public void updateStatus() { public void updateStatus() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) { if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::updateStatus); SwingUtilities.invokeLater(this::updateStatus);
@@ -498,11 +498,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int curCol = cols > 0 ? curPos % cols : 0; int curCol = cols > 0 ? curPos % cols : 0;
boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel(); boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel();
boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine(); boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine();
boolean blockPaste = haus.nightmare.j3270.config.Settings.getBlockPaste();
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || text.contains("\t") || text.contains("\n") || text.contains("\r"))) { if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || blockPaste || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected); client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected, blockPaste);
} else if (client.getPS() != null) { } else if (client.getPS() != null) {
if (blockPaste) {
client.getPS().pasteRectangular(text, curRow, curCol);
} else {
client.getPS().pasteString(text, curRow, curCol); client.getPS().pasteString(text, curRow, curCol);
}
} else { } else {
for (char ch : text.toCharArray()) { for (char ch : text.toCharArray()) {
if (ch == '\n' || ch == '\r') { if (ch == '\n' || ch == '\r') {
@@ -577,6 +582,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}); });
popup.add(blockModeItem); popup.add(blockModeItem);
JCheckBoxMenuItem blockPasteItem = new JCheckBoxMenuItem("Block Paste", haus.nightmare.j3270.config.Settings.getBlockPaste());
ThemeManager.styleMenuItem(blockPasteItem);
blockPasteItem.addActionListener(ev -> {
haus.nightmare.j3270.config.Settings.setBlockPaste(blockPasteItem.isSelected());
applyModeSettings();
});
popup.add(blockPasteItem);
popup.show(this, e.getX(), e.getY()); popup.show(this, e.getX(), e.getY());
} }
@@ -1181,10 +1194,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock()); ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
ip.setBlockPaste(haus.nightmare.j3270.config.Settings.getBlockPaste());
} }
if (client.getPS() != null) { if (client.getPS() != null) {
client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel()); client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel());
client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine()); client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine());
client.getPS().setBlockPaste(haus.nightmare.j3270.config.Settings.getBlockPaste());
} }
} }
if (statusBar != null) { if (statusBar != null) {
@@ -20,6 +20,7 @@ import java.awt.event.MouseEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import haus.nightmare.j3270.config.Settings;
/** /**
* Centralized theme manager for j3270 Java desktop UI. * Centralized theme manager for j3270 Java desktop UI.
@@ -33,6 +34,25 @@ public final class ThemeManager {
private ThemeManager() {} private ThemeManager() {}
public static Font getUiFont() {
return getUiFont(Font.PLAIN, 0);
}
public static Font getUiFont(int style, int sizeDelta) {
String family = Settings.getUiFontFamily();
int size = Math.max(8, Settings.getUiFontSize() + sizeDelta);
return new Font(family, style, size);
}
public static Font getMonospacedUiFont() {
return getMonospacedUiFont(Font.PLAIN, 0);
}
public static Font getMonospacedUiFont(int style, int sizeDelta) {
int size = Math.max(8, Settings.getUiFontSize() + sizeDelta);
return new Font(Font.MONOSPACED, style, size);
}
public static UITheme getTheme() { public static UITheme getTheme() {
return currentTheme; return currentTheme;
} }
@@ -433,6 +453,47 @@ public final class ThemeManager {
UIManager.put("TitledBorder.titleColor", fgMain); UIManager.put("TitledBorder.titleColor", fgMain);
UIManager.put("OptionPane.background", bgPanel); UIManager.put("OptionPane.background", bgPanel);
UIManager.put("OptionPane.messageForeground", fgMain); UIManager.put("OptionPane.messageForeground", fgMain);
// UI Font Defaults
Font uiFont = getUiFont();
Font uiFontBold = getUiFont(Font.BOLD, 0);
Font monoUiFont = getMonospacedUiFont();
UIManager.put("Button.font", uiFont);
UIManager.put("ToggleButton.font", uiFont);
UIManager.put("RadioButton.font", uiFont);
UIManager.put("CheckBox.font", uiFont);
UIManager.put("ColorChooser.font", uiFont);
UIManager.put("ComboBox.font", uiFont);
UIManager.put("Label.font", uiFont);
UIManager.put("List.font", uiFont);
UIManager.put("MenuBar.font", uiFont);
UIManager.put("MenuItem.font", uiFont);
UIManager.put("RadioButtonMenuItem.font", uiFont);
UIManager.put("CheckBoxMenuItem.font", uiFont);
UIManager.put("Menu.font", uiFont);
UIManager.put("PopupMenu.font", uiFont);
UIManager.put("OptionPane.font", uiFont);
UIManager.put("OptionPane.messageFont", uiFont);
UIManager.put("OptionPane.buttonFont", uiFont);
UIManager.put("Panel.font", uiFont);
UIManager.put("ProgressBar.font", uiFont);
UIManager.put("ScrollPane.font", uiFont);
UIManager.put("Viewport.font", uiFont);
UIManager.put("TabbedPane.font", uiFont);
UIManager.put("Table.font", uiFont);
UIManager.put("TableHeader.font", uiFontBold);
UIManager.put("TextField.font", uiFont);
UIManager.put("PasswordField.font", uiFont);
UIManager.put("TextArea.font", monoUiFont);
UIManager.put("TextPane.font", uiFont);
UIManager.put("EditorPane.font", uiFont);
UIManager.put("TitledBorder.font", uiFontBold);
UIManager.put("ToolBar.font", uiFont);
UIManager.put("ToolTip.font", uiFont);
UIManager.put("Tree.font", uiFont);
UIManager.put("Spinner.font", uiFont);
UIManager.put("FormattedTextField.font", uiFont);
} }
// ========================================================================= // =========================================================================
@@ -446,6 +507,7 @@ public final class ThemeManager {
public static JButton styleButton(JButton button, ButtonVariant variant) { public static JButton styleButton(JButton button, ButtonVariant variant) {
if (button == null) return null; if (button == null) return null;
button.setUI(new StyledButtonUI(variant)); button.setUI(new StyledButtonUI(variant));
button.setFont(getUiFont());
button.setFocusPainted(false); button.setFocusPainted(false);
button.setOpaque(false); button.setOpaque(false);
button.setContentAreaFilled(false); button.setContentAreaFilled(false);
@@ -465,6 +527,7 @@ public final class ThemeManager {
public static JTextField styleTextField(JTextField field) { public static JTextField styleTextField(JTextField field) {
if (field == null) return null; if (field == null) return null;
field.setFont(getUiFont());
field.setBackground(getBgComponent()); field.setBackground(getBgComponent());
field.setForeground(getFgMain()); field.setForeground(getFgMain());
field.setCaretColor(getFgMain()); field.setCaretColor(getFgMain());
@@ -478,6 +541,7 @@ public final class ThemeManager {
public static JTextArea styleTextArea(JTextArea area) { public static JTextArea styleTextArea(JTextArea area) {
if (area == null) return null; if (area == null) return null;
area.setFont(getMonospacedUiFont());
area.setBackground(getCodeAreaBg()); area.setBackground(getCodeAreaBg());
area.setForeground(getCodeAreaFg()); area.setForeground(getCodeAreaFg());
area.setCaretColor(getFgMain()); area.setCaretColor(getFgMain());
@@ -488,6 +552,7 @@ public final class ThemeManager {
public static JComboBox<?> styleComboBox(JComboBox<?> box) { public static JComboBox<?> styleComboBox(JComboBox<?> box) {
if (box == null) return null; if (box == null) return null;
box.setFont(getUiFont());
box.setBackground(getBgComponent()); box.setBackground(getBgComponent());
box.setForeground(getFgMain()); box.setForeground(getFgMain());
box.setRenderer(new DefaultListCellRenderer() { box.setRenderer(new DefaultListCellRenderer() {
@@ -510,12 +575,14 @@ public final class ThemeManager {
public static JTable styleTable(JTable table) { public static JTable styleTable(JTable table) {
if (table == null) return null; if (table == null) return null;
table.setFont(getUiFont());
table.setBackground(getBgComponent()); table.setBackground(getBgComponent());
table.setForeground(getFgMain()); table.setForeground(getFgMain());
table.setSelectionBackground(getSelectionBg()); table.setSelectionBackground(getSelectionBg());
table.setSelectionForeground(getSelectionFg()); table.setSelectionForeground(getSelectionFg());
table.setGridColor(getTableGrid()); table.setGridColor(getTableGrid());
table.setRowHeight(24); int rowHeight = Math.max(24, getUiFont().getSize() + 10);
table.setRowHeight(rowHeight);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() { table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override @Override
@@ -537,14 +604,14 @@ public final class ThemeManager {
if (header != null) { if (header != null) {
header.setBackground(getTableHeaderBg()); header.setBackground(getTableHeaderBg());
header.setForeground(getTableHeaderFg()); header.setForeground(getTableHeaderFg());
header.setFont(header.getFont().deriveFont(Font.BOLD)); header.setFont(getUiFont(Font.BOLD, 0));
header.setDefaultRenderer(new DefaultTableCellRenderer() { header.setDefaultRenderer(new DefaultTableCellRenderer() {
@Override @Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) { public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col); super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBackground(getTableHeaderBg()); setBackground(getTableHeaderBg());
setForeground(getTableHeaderFg()); setForeground(getTableHeaderFg());
setFont(getFont().deriveFont(Font.BOLD)); setFont(getUiFont(Font.BOLD, 0));
setBorder(BorderFactory.createCompoundBorder( setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()), BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
new EmptyBorder(4, 6, 4, 6))); new EmptyBorder(4, 6, 4, 6)));
@@ -558,6 +625,7 @@ public final class ThemeManager {
public static JTabbedPane styleTabbedPane(JTabbedPane tp) { public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
if (tp == null) return null; if (tp == null) return null;
tp.setUI(new StyledTabbedPaneUI()); tp.setUI(new StyledTabbedPaneUI());
tp.setFont(getUiFont());
tp.setBackground(getBgMain()); tp.setBackground(getBgMain());
tp.setForeground(getFgMain()); tp.setForeground(getFgMain());
return tp; return tp;
@@ -566,6 +634,7 @@ public final class ThemeManager {
public static JMenuBar styleMenuBar(JMenuBar bar) { public static JMenuBar styleMenuBar(JMenuBar bar) {
if (bar == null) return null; if (bar == null) return null;
bar.setUI(new StyledMenuBarUI()); bar.setUI(new StyledMenuBarUI());
bar.setFont(getUiFont());
bar.setBackground(getMenuBarBg()); bar.setBackground(getMenuBarBg());
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder())); bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
return bar; return bar;
@@ -574,6 +643,7 @@ public final class ThemeManager {
public static JMenu styleMenu(JMenu menu) { public static JMenu styleMenu(JMenu menu) {
if (menu == null) return null; if (menu == null) return null;
menu.setUI(new StyledMenuUI()); menu.setUI(new StyledMenuUI());
menu.setFont(getUiFont());
menu.setForeground(getMenuBarFg()); menu.setForeground(getMenuBarFg());
menu.setBackground(getMenuBarBg()); menu.setBackground(getMenuBarBg());
menu.setOpaque(false); menu.setOpaque(false);
@@ -583,6 +653,7 @@ public final class ThemeManager {
public static JMenuItem styleMenuItem(JMenuItem item) { public static JMenuItem styleMenuItem(JMenuItem item) {
if (item == null) return null; if (item == null) return null;
item.setUI(new StyledMenuItemUI()); item.setUI(new StyledMenuItemUI());
item.setFont(getUiFont());
item.setBackground(getMenuPopupBg()); item.setBackground(getMenuPopupBg());
item.setForeground(getMenuItemFg()); item.setForeground(getMenuItemFg());
return item; return item;
@@ -591,6 +662,7 @@ public final class ThemeManager {
public static JPopupMenu stylePopupMenu(JPopupMenu popup) { public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
if (popup == null) return null; if (popup == null) return null;
popup.setUI(new StyledPopupMenuUI()); popup.setUI(new StyledPopupMenuUI());
popup.setFont(getUiFont());
popup.setBackground(getMenuPopupBg()); popup.setBackground(getMenuPopupBg());
popup.setBorder(BorderFactory.createCompoundBorder( popup.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1), new LineBorder(getBorder(), 1),
@@ -610,12 +682,14 @@ public final class ThemeManager {
public static JSpinner styleSpinner(JSpinner sp) { public static JSpinner styleSpinner(JSpinner sp) {
if (sp == null) return null; if (sp == null) return null;
sp.setFont(getUiFont());
sp.setBackground(getBgComponent()); sp.setBackground(getBgComponent());
sp.setForeground(getFgMain()); sp.setForeground(getFgMain());
sp.setBorder(new LineBorder(getBorder(), 1)); sp.setBorder(new LineBorder(getBorder(), 1));
JComponent editor = sp.getEditor(); JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) { if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField(); JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setFont(getUiFont());
tf.setBackground(getBgComponent()); tf.setBackground(getBgComponent());
tf.setForeground(getFgMain()); tf.setForeground(getFgMain());
tf.setCaretColor(getFgMain()); tf.setCaretColor(getFgMain());
@@ -626,6 +700,7 @@ public final class ThemeManager {
public static JCheckBox styleCheckBox(JCheckBox cb) { public static JCheckBox styleCheckBox(JCheckBox cb) {
if (cb == null) return null; if (cb == null) return null;
cb.setFont(getUiFont());
cb.setOpaque(false); cb.setOpaque(false);
cb.setForeground(getFgMain()); cb.setForeground(getFgMain());
cb.setFocusPainted(false); cb.setFocusPainted(false);
@@ -634,6 +709,7 @@ public final class ThemeManager {
public static JRadioButton styleRadioButton(JRadioButton rb) { public static JRadioButton styleRadioButton(JRadioButton rb) {
if (rb == null) return null; if (rb == null) return null;
rb.setFont(getUiFont());
rb.setOpaque(false); rb.setOpaque(false);
rb.setForeground(getFgMain()); rb.setForeground(getFgMain());
rb.setFocusPainted(false); rb.setFocusPainted(false);
@@ -646,7 +722,7 @@ public final class ThemeManager {
title, title,
TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION, TitledBorder.DEFAULT_POSITION,
new Font(Font.SANS_SERIF, Font.BOLD, 12), getUiFont(Font.BOLD, 0),
getFgMain()); getFgMain());
} }
@@ -732,6 +808,7 @@ public final class ThemeManager {
if (comp instanceof JButton) { if (comp instanceof JButton) {
JButton btn = (JButton) comp; JButton btn = (JButton) comp;
btn.setFont(getUiFont());
if (!(btn.getUI() instanceof StyledButtonUI)) { if (!(btn.getUI() instanceof StyledButtonUI)) {
styleButton(btn, ButtonVariant.DEFAULT); styleButton(btn, ButtonVariant.DEFAULT);
} }
@@ -770,6 +847,21 @@ public final class ThemeManager {
if (comp instanceof JLabel) { if (comp instanceof JLabel) {
comp.setForeground(getFgMain(theme)); comp.setForeground(getFgMain(theme));
JLabel label = (JLabel) comp;
Font cur = label.getFont();
int style = (cur != null && cur.isBold()) ? Font.BOLD : Font.PLAIN;
Object deltaProp = label.getClientProperty("uiFontSizeDelta");
int delta = 0;
if (deltaProp instanceof Integer) {
delta = (Integer) deltaProp;
} else {
int baseSize = Settings.getUiFontSize();
if (cur != null && cur.getSize() > baseSize) {
delta = cur.getSize() - baseSize;
}
label.putClientProperty("uiFontSizeDelta", delta);
}
label.setFont(getUiFont(style, delta));
return; return;
} }
@@ -783,6 +875,7 @@ public final class ThemeManager {
if (b instanceof TitledBorder) { if (b instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) b; TitledBorder tb = (TitledBorder) b;
tb.setTitleColor(getFgMain(theme)); tb.setTitleColor(getFgMain(theme));
tb.setTitleFont(getUiFont(Font.BOLD, 0));
tb.setBorder(new LineBorder(getBorder(theme), 1)); tb.setBorder(new LineBorder(getBorder(theme), 1));
} }
} else if (comp instanceof Container) { } else if (comp instanceof Container) {
@@ -32,18 +32,20 @@ public class UntrustedCertificateDialog extends JDialog {
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setOpaque(false); headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️"); JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28)); iconLabel.putClientProperty("uiFontSizeDelta", 15);
iconLabel.setFont(ThemeManager.getUiFont(Font.PLAIN, 15));
headerPanel.add(iconLabel); headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setOpaque(false); titleBox.setOpaque(false);
JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); titleLabel.putClientProperty("uiFontSizeDelta", 3);
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
titleLabel.setForeground(ThemeManager.getOiaFgWarn()); titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel); titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); subtitleLabel.setFont(ThemeManager.getUiFont());
subtitleLabel.setForeground(ThemeManager.getFgMuted()); subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel); titleBox.add(subtitleLabel);
headerPanel.add(titleBox); headerPanel.add(titleBox);
@@ -70,7 +72,7 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString()); JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false); detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); detailsArea.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(detailsArea); ThemeManager.styleTextArea(detailsArea);
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@@ -92,7 +94,7 @@ public class UntrustedCertificateDialog extends JDialog {
JButton trustBtn = new JButton("Connect Anyway"); JButton trustBtn = new JButton("Connect Anyway");
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER); ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); trustBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
trustBtn.addActionListener(e -> { trustBtn.addActionListener(e -> {
accepted = true; accepted = true;
dispose(); dispose();
@@ -20,6 +20,7 @@ public class BehaviorSettingsTest {
private int origReconnectMaxRetries; private int origReconnectMaxRetries;
private boolean origInputMask; private boolean origInputMask;
private String origInputMaskChar; private String origInputMaskChar;
private boolean origBlockPaste;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
@@ -27,6 +28,7 @@ public class BehaviorSettingsTest {
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries(); origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
origInputMask = Settings.getInputMask(); origInputMask = Settings.getInputMask();
origInputMaskChar = Settings.getInputMaskChar(); origInputMaskChar = Settings.getInputMaskChar();
origBlockPaste = Settings.getBlockPaste();
} }
@AfterEach @AfterEach
@@ -35,6 +37,7 @@ public class BehaviorSettingsTest {
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries); Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
Settings.setInputMask(origInputMask); Settings.setInputMask(origInputMask);
Settings.setInputMaskChar(origInputMaskChar); Settings.setInputMaskChar(origInputMaskChar);
Settings.setBlockPaste(origBlockPaste);
} }
@Test @Test
@@ -74,6 +77,39 @@ public class BehaviorSettingsTest {
assertEquals("*", Settings.getInputMaskChar()); assertEquals("*", Settings.getInputMaskChar());
} }
@Test
@DisplayName("Block paste setting defaults to true and binds to SettingsDialog")
public void testBlockPasteSettingAndDialogBinding() throws Exception {
// Default must be true
assertTrue(Settings.getBlockPaste(), "Settings.getBlockPaste() should default to true");
Settings.setBlockPaste(false);
assertFalse(Settings.getBlockPaste());
Settings.setBlockPaste(true);
assertTrue(Settings.getBlockPaste());
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
Field blockPasteField = SettingsDialog.class.getDeclaredField("blockPasteCheck");
blockPasteField.setAccessible(true);
JCheckBox blockPasteCheck = (JCheckBox) blockPasteField.get(dialog);
assertNotNull(blockPasteCheck, "blockPasteCheck must exist in SettingsDialog");
assertEquals("Block paste", blockPasteCheck.getText());
assertEquals(Settings.getBlockPaste(), blockPasteCheck.isSelected());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test @Test
@DisplayName("INI export and load preserves autoReconnect and inputMask") @DisplayName("INI export and load preserves autoReconnect and inputMask")
public void testIniExportAndLoad() throws Exception { public void testIniExportAndLoad() throws Exception {
@@ -173,4 +209,90 @@ public class BehaviorSettingsTest {
} catch (HeadlessException ignored) { } catch (HeadlessException ignored) {
} }
} }
@Test
@DisplayName("SettingsDialog tabs are organized into Appearance, Behavior, Connection, and Entry Assist")
public void testSettingsDialogReorganizedTabsAndGrouping() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Find top-level JTabbedPane
JTabbedPane mainTabs = null;
for (Component c : dialog.getContentPane().getComponents()) {
if (c instanceof JTabbedPane) {
mainTabs = (JTabbedPane) c;
break;
}
}
assertNotNull(mainTabs, "Main JTabbedPane must exist");
assertEquals(4, mainTabs.getTabCount(), "Must have 4 main tabs");
assertEquals("Appearance", mainTabs.getTitleAt(0));
assertEquals("Behavior", mainTabs.getTitleAt(1));
assertEquals("Connection", mainTabs.getTitleAt(2));
assertEquals("Entry Assist", mainTabs.getTitleAt(3));
// Verify Appearance subtabs: Display & Font, Colors
Component appComp = mainTabs.getComponentAt(0);
assertTrue(appComp instanceof JPanel);
JTabbedPane appTabs = findTabbedPane((JPanel) appComp);
assertNotNull(appTabs, "Appearance panel must have subtabs");
assertEquals(2, appTabs.getTabCount());
assertEquals("Display & Font", appTabs.getTitleAt(0));
assertEquals("Colors", appTabs.getTitleAt(1));
// Verify Behavior subtabs: General, Keymap
Component behComp = mainTabs.getComponentAt(1);
assertTrue(behComp instanceof JPanel);
JTabbedPane behTabs = findTabbedPane((JPanel) behComp);
assertNotNull(behTabs, "Behavior panel must have subtabs");
assertEquals(2, behTabs.getTabCount());
assertEquals("General", behTabs.getTitleAt(0));
assertEquals("Keymap", behTabs.getTitleAt(1));
// Verify controls exist in SettingsDialog
Field cursorStyleField = SettingsDialog.class.getDeclaredField("cursorStyleBox");
cursorStyleField.setAccessible(true);
assertNotNull(cursorStyleField.get(dialog), "cursorStyleBox must exist");
Field crosshairField = SettingsDialog.class.getDeclaredField("crosshairRulerCheck");
crosshairField.setAccessible(true);
assertNotNull(crosshairField.get(dialog), "crosshairRulerCheck must exist");
Field fourColorField = SettingsDialog.class.getDeclaredField("fourColorOverrideCheck");
fourColorField.setAccessible(true);
assertNotNull(fourColorField.get(dialog), "fourColorOverrideCheck must exist");
Field numericLockField = SettingsDialog.class.getDeclaredField("numericFieldLockCheck");
numericLockField.setAccessible(true);
assertNotNull(numericLockField.get(dialog), "numericFieldLockCheck must exist");
Field autoSkipField = SettingsDialog.class.getDeclaredField("autoSkipCheck");
autoSkipField.setAccessible(true);
assertNotNull(autoSkipField.get(dialog), "autoSkipCheck must exist");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
private static JTabbedPane findTabbedPane(Container container) {
for (Component c : container.getComponents()) {
if (c instanceof JTabbedPane) {
return (JTabbedPane) c;
}
if (c instanceof Container) {
JTabbedPane nested = findTabbedPane((Container) c);
if (nested != null) return nested;
}
}
return null;
}
} }
@@ -15,17 +15,23 @@ public class FontSettingsPersistenceTest {
private int originalFontSize; private int originalFontSize;
private String originalFontFamily; private String originalFontFamily;
private int originalUiFontSize;
private String originalUiFontFamily;
@BeforeEach @BeforeEach
public void setup() { public void setup() {
originalFontSize = Settings.getFontSize(); originalFontSize = Settings.getFontSize();
originalFontFamily = Settings.getFontFamily(); originalFontFamily = Settings.getFontFamily();
originalUiFontSize = Settings.getUiFontSize();
originalUiFontFamily = Settings.getUiFontFamily();
} }
@AfterEach @AfterEach
public void tearDown() { public void tearDown() {
Settings.setFontSize(originalFontSize); Settings.setFontSize(originalFontSize);
Settings.setFontFamily(originalFontFamily); Settings.setFontFamily(originalFontFamily);
Settings.setUiFontSize(originalUiFontSize);
Settings.setUiFontFamily(originalUiFontFamily);
} }
@Test @Test
@@ -39,6 +45,76 @@ public class FontSettingsPersistenceTest {
assertEquals(18, Settings.getFontSize()); assertEquals(18, Settings.getFontSize());
} }
@Test
public void testUiFontSettingsIndependence() {
// Configure terminal fonts
Settings.setFontFamily("Courier");
Settings.setFontSize(20);
// Configure UI fonts
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(15);
// Verify independent getters
assertEquals("Courier", Settings.getFontFamily());
assertEquals(20, Settings.getFontSize());
assertEquals("Dialog", Settings.getUiFontFamily());
assertEquals(15, Settings.getUiFontSize());
// Change UI font and ensure terminal font is untouched
Settings.setUiFontSize(18);
Settings.setUiFontFamily("SansSerif");
assertEquals("Courier", Settings.getFontFamily(), "Terminal font family must not change when UI font changes");
assertEquals(20, Settings.getFontSize(), "Terminal font size must not change when UI font changes");
assertEquals("SansSerif", Settings.getUiFontFamily());
assertEquals(18, Settings.getUiFontSize());
// Change terminal font and ensure UI font is untouched
Settings.setFontSize(24);
Settings.setFontFamily("IBM 3270");
assertEquals("SansSerif", Settings.getUiFontFamily(), "UI font family must not change when terminal font changes");
assertEquals(18, Settings.getUiFontSize(), "UI font size must not change when terminal font changes");
assertEquals("IBM 3270", Settings.getFontFamily());
assertEquals(24, Settings.getFontSize());
}
@Test
public void testUiFontIniImportExport() throws Exception {
java.io.File tempIni = java.io.File.createTempFile("j3270_ui_font_test", ".ini");
tempIni.deleteOnExit();
try {
try (java.io.PrintWriter pw = new java.io.PrintWriter(tempIni)) {
pw.println("[appearance]");
pw.println("uiFontFamily = Dialog");
pw.println("uiFontSize = 16");
pw.println("fontFamily = Monospaced");
pw.println("fontSize = 22");
}
Settings.loadFromIniFile(tempIni.getAbsolutePath());
assertEquals("Dialog", Settings.getUiFontFamily());
assertEquals(16, Settings.getUiFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
assertEquals(22, Settings.getFontSize());
// Export to another file and check contents
java.io.File exportIni = java.io.File.createTempFile("j3270_ui_font_export", ".ini");
exportIni.deleteOnExit();
Settings.exportToIniFile(exportIni.getAbsolutePath());
String exported = new String(java.nio.file.Files.readAllBytes(exportIni.toPath()));
assertTrue(exported.contains("uiFontFamily = Dialog"), "Exported INI must contain uiFontFamily");
assertTrue(exported.contains("uiFontSize = 16"), "Exported INI must contain uiFontSize");
assertTrue(exported.contains("fontFamily = Monospaced"), "Exported INI must contain terminal fontFamily");
assertTrue(exported.contains("fontSize = 22"), "Exported INI must contain terminal fontSize");
} finally {
tempIni.delete();
}
}
@Test @Test
public void testTerminalPanelResizeDoesNotResetFontSize() { public void testTerminalPanelResizeDoesNotResetFontSize() {
try { try {
@@ -77,4 +153,57 @@ public class FontSettingsPersistenceTest {
// In headless environment without display, Settings persistence is verified above // In headless environment without display, Settings persistence is verified above
} }
} }
@Test
public void testSettingsDialogApplyDoesNotIncrementFontSize() throws Exception {
try {
Settings.setUiFontSize(16);
Settings.setFontSize(22);
Settings.setUiFontFamily("Dialog");
Settings.setFontFamily("Monospaced");
ThemeManager.applyUIManagerDefaults(ThemeManager.getTheme());
SettingsDialog dialog = new SettingsDialog(null);
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
applyMethod.setAccessible(true);
// Find all JLabels in SettingsDialog
java.util.List<javax.swing.JLabel> labels = new java.util.ArrayList<>();
findLabelsRecursively(dialog.getContentPane(), labels);
assertFalse(labels.isEmpty(), "SettingsDialog must contain labels");
for (javax.swing.JLabel label : labels) {
assertEquals(16, label.getFont().getSize(),
"Label '" + label.getText() + "' must initially have size 16");
}
// Click Apply multiple times
for (int click = 1; click <= 5; click++) {
applyMethod.invoke(dialog);
assertEquals(16, Settings.getUiFontSize(), "Settings uiFontSize must stay 16");
assertEquals(22, Settings.getFontSize(), "Settings fontSize must stay 22");
for (javax.swing.JLabel label : labels) {
assertEquals(16, label.getFont().getSize(),
"Label '" + label.getText() + "' must remain at size 16 after click #" + click);
}
}
dialog.dispose();
} catch (HeadlessException e) {
// Ignored in headless environment if dialog creation fails
}
}
private void findLabelsRecursively(java.awt.Container container, java.util.List<javax.swing.JLabel> labels) {
for (java.awt.Component c : container.getComponents()) {
if (c instanceof javax.swing.JLabel) {
labels.add((javax.swing.JLabel) c);
} else if (c instanceof java.awt.Container) {
findLabelsRecursively((java.awt.Container) c, labels);
}
}
}
} }
@@ -0,0 +1,157 @@
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.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
public class ImportConfigTest {
private String origFontFamily;
private int origFontSize;
private String origHost;
private int origPort;
private boolean origAutoReconnect;
@BeforeEach
public void setUp() {
origFontFamily = Settings.getFontFamily();
origFontSize = Settings.getFontSize();
origHost = Settings.getAutoConnectHost();
origPort = Settings.getAutoConnectPort();
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
}
@AfterEach
public void tearDown() {
Settings.setFontFamily(origFontFamily);
Settings.setFontSize(origFontSize);
Settings.setAutoConnectHost(origHost);
Settings.setAutoConnectPort(origPort);
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
}
@Test
@DisplayName("SettingsDialog contains Import Config button positioned to the left of Export Config")
public void testImportButtonPlacementAndProperties() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JButton btnImport = dialog.getBtnImport();
JButton btnExport = dialog.getBtnExport();
assertNotNull(btnImport, "Import Config button must exist");
assertNotNull(btnExport, "Export Config button must exist");
assertEquals("Import Config...", btnImport.getText());
assertEquals("Export Config...", btnExport.getText());
// Verify btnImport is in the same container and positioned before (to the left of) btnExport
Container parent = btnImport.getParent();
assertNotNull(parent, "Import button must have a parent container");
assertSame(parent, btnExport.getParent(), "Import and Export buttons must share the same parent panel");
Component[] comps = parent.getComponents();
int importIdx = -1;
int exportIdx = -1;
for (int i = 0; i < comps.length; i++) {
if (comps[i] == btnImport) importIdx = i;
if (comps[i] == btnExport) exportIdx = i;
}
assertTrue(importIdx >= 0, "btnImport must be in parent container");
assertTrue(exportIdx >= 0, "btnExport must be in parent container");
assertTrue(importIdx < exportIdx, "btnImport must be placed to the left of btnExport (index " + importIdx + " < " + exportIdx + ")");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("importConfigFile updates Settings and refreshes SettingsDialog controls")
public void testImportConfigFileUpdatesSettingsAndDialog() throws IOException {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
File tempIni = File.createTempFile("j3270_test_import", ".ini");
try {
try (FileWriter writer = new FileWriter(tempIni)) {
writer.write("[appearance]\n");
writer.write("fontSize = 24\n");
writer.write("fontFamily = Monospaced\n");
writer.write("\n[behavior]\n");
writer.write("startupBehavior = AUTO_CONNECT\n");
writer.write("autoConnectHost = testhost.example.org\n");
writer.write("autoConnectPort = 2323\n");
writer.write("autoConnectAutoReconnect = true\n");
}
SettingsDialog dialog = new SettingsDialog(app);
dialog.importConfigFile(tempIni);
// Verify Settings updated
assertEquals(24, Settings.getFontSize());
assertEquals("testhost.example.org", Settings.getAutoConnectHost());
assertEquals(2323, Settings.getAutoConnectPort());
assertTrue(Settings.getAutoConnectAutoReconnect());
// Verify dialog state reflects the imported values
assertEquals(Settings.StartupBehavior.AUTO_CONNECT, Settings.getStartupBehavior());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
} finally {
if (tempIni.exists()) {
tempIni.delete();
}
}
}
@Test
@DisplayName("importConfigFile throws FileNotFoundException for nonexistent file")
public void testImportNonExistentFileThrows() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
File nonExistent = new File("/path/to/nonexistent/file_" + System.currentTimeMillis() + ".ini");
assertThrows(FileNotFoundException.class, () -> {
dialog.importConfigFile(nonExistent);
});
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
}
@@ -262,4 +262,81 @@ public class ThemeManagerTest {
// Handled in headless CI // Handled in headless CI
} }
} }
@Test
public void testUiFontRetrievalAndDefaults() {
String prevFamily = Settings.getUiFontFamily();
int prevSize = Settings.getUiFontSize();
try {
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(16);
Font uiFont = ThemeManager.getUiFont();
assertEquals("Dialog", uiFont.getFamily());
assertEquals(16, uiFont.getSize());
assertEquals(Font.PLAIN, uiFont.getStyle());
Font boldOffset = ThemeManager.getUiFont(Font.BOLD, 2);
assertEquals(18, boldOffset.getSize());
assertEquals(Font.BOLD, boldOffset.getStyle());
Font monoFont = ThemeManager.getMonospacedUiFont();
assertEquals(Font.MONOSPACED, monoFont.getName());
assertEquals(16, monoFont.getSize());
// Test applyUIManagerDefaults sets font properties
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
assertEquals(uiFont, UIManager.get("Button.font"));
assertEquals(uiFont, UIManager.get("Label.font"));
assertEquals(uiFont, UIManager.get("Menu.font"));
assertEquals(uiFont, UIManager.get("Table.font"));
assertEquals(monoFont, UIManager.get("TextArea.font"));
} finally {
Settings.setUiFontFamily(prevFamily);
Settings.setUiFontSize(prevSize);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
}
}
@Test
public void testLabelFontNotIncrementedOnRepeatedThemeApplication() {
String prevFamily = Settings.getUiFontFamily();
int prevSize = Settings.getUiFontSize();
try {
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(16);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
JLabel label = new JLabel("Settings Label");
ThemeManager.applyTheme(label);
assertEquals(16, label.getFont().getSize(), "Initial label font size must match configured uiFontSize");
// Repeated theme applications (e.g. subsequent clicks of 'Apply' in Settings dialog)
// must never increment font size
for (int i = 1; i <= 5; i++) {
ThemeManager.applyTheme(label);
assertEquals(16, label.getFont().getSize(),
"Label font size must remain 16 on subsequent application #" + i);
}
// Test label with custom delta
JLabel headerLabel = new JLabel("Header Label");
headerLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
headerLabel.putClientProperty("uiFontSizeDelta", 3);
ThemeManager.applyTheme(headerLabel);
assertEquals(19, headerLabel.getFont().getSize(), "Custom header label should have base + 3 size");
for (int i = 1; i <= 5; i++) {
ThemeManager.applyTheme(headerLabel);
assertEquals(19, headerLabel.getFont().getSize(),
"Header label font size must remain 19 on subsequent application #" + i);
}
} finally {
Settings.setUiFontFamily(prevFamily);
Settings.setUiFontSize(prevSize);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
}
}
} }
@@ -546,6 +546,29 @@ public class ECLPS implements ECLConstants {
private boolean enablePasteFromExcel = true; private boolean enablePasteFromExcel = true;
private boolean pasteStopAtProtectedLine = false; private boolean pasteStopAtProtectedLine = false;
private boolean blockPaste = true;
public boolean isBlockPaste() {
if (session != null && session.getProperties() != null) {
String p = session.getProperties().getProperty(ECLSession.BLOCK_PASTE);
if (p != null) return Boolean.parseBoolean(p);
}
return blockPaste;
}
public boolean IsBlockPaste() { return isBlockPaste(); }
public void setBlockPaste(boolean val) {
this.blockPaste = val;
if (session != null && session.getProperties() != null) {
session.getProperties().setProperty(ECLSession.BLOCK_PASTE, String.valueOf(val));
}
if (inputProcessor != null) {
inputProcessor.setBlockPaste(val);
}
}
public void SetBlockPaste(boolean val) { setBlockPaste(val); }
public boolean isEnablePasteFromExcel() { public boolean isEnablePasteFromExcel() {
if (session != null && session.getProperties() != null) { if (session != null && session.getProperties() != null) {
@@ -591,7 +614,7 @@ public class ECLPS implements ECLConstants {
setCursorPos(row, col); setCursorPos(row, col);
} }
if (inputProcessor != null) { if (inputProcessor != null) {
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine()); return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine(), isBlockPaste());
} }
return 0; return 0;
} }
@@ -605,9 +628,9 @@ public class ECLPS implements ECLConstants {
*/ */
public synchronized int pasteString(String text, int row, int col) { public synchronized int pasteString(String text, int row, int col) {
if (text == null || text.isEmpty() || screen == null) return 0; if (text == null || text.isEmpty() || screen == null) return 0;
if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine())) { if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine() || isBlockPaste())) {
setCursorPos(row, col); setCursorPos(row, col);
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine()); return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste());
} }
int rows = screen.getRows(); int rows = screen.getRows();
int cols = screen.getCols(); int cols = screen.getCols();
@@ -38,6 +38,7 @@ public class ECLSession {
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel"; public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions"; public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine"; public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
public static final String BLOCK_PASTE = "blockPaste";
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap"; public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap"; public static final String PASTE_LINE_WRAP = "pasteLineWrap";
public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode"; public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode";
@@ -137,6 +137,10 @@ public class InputProcessor {
private boolean aplKeyboardMode = false; private boolean aplKeyboardMode = false;
private boolean numericFieldLock = true; private boolean numericFieldLock = true;
private boolean autoSkipEnabled = true; private boolean autoSkipEnabled = true;
private boolean blockPaste = true;
public boolean isBlockPaste() { return blockPaste; }
public void setBlockPaste(boolean val) { this.blockPaste = val; }
public void setBellListener(BellListener listener) { this.bellListener = listener; } public void setBellListener(BellListener listener) { this.bellListener = listener; }
public BellListener getBellListener() { return bellListener; } public BellListener getBellListener() { return bellListener; }
@@ -446,6 +450,19 @@ public class InputProcessor {
* @return number of characters pasted * @return number of characters pasted
*/ */
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) { public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) {
return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, this.blockPaste);
}
/**
* Pastes text onto the screen with tabular, boundary, and block alignment controls.
*
* @param text text to paste
* @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances
* @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries
* @param blockPaste whether newlines align each line with the initial starting cursor column
* @return number of characters pasted
*/
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine, boolean blockPaste) {
if (text == null || text.isEmpty() || screen == null || keyboardLocked) { if (text == null || text.isEmpty() || screen == null || keyboardLocked) {
return 0; return 0;
} }
@@ -463,6 +480,7 @@ public class InputProcessor {
int count = 0; int count = 0;
int len = text.length(); int len = text.length();
int i = 0; int i = 0;
int startCol = screen.getCursorCol();
while (i < len && !keyboardLocked) { while (i < len && !keyboardLocked) {
char ch = text.charAt(i); char ch = text.charAt(i);
@@ -472,7 +490,34 @@ public class InputProcessor {
if (ch == '\r' && (i + 1) < len && text.charAt(i + 1) == '\n') { if (ch == '\r' && (i + 1) < len && text.charAt(i + 1) == '\n') {
i++; // skip \n of \r\n i++; // skip \n of \r\n
} }
if (enablePasteFromExcel) { if (blockPaste) {
int curRow = screen.getCursorRow();
int nextRow = (curRow + 1) % screen.getRows();
if (pasteStopAtProtectedLine && isLineProtected(nextRow)) {
break; // Stop paste when next line is protected
}
int cols = screen.getCols();
int rows = screen.getRows();
int targetAddr = nextRow * cols + startCol;
if (screen.isFormatted()) {
int size = rows * cols;
ExtendedAttribute ea = screen.getCell(targetAddr);
if (ea.isFieldAttribute()) {
targetAddr = (targetAddr + 1) % size;
}
byte faVal = screen.getFieldAttributeAt(targetAddr);
if (faIsProtected(faVal & 0xFF)) {
if (pasteStopAtProtectedLine) {
break;
} else {
targetAddr = screen.findNextUnprotected(targetAddr == 0 ? (size - 1) : (targetAddr - 1));
}
}
}
screen.setCursorAddress(targetAddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
} else if (enablePasteFromExcel) {
int curRow = screen.getCursorRow(); int curRow = screen.getCursorRow();
int nextRow = (curRow + 1) % screen.getRows(); int nextRow = (curRow + 1) % screen.getRows();
if (pasteStopAtProtectedLine && isLineProtected(nextRow)) { if (pasteStopAtProtectedLine && isLineProtected(nextRow)) {
@@ -0,0 +1,144 @@
package haus.nightmare.lib3270j.input;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the "Block paste" feature.
* Ensures that multi-line pastes maintain column alignment with the initial start column
* when blockPaste is active (the default), and revert to standard 3270 Newline field advance
* when blockPaste is disabled.
*/
public class BlockPasteTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testBlockPasteDefaultsToTrue() {
assertTrue(input.isBlockPaste(), "InputProcessor blockPaste should default to true");
assertTrue(ps.isBlockPaste(), "ECLPS blockPaste should default to true");
}
@Test
public void testBlockPasteAlignsStartColumnInUnformattedScreen() {
screen.erase(false);
// Start cursor at row 0, column 15
int startPos = screen.rowColToAddress(0, 15);
screen.setCursorAddress(startPos);
String multilineText = "FIRST\nSECOND\nTHIRD";
int pasted = input.pasteText(multilineText, true, false, true);
assertEquals(16, pasted);
// Verify row 0, col 15-19 has "FIRST"
assertEquals("FIRST", ps.getString(startPos, 5));
// Verify row 1, col 15-20 has "SECOND"
int row1Col15 = screen.rowColToAddress(1, 15);
assertEquals("SECOND", ps.getString(row1Col15, 6));
// Verify row 2, col 15-19 has "THIRD"
int row2Col15 = screen.rowColToAddress(2, 15);
assertEquals("THIRD", ps.getString(row2Col15, 5));
// Verify start of row 1 (cols 0-5) did NOT receive the paste
int row1Col0 = screen.rowColToAddress(1, 0);
assertNotEquals("SECOND", ps.getString(row1Col0, 6));
}
@Test
public void testBlockPasteAlignsStartColumnInFormattedScreen() {
screen.erase(false);
// Set up 3 rows each with an unprotected field starting at col 0 (FA at 0, 80, 160)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
screen.setFieldAttribute(160, (byte) FA_PRINTABLE);
// Start pasting at Row 0, Col 25
int startPos = screen.rowColToAddress(0, 25);
screen.setCursorAddress(startPos);
String multilineText = "ALPHA\r\nBETA\r\nGAMMA";
int pasted = input.pasteText(multilineText, true, false, true);
assertEquals(14, pasted);
assertEquals("ALPHA", ps.getString(screen.rowColToAddress(0, 25), 5));
assertEquals("BETA", ps.getString(screen.rowColToAddress(1, 25), 4));
assertEquals("GAMMA", ps.getString(screen.rowColToAddress(2, 25), 5));
}
@Test
public void testDisabledBlockPasteRevertsToFirstUnprotectedField() {
screen.erase(false);
// Row 0: FA at 0 (unprotected col 1-79)
// Row 1: FA at 80 (unprotected col 1-79)
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
// Start pasting at Row 0, Col 25
int startPos = screen.rowColToAddress(0, 25);
screen.setCursorAddress(startPos);
// blockPaste = false: newline should advance to first unprotected field on next row (pos 81 / col 1)
String multilineText = "LINE1\nLINE2";
input.pasteText(multilineText, true, false, false);
assertEquals("LINE1", ps.getString(screen.rowColToAddress(0, 25), 5));
// LINE2 must be at row 1, col 1 (pos 81) rather than col 25
assertEquals("LINE2", ps.getString(81, 5));
}
@Test
public void testBlockPasteStopAtProtectedBoundary() {
screen.erase(false);
// Row 0: unprotected field at pos 0
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
// Row 1: entirely protected row
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
int startPos = screen.rowColToAddress(0, 10);
screen.setCursorAddress(startPos);
// pasteStopAtProtectedLine = true
String multilineText = "ROW0\nROW1";
int pasted = input.pasteText(multilineText, true, true, true);
// Should paste "ROW0" (4 chars) and halt when row 1 is protected
assertEquals(4, pasted);
assertEquals("ROW0", ps.getString(startPos, 4));
}
@Test
public void testEclPsPasteStringHonorsBlockPaste() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
ps.setBlockPaste(true);
assertTrue(ps.isBlockPaste());
int pasted = ps.pasteString("DATA1\nDATA2", 0, 12);
assertEquals(10, pasted);
assertEquals("DATA1", ps.getString(screen.rowColToAddress(0, 12), 5));
assertEquals("DATA2", ps.getString(screen.rowColToAddress(1, 12), 5));
}
}