IBM-DYNAMIC tweaking
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m25s

This commit is contained in:
2026-09-04 20:58:19 -04:00
parent cbb310b889
commit 74e30d1ead
18 changed files with 1374 additions and 86 deletions
@@ -92,6 +92,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private void buildUI() { private void buildUI() {
terminalPanel = new TerminalPanel(); terminalPanel = new TerminalPanel();
statusBar = new StatusBar(); statusBar = new StatusBar();
terminalPanel.setStatusBar(statusBar);
getContentPane().setLayout(new BorderLayout()); getContentPane().setLayout(new BorderLayout());
getContentPane().setBackground(Color.BLACK); getContentPane().setBackground(Color.BLACK);
@@ -259,7 +260,89 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog)); viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
menuBar.add(viewMenu); menuBar.add(viewMenu);
// 4. Actions menu // 4. Modes menu
JMenu modesMenu = createMenu("Modes");
JCheckBoxMenuItem docModeItem = new JCheckBoxMenuItem("Document Mode (DOC)", terminalPanel.isDocMode());
ThemeManager.styleMenuItem(docModeItem);
docModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.ALT_DOWN_MASK));
docModeItem.addActionListener(e -> terminalPanel.toggleDocMode());
modesMenu.add(docModeItem);
JCheckBoxMenuItem wordWrapItem = new JCheckBoxMenuItem("Word Wrap Mode", terminalPanel.isWordWrap());
ThemeManager.styleMenuItem(wordWrapItem);
wordWrapItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.ALT_DOWN_MASK));
wordWrapItem.addActionListener(e -> terminalPanel.toggleWordWrap());
modesMenu.add(wordWrapItem);
JCheckBoxMenuItem aplModeItem = new JCheckBoxMenuItem("APL Keyboard Mode", terminalPanel.isAplMode());
ThemeManager.styleMenuItem(aplModeItem);
aplModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.ALT_DOWN_MASK));
aplModeItem.addActionListener(e -> terminalPanel.toggleAplMode());
modesMenu.add(aplModeItem);
JCheckBoxMenuItem insertModeItem = new JCheckBoxMenuItem("Insert Mode", client != null && client.isInsertMode());
ThemeManager.styleMenuItem(insertModeItem);
insertModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0));
insertModeItem.addActionListener(e -> {
if (client != null) {
client.toggleInsert();
terminalPanel.refreshScreen();
}
});
modesMenu.add(insertModeItem);
modesMenu.addSeparator();
JCheckBoxMenuItem fourColorItem = new JCheckBoxMenuItem("Base 4-Color Override", haus.nightmare.j3270.config.Settings.getFourColorOverride());
ThemeManager.styleMenuItem(fourColorItem);
fourColorItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setFourColorOverride(fourColorItem.isSelected());
terminalPanel.repaint();
});
modesMenu.add(fourColorItem);
JCheckBoxMenuItem numLockItem = new JCheckBoxMenuItem("Numeric Field Lock", haus.nightmare.j3270.config.Settings.getNumericFieldLock());
ThemeManager.styleMenuItem(numLockItem);
numLockItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setNumericFieldLock(numLockItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(numLockItem);
JCheckBoxMenuItem autoSkipItem = new JCheckBoxMenuItem("Auto-Skip Across Fields", haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
ThemeManager.styleMenuItem(autoSkipItem);
autoSkipItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setAutoSkipEnabled(autoSkipItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(autoSkipItem);
JCheckBoxMenuItem insertOffAidItem = new JCheckBoxMenuItem("Reset Insert on AID Key", haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
ThemeManager.styleMenuItem(insertOffAidItem);
insertOffAidItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setInsertOffOnAid(insertOffAidItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(insertOffAidItem);
modesMenu.addMenuListener(new javax.swing.event.MenuListener() {
@Override
public void menuSelected(javax.swing.event.MenuEvent e) {
docModeItem.setSelected(terminalPanel.isDocMode());
wordWrapItem.setSelected(terminalPanel.isWordWrap());
aplModeItem.setSelected(terminalPanel.isAplMode());
insertModeItem.setSelected(client != null && client.isInsertMode());
fourColorItem.setSelected(haus.nightmare.j3270.config.Settings.getFourColorOverride());
numLockItem.setSelected(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
autoSkipItem.setSelected(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
insertOffAidItem.setSelected(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
}
@Override public void menuDeselected(javax.swing.event.MenuEvent e) {}
@Override public void menuCanceled(javax.swing.event.MenuEvent e) {}
});
menuBar.add(modesMenu);
// 5. Actions menu
JMenu actionsMenu = createMenu("Actions"); JMenu actionsMenu = createMenu("Actions");
actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> { actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
@@ -33,6 +33,7 @@ public class Settings {
public static void setFontFamily(String family) { public static void setFontFamily(String family) {
prefs.put("fontFamily", family); prefs.put("fontFamily", family);
flushPrefs();
} }
public static int getFontSize() { public static int getFontSize() {
@@ -41,6 +42,14 @@ public class Settings {
public static void setFontSize(int size) { public static void setFontSize(int size) {
prefs.putInt("fontSize", size); prefs.putInt("fontSize", size);
flushPrefs();
}
private static void flushPrefs() {
try {
prefs.flush();
} catch (Exception ignored) {
}
} }
public static StartupBehavior getStartupBehavior() { public static StartupBehavior getStartupBehavior() {
@@ -269,6 +278,101 @@ public class Settings {
prefs.put("cursorStyle", style != null ? style.toUpperCase() : "BLOCK"); prefs.put("cursorStyle", style != null ? style.toUpperCase() : "BLOCK");
} }
// ========== Entry Assist & Modes ==========
public static boolean getEntryAssistDocMode() {
return prefs.getBoolean("entryAssistDocMode", false);
}
public static void setEntryAssistDocMode(boolean docMode) {
prefs.putBoolean("entryAssistDocMode", docMode);
}
public static boolean getEntryAssistWordWrap() {
return prefs.getBoolean("entryAssistWordWrap", false);
}
public static void setEntryAssistWordWrap(boolean wordWrap) {
prefs.putBoolean("entryAssistWordWrap", wordWrap);
}
public static int getEntryAssistStartCol() {
return prefs.getInt("entryAssistStartCol", 1);
}
public static void setEntryAssistStartCol(int startCol) {
prefs.putInt("entryAssistStartCol", startCol);
}
public static int getEntryAssistEndCol() {
return prefs.getInt("entryAssistEndCol", 80);
}
public static void setEntryAssistEndCol(int endCol) {
prefs.putInt("entryAssistEndCol", endCol);
}
public static boolean getEntryAssistBell() {
return prefs.getBoolean("entryAssistBell", false);
}
public static void setEntryAssistBell(boolean bell) {
prefs.putBoolean("entryAssistBell", bell);
}
public static int getEntryAssistBellCol() {
return prefs.getInt("entryAssistBellCol", 75);
}
public static void setEntryAssistBellCol(int col) {
prefs.putInt("entryAssistBellCol", col);
}
public static String getEntryAssistTabStops() {
return prefs.get("entryAssistTabStops", "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75");
}
public static void setEntryAssistTabStops(String stops) {
prefs.put("entryAssistTabStops", stops != null ? stops : "");
}
public static int[] getEntryAssistTabStopsArray() {
String s = getEntryAssistTabStops();
if (s == null || s.trim().isEmpty()) return new int[0];
String[] parts = s.split(",");
java.util.List<Integer> list = new java.util.ArrayList<>();
for (String p : parts) {
try {
int val = Integer.parseInt(p.trim());
if (val > 0) list.add(val - 1);
} catch (NumberFormatException ignored) {}
}
int[] res = new int[list.size()];
for (int i = 0; i < list.size(); i++) res[i] = list.get(i);
return res;
}
public static boolean getInsertOffOnAid() {
return prefs.getBoolean("insertOffOnAid", true);
}
public static void setInsertOffOnAid(boolean val) {
prefs.putBoolean("insertOffOnAid", val);
}
public static boolean getFourColorOverride() {
return prefs.getBoolean("fourColorOverride", false);
}
public static void setFourColorOverride(boolean val) {
prefs.putBoolean("fourColorOverride", val);
}
public static boolean getNumericFieldLock() {
return prefs.getBoolean("numericFieldLock", true);
}
public static void setNumericFieldLock(boolean val) {
prefs.putBoolean("numericFieldLock", val);
}
public static boolean getAutoSkipEnabled() {
return prefs.getBoolean("autoSkipEnabled", true);
}
public static void setAutoSkipEnabled(boolean val) {
prefs.putBoolean("autoSkipEnabled", val);
}
private static void applyConfigEntry(String section, String key, String value) { private static void applyConfigEntry(String section, String key, String value) {
switch (section) { switch (section) {
case "appearance": case "appearance":
@@ -365,6 +469,55 @@ public class Settings {
} }
break; break;
case "entryassist":
case "modes":
switch (key) {
case "docMode":
case "entryAssistDocMode":
setEntryAssistDocMode(Boolean.parseBoolean(value));
break;
case "wordWrap":
case "entryAssistWordWrap":
setEntryAssistWordWrap(Boolean.parseBoolean(value));
break;
case "startCol":
case "entryAssistStartCol":
setEntryAssistStartCol(Integer.parseInt(value));
break;
case "endCol":
case "entryAssistEndCol":
setEntryAssistEndCol(Integer.parseInt(value));
break;
case "bell":
case "entryAssistBell":
setEntryAssistBell(Boolean.parseBoolean(value));
break;
case "bellCol":
case "entryAssistBellCol":
setEntryAssistBellCol(Integer.parseInt(value));
break;
case "tabStops":
case "entryAssistTabStops":
setEntryAssistTabStops(value);
break;
case "insertOffOnAid":
setInsertOffOnAid(Boolean.parseBoolean(value));
break;
case "fourColorOverride":
setFourColorOverride(Boolean.parseBoolean(value));
break;
case "numericFieldLock":
setNumericFieldLock(Boolean.parseBoolean(value));
break;
case "autoSkip":
case "autoSkipEnabled":
setAutoSkipEnabled(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown entryassist/modes key: " + key);
}
break;
default: default:
// Allow bare keys outside any section — treat as raw prefs // Allow bare keys outside any section — treat as raw prefs
log.fine("Setting raw preference: " + key + " = " + value); log.fine("Setting raw preference: " + key + " = " + value);
@@ -412,6 +565,25 @@ public class Settings {
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
w.println(); w.println();
// [entryassist]
w.println("[entryassist]");
w.println("docMode = " + getEntryAssistDocMode());
w.println("wordWrap = " + getEntryAssistWordWrap());
w.println("startCol = " + getEntryAssistStartCol());
w.println("endCol = " + getEntryAssistEndCol());
w.println("bell = " + getEntryAssistBell());
w.println("bellCol = " + getEntryAssistBellCol());
w.println("tabStops = " + getEntryAssistTabStops());
w.println();
// [modes]
w.println("[modes]");
w.println("insertOffOnAid = " + getInsertOffOnAid());
w.println("fourColorOverride = " + getFourColorOverride());
w.println("numericFieldLock = " + getNumericFieldLock());
w.println("autoSkip = " + getAutoSkipEnabled());
w.println();
// [graphics] // [graphics]
w.println("[graphics]"); w.println("[graphics]");
w.println("graphicsMode = " + getGraphicsMode().name()); w.println("graphicsMode = " + getGraphicsMode().name());
@@ -12,6 +12,50 @@ import java.io.File;
public class FileTransferDialog extends JDialog { public class FileTransferDialog extends JDialog {
public static class TransferSessionState {
public FTConfig.HostType hostType = FTConfig.HostType.TSO;
public boolean isSend = false;
public String localFile = "";
public String hostFile = "";
public boolean isAscii = true;
public int mtu = FTConstants.DFT_BUF;
public boolean crFlag = true;
public boolean remapFlag = true;
public boolean append = false;
public boolean overwrite = false;
public String recfm = "";
public String lrecl = "";
public String blksize = "";
public String space = "";
public String options = "";
}
private static TransferSessionState lastTransferState = null;
public static TransferSessionState getLastTransferState() {
return lastTransferState;
}
public static void setLastTransferState(TransferSessionState state) {
lastTransferState = state;
}
public static void resetSessionState() {
lastTransferState = null;
}
public static String formatHostFilename(String localPath, FTConfig.HostType hostType) {
if (localPath == null || localPath.trim().isEmpty()) {
return "";
}
File f = new File(localPath.trim());
String name = f.getName();
if (hostType == FTConfig.HostType.CMS) {
name = name.replace('.', ' ');
}
return name;
}
private final FileTransfer coordinator; private final FileTransfer coordinator;
private final Frame owner; private final Frame owner;
@@ -66,6 +110,16 @@ public class FileTransferDialog extends JDialog {
// Host Type // Host Type
hostTypeCombo = new JComboBox<>(FTConfig.HostType.values()); hostTypeCombo = new JComboBox<>(FTConfig.HostType.values());
hostTypeCombo.addActionListener(e -> {
updateOptionStates();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (hostType == FTConfig.HostType.CMS) {
String hostText = hostFileField.getText().trim();
if (hostText.contains(".")) {
hostFileField.setText(hostText.replace('.', ' '));
}
}
});
formPanel.add(createRow("Host Environment:", hostTypeCombo)); formPanel.add(createRow("Host Environment:", hostTypeCombo));
// Direction // Direction
@@ -88,6 +142,15 @@ public class FileTransferDialog extends JDialog {
// Local File // Local File
localFileField = new JTextField(20); localFileField = new JTextField(20);
localFileField.addFocusListener(new java.awt.event.FocusAdapter() {
@Override
public void focusLost(java.awt.event.FocusEvent e) {
if (hostFileField.getText().trim().isEmpty() && !localFileField.getText().trim().isEmpty()) {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
hostFileField.setText(formatHostFilename(localFileField.getText(), hostType));
}
}
});
browseLocalButton = new JButton("Browse..."); browseLocalButton = new JButton("Browse...");
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
browseLocalButton.addActionListener(e -> browseLocalFile()); browseLocalButton.addActionListener(e -> browseLocalFile());
@@ -211,13 +274,47 @@ public class FileTransferDialog extends JDialog {
setContentPane(mainPanel); setContentPane(mainPanel);
prefillFromLastTransfer();
ThemeManager.applyThemeToWindow(this); ThemeManager.applyThemeToWindow(this);
updateOptionStates(); updateOptionStates();
} }
private void prefillFromLastTransfer() {
if (lastTransferState != null) {
if (lastTransferState.hostType != null) {
hostTypeCombo.setSelectedItem(lastTransferState.hostType);
}
sendRadio.setSelected(lastTransferState.isSend);
receiveRadio.setSelected(!lastTransferState.isSend);
if (lastTransferState.localFile != null) {
localFileField.setText(lastTransferState.localFile);
}
if (lastTransferState.hostFile != null) {
hostFileField.setText(lastTransferState.hostFile);
}
asciiRadio.setSelected(lastTransferState.isAscii);
binaryRadio.setSelected(!lastTransferState.isAscii);
if (lastTransferState.mtu > 0) {
mtuCombo.setSelectedItem(lastTransferState.mtu);
}
crCheck.setSelected(lastTransferState.crFlag);
remapCheck.setSelected(lastTransferState.remapFlag);
appendCheck.setSelected(lastTransferState.append);
overwriteCheck.setSelected(lastTransferState.overwrite);
if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm);
if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl);
if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize);
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
}
}
private void updateOptionStates() { private void updateOptionStates() {
boolean isSend = sendRadio.isSelected(); boolean isSend = sendRadio.isSelected();
boolean isAscii = asciiRadio.isSelected(); boolean isAscii = asciiRadio.isSelected();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
boolean isTso = (hostType == FTConfig.HostType.TSO);
boolean isCms = (hostType == FTConfig.HostType.CMS);
appendCheck.setEnabled(!isSend); appendCheck.setEnabled(!isSend);
overwriteCheck.setEnabled(!isSend); overwriteCheck.setEnabled(!isSend);
@@ -225,10 +322,11 @@ public class FileTransferDialog extends JDialog {
crCheck.setEnabled(isAscii); crCheck.setEnabled(isAscii);
remapCheck.setEnabled(isAscii); remapCheck.setEnabled(isAscii);
recfmField.setEnabled(isSend); recfmField.setEnabled(isSend && isTso);
lreclField.setEnabled(isSend); lreclField.setEnabled(isSend && isTso);
blksizeField.setEnabled(isSend); blksizeField.setEnabled(isSend && isTso);
spaceField.setEnabled(isSend); spaceField.setEnabled(isSend && isTso);
optionsField.setEnabled(isCms);
} }
private JPanel createRow(String labelText, Component comp) { private JPanel createRow(String labelText, Component comp) {
@@ -245,9 +343,13 @@ public class FileTransferDialog extends JDialog {
private void browseLocalFile() { private void browseLocalFile() {
JFileChooser chooser = new JFileChooser(); JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
localFileField.setText(chooser.getSelectedFile().getAbsolutePath()); File selected = chooser.getSelectedFile();
if (hostFileField.getText().trim().isEmpty()) { localFileField.setText(selected.getAbsolutePath());
hostFileField.setText(chooser.getSelectedFile().getName()); FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
boolean shouldAutofill = hostFileField.getText().trim().isEmpty() ||
(lastTransferState != null && hostFileField.getText().trim().equals(lastTransferState.hostFile));
if (shouldAutofill) {
hostFileField.setText(formatHostFilename(selected.getName(), hostType));
} }
} }
} }
@@ -260,10 +362,21 @@ public class FileTransferDialog extends JDialog {
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) { if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
hostFileField.setText(dialog.getSelectedHostFile()); hostFileField.setText(dialog.getSelectedHostFile());
if (localFileField.getText().trim().isEmpty()) { boolean shouldAutofill = localFileField.getText().trim().isEmpty() ||
(lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile));
if (shouldAutofill) {
String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", ""); String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", "");
if (hostType == FTConfig.HostType.CMS) {
String[] tokens = cleanName.trim().split("\\s+");
if (tokens.length >= 2) {
cleanName = tokens[0].toLowerCase() + "." + tokens[1].toLowerCase();
} else if (tokens.length == 1) {
cleanName = tokens[0].toLowerCase();
}
} else {
int lastDot = cleanName.lastIndexOf('.'); int lastDot = cleanName.lastIndexOf('.');
if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1); if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1);
}
localFileField.setText(cleanName); localFileField.setText(cleanName);
} }
} }
@@ -298,6 +411,8 @@ public class FileTransferDialog extends JDialog {
config.setSpace(spaceField.getText().trim()); config.setSpace(spaceField.getText().trim());
config.setOptions(optionsField.getText().trim()); config.setOptions(optionsField.getText().trim());
recordSessionTransfer();
String error = coordinator.startTransfer(config); String error = coordinator.startTransfer(config);
if (error != null) { if (error != null) {
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
@@ -306,6 +421,45 @@ public class FileTransferDialog extends JDialog {
} }
} }
public void recordSessionTransfer() {
TransferSessionState state = new TransferSessionState();
state.hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
state.isSend = sendRadio.isSelected();
state.localFile = localFileField.getText().trim();
state.hostFile = hostFileField.getText().trim();
state.isAscii = asciiRadio.isSelected();
Integer mtu = (Integer) mtuCombo.getSelectedItem();
state.mtu = (mtu != null) ? mtu : FTConstants.DFT_BUF;
state.crFlag = crCheck.isSelected();
state.remapFlag = remapCheck.isSelected();
state.append = appendCheck.isSelected();
state.overwrite = overwriteCheck.isSelected();
state.recfm = recfmField.getText().trim();
state.lrecl = lreclField.getText().trim();
state.blksize = blksizeField.getText().trim();
state.space = spaceField.getText().trim();
state.options = optionsField.getText().trim();
lastTransferState = state;
}
public JComboBox<FTConfig.HostType> getHostTypeCombo() { return hostTypeCombo; }
public JRadioButton getSendRadio() { return sendRadio; }
public JRadioButton getReceiveRadio() { return receiveRadio; }
public JTextField getLocalFileField() { return localFileField; }
public JTextField getHostFileField() { return hostFileField; }
public JRadioButton getAsciiRadio() { return asciiRadio; }
public JRadioButton getBinaryRadio() { return binaryRadio; }
public JComboBox<Integer> getMtuCombo() { return mtuCombo; }
public JCheckBox getCrCheck() { return crCheck; }
public JCheckBox getRemapCheck() { return remapCheck; }
public JCheckBox getAppendCheck() { return appendCheck; }
public JCheckBox getOverwriteCheck() { return overwriteCheck; }
public JTextField getRecfmField() { return recfmField; }
public JTextField getLreclField() { return lreclField; }
public JTextField getBlksizeField() { return blksizeField; }
public JTextField getSpaceField() { return spaceField; }
public JTextField getOptionsField() { return optionsField; }
private void applyTheme(Container container) { private void applyTheme(Container container) {
Color fg = new Color(200, 200, 200); Color fg = new Color(200, 200, 200);
Color bg = new Color(40, 40, 40); Color bg = new Color(40, 40, 40);
@@ -34,6 +34,19 @@ public class SettingsDialog extends JDialog {
private JSpinner dynamicRowsSpinner; private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner; private JSpinner dynamicColsSpinner;
// Entry Assist & Modes tab
private JCheckBox docModeCheck;
private JCheckBox wordWrapCheck;
private JSpinner startColSpinner;
private JSpinner endColSpinner;
private JCheckBox bellCheck;
private JSpinner bellColSpinner;
private JTextField tabStopsField;
private JCheckBox insertOffOnAidCheck;
private JCheckBox fourColorOverrideCheck;
private JCheckBox numericFieldLockCheck;
private JCheckBox autoSkipCheck;
// Advanced tab state tracking // Advanced tab state tracking
private final Color[] tempHostColors = new Color[16]; private final Color[] tempHostColors = new Color[16];
private final Map<String, Color> tempMonoColors = new HashMap<>(); private final Map<String, Color> tempMonoColors = new HashMap<>();
@@ -45,7 +58,7 @@ public class SettingsDialog extends JDialog {
this.parentApp = parent; this.parentApp = parent;
initComponents(); initComponents();
setSize(560, 480); setSize(600, 520);
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
} }
@@ -55,6 +68,7 @@ public class SettingsDialog extends JDialog {
tabbedPane.addTab("Appearance", createAppearancePanel()); tabbedPane.addTab("Appearance", createAppearancePanel());
tabbedPane.addTab("Behavior", createBehaviorPanel()); tabbedPane.addTab("Behavior", createBehaviorPanel());
tabbedPane.addTab("Entry Assist & Modes", createEntryAssistPanel());
tabbedPane.addTab("Advanced", createAdvancedPanel()); tabbedPane.addTab("Advanced", createAdvancedPanel());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
@@ -133,11 +147,31 @@ public class SettingsDialog extends JDialog {
panel.add(fontLabel, gbc); panel.add(fontLabel, gbc);
String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
fontBox = new JComboBox<>(fonts); java.util.List<String> fontList = new java.util.ArrayList<>();
// Find default boolean hasMonospaced = false;
String currentFont = Settings.getFontFamily(); String currentFont = Settings.getFontFamily();
for (int i = 0; i < fonts.length; i++) { for (String f : fonts) {
if (fonts[i].equalsIgnoreCase(currentFont)) { if ("Monospaced".equalsIgnoreCase(f)) {
hasMonospaced = true;
}
fontList.add(f);
}
if (!hasMonospaced) {
fontList.add(0, "Monospaced");
}
boolean hasCurrent = false;
for (String f : fontList) {
if (f.equalsIgnoreCase(currentFont)) {
hasCurrent = true;
break;
}
}
if (!hasCurrent && currentFont != null && !currentFont.trim().isEmpty()) {
fontList.add(0, currentFont);
}
fontBox = new JComboBox<>(fontList.toArray(new String[0]));
for (int i = 0; i < fontBox.getItemCount(); i++) {
if (fontBox.getItemAt(i).equalsIgnoreCase(currentFont)) {
fontBox.setSelectedIndex(i); fontBox.setSelectedIndex(i);
break; break;
} }
@@ -267,6 +301,105 @@ public class SettingsDialog extends JDialog {
return panel; return panel;
} }
private JPanel createEntryAssistPanel() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// Group 1: Entry Assist / Document Mode
JPanel eaGroup = new JPanel(new GridBagLayout());
eaGroup.setBorder(BorderFactory.createTitledBorder("Entry Assist (Document Mode)"));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 6, 4, 6);
gbc.anchor = GridBagConstraints.WEST;
docModeCheck = new JCheckBox("Enable Document Mode (DOC)", Settings.getEntryAssistDocMode());
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 2;
eaGroup.add(docModeCheck, gbc);
wordWrapCheck = new JCheckBox("Enable Word Wrap (V)", Settings.getEntryAssistWordWrap());
gbc.gridy = 1;
eaGroup.add(wordWrapCheck, gbc);
// Margins
gbc.gridy = 2;
gbc.gridwidth = 1;
eaGroup.add(new JLabel("Margins:"), gbc);
JPanel marginPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
marginPanel.setOpaque(false);
marginPanel.add(new JLabel("Left:"));
startColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistStartCol(), 1, 80, 1));
ThemeManager.styleSpinner(startColSpinner);
marginPanel.add(startColSpinner);
marginPanel.add(new JLabel("Right:"));
endColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistEndCol(), 1, 80, 1));
ThemeManager.styleSpinner(endColSpinner);
marginPanel.add(endColSpinner);
gbc.gridx = 1;
eaGroup.add(marginPanel, gbc);
// End-of-Line Bell
gbc.gridx = 0;
gbc.gridy = 3;
bellCheck = new JCheckBox("Audible EOL Bell at Col:", Settings.getEntryAssistBell());
eaGroup.add(bellCheck, gbc);
bellColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistBellCol(), 1, 80, 1));
ThemeManager.styleSpinner(bellColSpinner);
gbc.gridx = 1;
eaGroup.add(bellColSpinner, gbc);
// Tab Stops
gbc.gridx = 0;
gbc.gridy = 4;
eaGroup.add(new JLabel("Tab Stops:"), gbc);
tabStopsField = new JTextField(Settings.getEntryAssistTabStops(), 20);
tabStopsField.setToolTipText("Comma-separated column numbers (1-80, e.g. 1,9,17,25,33,41,49,57,65,73)");
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
eaGroup.add(tabStopsField, gbc);
panel.add(eaGroup);
panel.add(Box.createVerticalStrut(10));
// Group 2: Terminal Operational Modes
JPanel modesGroup = new JPanel(new GridBagLayout());
modesGroup.setBorder(BorderFactory.createTitledBorder("Terminal Operational Modes"));
GridBagConstraints gbcM = new GridBagConstraints();
gbcM.insets = new Insets(4, 6, 4, 6);
gbcM.anchor = GridBagConstraints.WEST;
gbcM.fill = GridBagConstraints.HORIZONTAL;
gbcM.gridx = 0;
gbcM.weightx = 1.0;
insertOffOnAidCheck = new JCheckBox("Reset Insert mode on AID key (Enter, PF, PA, Clear)", Settings.getInsertOffOnAid());
gbcM.gridy = 0;
modesGroup.add(insertOffOnAidCheck, gbcM);
fourColorOverrideCheck = new JCheckBox("Base 4-Color Override mode (3279 green/white/red/turquoise)", Settings.getFourColorOverride());
gbcM.gridy = 1;
modesGroup.add(fourColorOverrideCheck, gbcM);
numericFieldLockCheck = new JCheckBox("Lock keyboard on non-numeric input in numeric fields (-NUMERIC)", Settings.getNumericFieldLock());
gbcM.gridy = 2;
modesGroup.add(numericFieldLockCheck, gbcM);
autoSkipCheck = new JCheckBox("Auto-Skip to next unprotected field when field is filled", Settings.getAutoSkipEnabled());
gbcM.gridy = 3;
modesGroup.add(autoSkipCheck, gbcM);
panel.add(modesGroup);
panel.add(Box.createVerticalGlue());
return panel;
}
private JPanel createAdvancedPanel() { private JPanel createAdvancedPanel() {
JPanel panel = new JPanel(new BorderLayout()); JPanel panel = new JPanel(new BorderLayout());
@@ -585,6 +718,22 @@ public class SettingsDialog extends JDialog {
Settings.setDynamicCols((Integer) dynamicColsSpinner.getValue()); Settings.setDynamicCols((Integer) dynamicColsSpinner.getValue());
} }
// Entry Assist & Modes
if (docModeCheck != null) {
Settings.setEntryAssistDocMode(docModeCheck.isSelected());
Settings.setEntryAssistWordWrap(wordWrapCheck.isSelected());
Settings.setEntryAssistStartCol((Integer) startColSpinner.getValue());
Settings.setEntryAssistEndCol((Integer) endColSpinner.getValue());
Settings.setEntryAssistBell(bellCheck.isSelected());
Settings.setEntryAssistBellCol((Integer) bellColSpinner.getValue());
Settings.setEntryAssistTabStops(tabStopsField.getText().trim());
Settings.setInsertOffOnAid(insertOffOnAidCheck.isSelected());
Settings.setFourColorOverride(fourColorOverrideCheck.isSelected());
Settings.setNumericFieldLock(numericFieldLockCheck.isSelected());
Settings.setAutoSkipEnabled(autoSkipCheck.isSelected());
}
// Propagate visual changes to the app // Propagate visual changes to the app
// Save Colors // Save Colors
for (int i=0; i<16; i++) { for (int i=0; i<16; i++) {
@@ -600,6 +749,9 @@ public class SettingsDialog extends JDialog {
} }
parentApp.getTerminalPanel().reloadSettings(); parentApp.getTerminalPanel().reloadSettings();
if (parentApp != null && (parentApp.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) {
parentApp.getTerminalPanel().guardedPack();
}
ThemeManager.applyThemeToWindow(this); ThemeManager.applyThemeToWindow(this);
return true; return true;
@@ -18,7 +18,11 @@ public class StatusBar extends JPanel {
private final JLabel tlsStatus; private final JLabel tlsStatus;
private final JLabel luName; private final JLabel luName;
private final JLabel lockStatus; private final JLabel lockStatus;
private final JLabel insertStatus;
private final JLabel aplStatus;
private final JLabel fieldTypeStatus; private final JLabel fieldTypeStatus;
private final JLabel docModeStatus;
private final JLabel wordWrapStatus;
private final JLabel codePageInfo; private final JLabel codePageInfo;
private final JLabel modelInfo; private final JLabel modelInfo;
private final JLabel cursorPosition; private final JLabel cursorPosition;
@@ -38,7 +42,35 @@ public class StatusBar extends JPanel {
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
lockStatus = createLabel("", oiaFont, ThemeManager.getOiaInputInhibited()); lockStatus = createLabel("", oiaFont, ThemeManager.getOiaInputInhibited());
insertStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Insert Mode (^ / Insert key) - Click to toggle", () -> {
if (client != null) {
client.toggleInsert();
if (terminalPanel != null) terminalPanel.refreshScreen();
updateStatus();
}
});
aplStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaFgAlert(), "APL Keyboard Mode (Alt+F3) - Click to toggle", () -> {
if (client != null) {
client.toggleAplMode();
if (terminalPanel != null) terminalPanel.refreshScreen();
updateStatus();
}
});
fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
docModeStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Entry Assist Document Mode (Alt+F1) - Click to toggle", () -> {
if (client != null) {
client.toggleDocMode();
if (terminalPanel != null) terminalPanel.refreshScreen();
updateStatus();
}
});
wordWrapStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Entry Assist Word Wrap (Alt+F2) - Click to toggle", () -> {
if (client != null) {
client.toggleWordWrap();
if (terminalPanel != null) terminalPanel.refreshScreen();
updateStatus();
}
});
codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal()); cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
@@ -51,9 +83,17 @@ public class StatusBar extends JPanel {
add(luName); add(luName);
add(Box.createHorizontalStrut(12)); add(Box.createHorizontalStrut(12));
add(lockStatus); add(lockStatus);
add(Box.createHorizontalStrut(8));
add(insertStatus);
add(Box.createHorizontalStrut(8));
add(aplStatus);
add(Box.createHorizontalStrut(10)); add(Box.createHorizontalStrut(10));
add(fieldTypeStatus); add(fieldTypeStatus);
add(Box.createHorizontalGlue()); add(Box.createHorizontalGlue());
add(docModeStatus);
add(Box.createHorizontalStrut(8));
add(wordWrapStatus);
add(Box.createHorizontalStrut(12));
add(codePageInfo); add(codePageInfo);
add(Box.createHorizontalStrut(12)); add(Box.createHorizontalStrut(12));
add(modelInfo); add(modelInfo);
@@ -69,6 +109,23 @@ public class StatusBar extends JPanel {
return label; return label;
} }
private JLabel createClickableLabel(String text, Font font, Color fg, String tooltip, Runnable onClick) {
JLabel label = new JLabel(text);
label.setFont(font);
label.setForeground(fg);
label.setToolTipText(tooltip);
label.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR));
label.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (onClick != null) {
onClick.run();
}
}
});
return label;
}
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) { public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
@@ -90,7 +147,11 @@ public class StatusBar extends JPanel {
luName.setText(""); luName.setText("");
lockStatus.setText(""); lockStatus.setText("");
lockStatus.setForeground(ThemeManager.getOiaInputInhibited(theme)); lockStatus.setForeground(ThemeManager.getOiaInputInhibited(theme));
insertStatus.setText("");
aplStatus.setText("");
fieldTypeStatus.setText(""); fieldTypeStatus.setText("");
docModeStatus.setText("");
wordWrapStatus.setText("");
codePageInfo.setText(""); codePageInfo.setText("");
modelInfo.setText(""); modelInfo.setText("");
cursorPosition.setText("001/001"); cursorPosition.setText("001/001");
@@ -203,13 +264,42 @@ public class StatusBar extends JPanel {
break; break;
} }
lockStatus.setForeground(lockFg); lockStatus.setForeground(lockFg);
} else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT");
lockStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else { } else {
lockStatus.setText(""); lockStatus.setText("");
} }
// Insert Mode Indicator
if (client.getInputProcessor() != null && client.getInputProcessor().isInsertMode()) {
insertStatus.setText("^ INS");
insertStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
insertStatus.setText("");
}
// APL Keyboard Mode Indicator
if (client.getInputProcessor() != null && client.getInputProcessor().isAplKeyboardMode()) {
aplStatus.setText("APL");
aplStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
} else {
aplStatus.setText("");
}
// Entry Assist DOC Mode Indicator
if (client.getScreenBuffer() != null && client.getScreenBuffer().isEntryAssistDOCmode()) {
docModeStatus.setText("DOC");
docModeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
docModeStatus.setText("");
}
// Entry Assist Word Wrap Indicator
if (client.getScreenBuffer() != null && client.getScreenBuffer().isEntryAssistWordWrap()) {
wordWrapStatus.setText("V");
wordWrapStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
wordWrapStatus.setText("");
}
// Field status: Numeric vs Alphanumeric // Field status: Numeric vs Alphanumeric
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) { if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) { if (client.getOIA().isNumeric()) {
@@ -84,6 +84,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private long lastGraphicsUpdateCount = -1; private long lastGraphicsUpdateCount = -1;
private java.awt.image.BufferedImage cachedGraphicsImage = null; private java.awt.image.BufferedImage cachedGraphicsImage = null;
private boolean resizeGuard = false; private boolean resizeGuard = false;
private StatusBar statusBar = null;
/** /**
* Compute the horizontal render offset to center the grid within the panel. * Compute the horizontal render offset to center the grid within the panel.
@@ -331,12 +332,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
addMouseListener(mouseHandler); addMouseListener(mouseHandler);
addMouseMotionListener(mouseHandler); addMouseMotionListener(mouseHandler);
// Handle resize: auto-fit font to window size // Handle resize: keep user configured font persistent without auto-fit reset
addComponentListener(new ComponentAdapter() { addComponentListener(new ComponentAdapter() {
@Override @Override
public void componentResized(ComponentEvent e) { public void componentResized(ComponentEvent e) {
if (resizeGuard) return; if (resizeGuard) return;
autoFitFont(); repaint();
} }
}); });
} }
@@ -564,47 +565,6 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
// ========== Font auto-resize ==========
private void autoFitFont() {
int panelW = getWidth();
int panelH = getHeight();
if (panelW <= 0 || panelH <= 0) return;
int termCols = 80;
int termRows = 24;
if (client != null) {
ScreenBuffer sb = client.getScreenBuffer();
termCols = sb.getDisplayCols();
termRows = sb.getDisplayRows();
}
int availW = panelW - 2 * padding;
int availH = panelH - 2 * padding;
if (availW <= 0 || availH <= 0) return;
int bestSize = 8;
for (int testSize = 8; testSize <= 72; testSize++) {
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
FontMetrics fm = getFontMetrics(testFont);
int testCellW = fm.charWidth('M');
int testCellH = fm.getHeight();
if (testCellW * termCols <= availW && testCellH * termRows <= availH) {
bestSize = testSize;
} else {
break;
}
}
if (bestSize != currentFontSize) {
currentFontSize = bestSize;
haus.nightmare.j3270.config.Settings.setFontSize(bestSize);
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
updateCellSize();
}
repaint();
}
public void guardedPack() { public void guardedPack() {
resizeGuard = true; resizeGuard = true;
@@ -671,6 +631,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S")); bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
bindKeyToMap(im, "CURSEL", haus.nightmare.j3270.config.Settings.getKeyBinding("CURSEL", "alt Q")); bindKeyToMap(im, "CURSEL", haus.nightmare.j3270.config.Settings.getKeyBinding("CURSEL", "alt Q"));
// Operational Mode keybindings
bindKeyToMap(im, "DOCMODE", haus.nightmare.j3270.config.Settings.getKeyBinding("DOCMODE", "alt F1"));
bindKeyToMap(im, "WORDWRAP", haus.nightmare.j3270.config.Settings.getKeyBinding("WORDWRAP", "alt F2"));
bindKeyToMap(im, "APL", haus.nightmare.j3270.config.Settings.getKeyBinding("APL", "alt F3"));
// Copy/Paste/Lightpen bindings // Copy/Paste/Lightpen bindings
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
@@ -683,6 +648,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
am.put("j3270-ESCAPE", createAction(this::handleReset)); am.put("j3270-ESCAPE", createAction(this::handleReset));
am.put("j3270-TAB", createAction(() -> handleTab(false))); am.put("j3270-TAB", createAction(() -> handleTab(false)));
am.put("j3270-shift TAB", createAction(() -> handleTab(true))); am.put("j3270-shift TAB", createAction(() -> handleTab(true)));
am.put("j3270-DOCMODE", createAction(this::toggleDocMode));
am.put("j3270-WORDWRAP", createAction(this::toggleWordWrap));
am.put("j3270-APL", createAction(this::toggleAplMode));
am.put("j3270-UP", createAction(() -> handleCursor("up"))); am.put("j3270-UP", createAction(() -> handleCursor("up")));
am.put("j3270-DOWN", createAction(() -> handleCursor("down"))); am.put("j3270-DOWN", createAction(() -> handleCursor("down")));
am.put("j3270-LEFT", createAction(() -> handleCursor("left"))); am.put("j3270-LEFT", createAction(() -> handleCursor("left")));
@@ -978,9 +946,48 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
var ip = client.getInputProcessor(); var ip = client.getInputProcessor();
ip.setInsertMode(!ip.isInsertMode()); ip.setInsertMode(!ip.isInsertMode());
refreshScreen(); refreshScreen();
if (statusBar != null) statusBar.updateStatus();
} }
} }
public void toggleDocMode() {
if (client != null) {
client.toggleDocMode();
haus.nightmare.j3270.config.Settings.setEntryAssistDocMode(client.isDocMode());
refreshScreen();
if (statusBar != null) statusBar.updateStatus();
}
}
public void toggleWordWrap() {
if (client != null) {
client.toggleWordWrap();
haus.nightmare.j3270.config.Settings.setEntryAssistWordWrap(client.isWordWrap());
refreshScreen();
if (statusBar != null) statusBar.updateStatus();
}
}
public void toggleAplMode() {
if (client != null) {
client.toggleAplMode();
refreshScreen();
if (statusBar != null) statusBar.updateStatus();
}
}
public boolean isDocMode() {
return client != null && client.isDocMode();
}
public boolean isWordWrap() {
return client != null && client.isWordWrap();
}
public boolean isAplMode() {
return client != null && client.isAplMode();
}
private void handleClear() { private void handleClear() {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
if (client.getConnectionState().isNvt()) { if (client.getConnectionState().isNvt()) {
@@ -1046,11 +1053,47 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
public void setStatusBar(StatusBar statusBar) {
this.statusBar = statusBar;
}
public StatusBar getStatusBar() {
return statusBar;
}
public void applyModeSettings() {
if (client != null) {
ScreenBuffer sb = client.getScreenBuffer();
if (sb != null) {
sb.setEntryAssistDOCmode(haus.nightmare.j3270.config.Settings.getEntryAssistDocMode());
sb.setEntryAssistWordWrap(haus.nightmare.j3270.config.Settings.getEntryAssistWordWrap());
sb.setLeftMargin(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistStartCol() - 1));
sb.setRightMargin(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistEndCol() - 1));
sb.setWordTabPositions(haus.nightmare.j3270.config.Settings.getEntryAssistTabStopsArray());
}
haus.nightmare.lib3270j.input.InputProcessor ip = client.getInputProcessor();
if (ip != null) {
ip.setBellEnabled(haus.nightmare.j3270.config.Settings.getEntryAssistBell());
ip.setBellColumn(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistBellCol() - 1));
ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
}
}
if (statusBar != null) {
statusBar.updateStatus();
}
repaint();
}
public void refreshScreen() { public void refreshScreen() {
if (client != null) { if (client != null) {
client.getScreenBuffer().updateDisplaySnapshot(); client.getScreenBuffer().updateDisplaySnapshot();
} }
repaint(); repaint();
if (statusBar != null) {
statusBar.updateStatus();
}
Container parent = getParent(); Container parent = getParent();
while (parent != null) { while (parent != null) {
if (parent instanceof JFrame) { if (parent instanceof JFrame) {
@@ -1088,6 +1131,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
setupColors(); setupColors();
setupKeyBindings(); setupKeyBindings();
setupFont(); setupFont();
applyModeSettings();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode(); blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler(); crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle(); String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
@@ -1137,6 +1181,13 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
if (client != null) { if (client != null) {
setupGraphicsPlaneRenderer(); setupGraphicsPlaneRenderer();
updateCellSize(); updateCellSize();
applyModeSettings();
if (client.getInputProcessor() != null) {
client.getInputProcessor().setBellListener(() -> {
SwingUtilities.invokeLater(() -> Toolkit.getDefaultToolkit().beep());
});
}
client.setNvtClipboardHandler(new haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler() { client.setNvtClipboardHandler(new haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler() {
@Override @Override
@@ -1564,11 +1615,13 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) { private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
if (!haus.nightmare.j3270.config.Settings.getFourColorOverride()) {
int fg = ea.fg != 0 ? (ea.fg & 0xFF) int fg = ea.fg != 0 ? (ea.fg & 0xFF)
: (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0); : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
if (fg >= 0xf0 && fg <= 0xff) { if (fg >= 0xf0 && fg <= 0xff) {
return hostColors[fg - 0xf0]; return hostColors[fg - 0xf0];
} }
}
if (faIsProtected(currentFA & 0xFF)) { if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_TURQUOISE]; return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_TURQUOISE];
} }
@@ -0,0 +1,102 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.lib3270j.ft.FTConfig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.HeadlessException;
import static org.junit.jupiter.api.Assertions.*;
public class FileTransferSessionAndCmsTest {
@BeforeEach
@AfterEach
public void cleanup() {
FileTransferDialog.resetSessionState();
}
@Test
public void testCmsFilenamePeriodReplacement() {
// Test replacing periods with spaces for CMS
assertEquals("fscms exec", FileTransferDialog.formatHostFilename("fscms.exec", FTConfig.HostType.CMS));
assertEquals("fscms exec", FileTransferDialog.formatHostFilename("/var/tmp/fscms.exec", FTConfig.HostType.CMS));
assertEquals("test script exec", FileTransferDialog.formatHostFilename("test.script.exec", FTConfig.HostType.CMS));
assertEquals("noextension", FileTransferDialog.formatHostFilename("noextension", FTConfig.HostType.CMS));
// TSO and CICS should retain periods
assertEquals("fscms.exec", FileTransferDialog.formatHostFilename("fscms.exec", FTConfig.HostType.TSO));
assertEquals("my.dataset.name", FileTransferDialog.formatHostFilename("/path/to/my.dataset.name", FTConfig.HostType.TSO));
assertEquals("cics.file", FileTransferDialog.formatHostFilename("cics.file", FTConfig.HostType.CICS));
}
@Test
public void testSessionStateRememberedInMemory() {
assertNull(FileTransferDialog.getLastTransferState());
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.CMS;
state.isSend = true;
state.localFile = "/tmp/fscms.exec";
state.hostFile = "fscms exec";
state.isAscii = true;
state.mtu = 8192;
state.crFlag = false;
state.remapFlag = false;
state.append = true;
state.overwrite = true;
state.options = "CLEAR";
FileTransferDialog.setLastTransferState(state);
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
assertNotNull(retrieved);
assertEquals(FTConfig.HostType.CMS, retrieved.hostType);
assertTrue(retrieved.isSend);
assertEquals("/tmp/fscms.exec", retrieved.localFile);
assertEquals("fscms exec", retrieved.hostFile);
assertTrue(retrieved.isAscii);
assertEquals(8192, retrieved.mtu);
assertFalse(retrieved.crFlag);
assertFalse(retrieved.remapFlag);
assertTrue(retrieved.append);
assertTrue(retrieved.overwrite);
assertEquals("CLEAR", retrieved.options);
}
@Test
public void testDialogPrefillingFromSessionState() {
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.CMS;
state.isSend = true;
state.localFile = "/tmp/fscms.exec";
state.hostFile = "fscms exec";
state.isAscii = false; // Binary
state.mtu = 4096;
state.crFlag = true;
state.remapFlag = true;
state.append = false;
state.overwrite = true;
state.options = "ASCII";
FileTransferDialog.setLastTransferState(state);
try {
FileTransferDialog dialog = new FileTransferDialog(null, null);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem());
assertTrue(dialog.getSendRadio().isSelected());
assertFalse(dialog.getReceiveRadio().isSelected());
assertEquals("/tmp/fscms.exec", dialog.getLocalFileField().getText());
assertEquals("fscms exec", dialog.getHostFileField().getText());
assertFalse(dialog.getAsciiRadio().isSelected());
assertTrue(dialog.getBinaryRadio().isSelected());
assertEquals(4096, dialog.getMtuCombo().getSelectedItem());
assertTrue(dialog.getOverwriteCheck().isSelected());
assertEquals("ASCII", dialog.getOptionsField().getText());
dialog.dispose();
} catch (HeadlessException e) {
// Headless environment; verified through state retention
}
}
}
@@ -0,0 +1,80 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.Dimension;
import java.awt.HeadlessException;
import java.awt.event.ComponentEvent;
import static org.junit.jupiter.api.Assertions.*;
public class FontSettingsPersistenceTest {
private int originalFontSize;
private String originalFontFamily;
@BeforeEach
public void setup() {
originalFontSize = Settings.getFontSize();
originalFontFamily = Settings.getFontFamily();
}
@AfterEach
public void tearDown() {
Settings.setFontSize(originalFontSize);
Settings.setFontFamily(originalFontFamily);
}
@Test
public void testSettingsPersistence() {
Settings.setFontSize(22);
Settings.setFontFamily("Monospaced");
assertEquals(22, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
Settings.setFontSize(18);
assertEquals(18, Settings.getFontSize());
}
@Test
public void testTerminalPanelResizeDoesNotResetFontSize() {
try {
Settings.setFontSize(24);
Settings.setFontFamily("Monospaced");
TerminalPanel panel = new TerminalPanel();
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
// Simulate window / component resize events
panel.setSize(new Dimension(800, 600));
ComponentEvent resizeEvent1 = new ComponentEvent(panel, ComponentEvent.COMPONENT_RESIZED);
for (java.awt.event.ComponentListener cl : panel.getComponentListeners()) {
cl.componentResized(resizeEvent1);
}
// Verify font size and family did NOT reset
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
// Simulate another resize to smaller dimension
panel.setSize(new Dimension(400, 300));
ComponentEvent resizeEvent2 = new ComponentEvent(panel, ComponentEvent.COMPONENT_RESIZED);
for (java.awt.event.ComponentListener cl : panel.getComponentListeners()) {
cl.componentResized(resizeEvent2);
}
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
panel.dispose();
} catch (HeadlessException e) {
// In headless environment without display, Settings persistence is verified above
}
}
}
@@ -434,7 +434,7 @@ public class ConnectionConfig {
return terminalName; return terminalName;
} }
if (isDynamicModel()) { if (isDynamicModel()) {
return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC"; return "IBM-DYNAMIC";
} }
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType(); return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
} }
@@ -462,8 +462,23 @@ public class Telnet3270Client {
public void processFieldMark() { inputProcessor.processFieldMark(); } public void processFieldMark() { inputProcessor.processFieldMark(); }
/** Toggle Insert Mode. */ /** Toggle Insert Mode. */
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); } public boolean isInsertMode() { return inputProcessor != null && inputProcessor.isInsertMode(); }
public void processToggleInsert() { inputProcessor.processToggleInsert(); } public void toggleInsert() { if (inputProcessor != null) inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
public void processToggleInsert() { if (inputProcessor != null) inputProcessor.processToggleInsert(); }
/** Document Mode (Entry Assist) operations. */
public boolean isDocMode() { return screenBuffer != null && screenBuffer.isEntryAssistDOCmode(); }
public void setDocMode(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistDOCmode(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleDocMode() { setDocMode(!isDocMode()); }
public boolean isWordWrap() { return screenBuffer != null && screenBuffer.isEntryAssistWordWrap(); }
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleWordWrap() { setWordWrap(!isWordWrap()); }
/** APL Keyboard Mode operations. */
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
public void toggleAplMode() { if (inputProcessor != null) inputProcessor.toggleAplKeyboardMode(); }
/** Move word left. */ /** Move word left. */
public void processWordLeft() { inputProcessor.processWordLeft(); } public void processWordLeft() { inputProcessor.processWordLeft(); }
@@ -46,11 +46,11 @@ public enum TerminalModel {
/** /**
* Returns the terminal type string for TN3270E negotiation. * Returns the terminal type string for TN3270E negotiation.
* e.g., "IBM-3279-4-E" for a color model 4 with extended data stream, * e.g., "IBM-3279-4-E" for a color model 4 with extended data stream,
* or "IBM-DYNAMIC-E" for dynamic model. * or "IBM-DYNAMIC" for dynamic model.
*/ */
public String getTerminalType() { public String getTerminalType() {
if (modelNumber == 0) { if (modelNumber == 0) {
return "IBM-DYNAMIC-E"; return "IBM-DYNAMIC";
} }
return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber); return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber);
} }
@@ -178,7 +178,7 @@ public class ECLOIA implements ECLConstants {
} }
} }
private synchronized void notifyOIAChanged() { public synchronized void notifyOIAChanged() {
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(), ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
getAlphanumericType(), isInsertMode(), getStatusString()); getAlphanumericType(), isInsertMode(), getStatusString());
for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) { for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) {
@@ -484,6 +484,7 @@ public class ECLOIA implements ECLConstants {
if (isNumeric()) s |= STATE_NUMFIELD; if (isNumeric()) s |= STATE_NUMFIELD;
if (screen != null && screen.isEntryAssistDOCmode()) s |= STATE_DOC_MODE; if (screen != null && screen.isEntryAssistDOCmode()) s |= STATE_DOC_MODE;
if (screen != null && screen.isEntryAssistWordWrap()) s |= STATE_WORDWRAP; if (screen != null && screen.isEntryAssistWordWrap()) s |= STATE_WORDWRAP;
if (isApl()) s |= STATE_APL;
if (isXSystem()) s |= STATE_SYS_LOCK; if (isXSystem()) s |= STATE_SYS_LOCK;
if (isXComm()) s |= STATE_COMM_CHECK; if (isXComm()) s |= STATE_COMM_CHECK;
return s; return s;
@@ -494,6 +495,21 @@ public class ECLOIA implements ECLConstants {
public int getStatusFlags() { return GetStatusFlags(); } public int getStatusFlags() { return GetStatusFlags(); }
public long getStatusFlagsEx() { return GetStatusFlagsEx(); } public long getStatusFlagsEx() { return GetStatusFlagsEx(); }
public boolean isApl() {
return inputProcessor != null && inputProcessor.isAplKeyboardMode();
}
public boolean IsApl() { return isApl(); }
public boolean isDocMode() {
return screen != null && screen.isEntryAssistDOCmode();
}
public boolean IsDocMode() { return isDocMode(); }
public boolean isWordWrap() {
return screen != null && screen.isEntryAssistWordWrap();
}
public boolean IsWordWrap() { return isWordWrap(); }
public synchronized void setBitmaskState(long flag, boolean on) { public synchronized void setBitmaskState(long flag, boolean on) {
this.previousState = this.state; this.previousState = this.state;
if (on) { if (on) {
@@ -118,7 +118,91 @@ public class InputProcessor {
} }
} }
public boolean isInsertMode() { return insertMode; } public boolean isInsertMode() { return insertMode; }
public void setInsertMode(boolean insert) { this.insertMode = insert; } public void setInsertMode(boolean insert) {
this.insertMode = insert;
if (oia != null) {
oia.notifyOIAChanged();
}
}
@FunctionalInterface
public interface BellListener {
void onBell();
}
private BellListener bellListener;
private boolean bellEnabled = false;
private int bellColumn = 74; // 0-based index for column 75
private boolean insertOffOnAid = true;
private boolean aplKeyboardMode = false;
private boolean numericFieldLock = true;
private boolean autoSkipEnabled = true;
public void setBellListener(BellListener listener) { this.bellListener = listener; }
public BellListener getBellListener() { return bellListener; }
public boolean isBellEnabled() { return bellEnabled; }
public void setBellEnabled(boolean enabled) { this.bellEnabled = enabled; }
public int getBellColumn() { return bellColumn; }
public void setBellColumn(int col) { this.bellColumn = col; }
public boolean isInsertOffOnAid() { return insertOffOnAid; }
public void setInsertOffOnAid(boolean val) { this.insertOffOnAid = val; }
public boolean isAplKeyboardMode() { return aplKeyboardMode; }
public void setAplKeyboardMode(boolean enabled) {
this.aplKeyboardMode = enabled;
if (oia != null) {
oia.notifyOIAChanged();
}
}
public void toggleAplKeyboardMode() {
setAplKeyboardMode(!aplKeyboardMode);
}
public boolean isNumericFieldLock() { return numericFieldLock; }
public void setNumericFieldLock(boolean lock) { this.numericFieldLock = lock; }
public boolean isAutoSkipEnabled() { return autoSkipEnabled; }
public void setAutoSkipEnabled(boolean enabled) { this.autoSkipEnabled = enabled; }
/**
* Map ASCII key character to IBM 3270 APL / Graphic Escape code point.
*/
private int getAplCodeForChar(char ch) {
char upper = Character.toUpperCase(ch);
switch (upper) {
case 'A': return 0x81; // Alpha
case 'B': return 0x82; // Up tack / decode
case 'C': return 0x83; // Intersection
case 'D': return 0x84; // Floor
case 'E': return 0x85; // Vertical line
case 'F': return 0x87; // Del / Grad
case 'G': return 0x88; // Delta
case 'H': return 0x89; // Iota
case 'I': return 0x8A; // Right arrow
case 'J': return 0x8B; // Quote Quad
case 'K': return 0x8C; // Less than or equal
case 'L': return 0xAD; // [ Bracket left
case 'M': return 0x8E; // × Multiply
case 'N': return 0x8F; // ÷ Divide
case 'O': return 0x90; // Circle Star
case 'P': return 0x91; // Domino / Quad divide
case 'Q': return 0x92; // Down tack / encode
case 'R': return 0x95; // Rho
case 'S': return 0x94; // Ceiling
case 'T': return 0x98; // Epsilon underbar
case 'U': return 0x93; // Union
case 'V': return 0x97; // Not equal
case 'W': return 0x96; // Omega
case 'X': return 0xAC; // Transpose
case 'Y': return 0xA8; // Up arrow / Take
case 'Z': return 0xA9; // Down arrow / Drop
case '-': return 0xA2; // Horizontal line
case '|': return 0x85; // Vertical line
case '+': return 0xCB; // Cross
default: return -1;
}
}
/** /**
* Enter a character at the current cursor position. * Enter a character at the current cursor position.
@@ -142,8 +226,8 @@ public class InputProcessor {
int baddr = screen.getCursorAddress(); int baddr = screen.getCursorAddress();
baddr = ((baddr % size) + size) % size; baddr = ((baddr % size) + size) % size;
// Entry Assist DOC mode / Word Wrap handling // Entry Assist Word Wrap handling
if ((screen.isEntryAssistDOCmode() || screen.isEntryAssistWordWrap()) && !isNvtMode()) { if (screen.isEntryAssistWordWrap() && !isNvtMode()) {
int curCol = baddr % screen.getCols(); int curCol = baddr % screen.getCols();
int endCol = screen.getEntryAssistEndColumn(); int endCol = screen.getEntryAssistEndColumn();
int startCol = screen.getEntryAssistStartColumn(); int startCol = screen.getEntryAssistStartColumn();
@@ -178,7 +262,7 @@ public class InputProcessor {
} }
// Numeric-only field check: digits 0-9, minus (-), period (.), space ( ), DUP, FM // Numeric-only field check: digits 0-9, minus (-), period (.), space ( ), DUP, FM
if (faIsNumeric(faVal & 0xFF)) { if (numericFieldLock && faIsNumeric(faVal & 0xFF)) {
boolean isValidNumeric = (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == ' ' boolean isValidNumeric = (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == ' '
|| ch == '*' || ch == ';' || ch == (char) FCORDER_DUP || ch == (char) FCORDER_FM; || ch == '*' || ch == ';' || ch == (char) FCORDER_DUP || ch == (char) FCORDER_FM;
if (!isValidNumeric) { if (!isValidNumeric) {
@@ -191,8 +275,20 @@ public class InputProcessor {
} }
} }
// Translate character to EBCDIC // Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode)
int ebc = translator.unicodeToEbcdic(ch); boolean isAplChar = false;
int ebc = -1;
if (aplKeyboardMode) {
int aplCode = getAplCodeForChar(ch);
if (aplCode >= 0) {
ebc = aplCode;
ch = translator.mapAPL(aplCode);
isAplChar = true;
}
}
if (ebc < 0) {
ebc = translator.unicodeToEbcdic(ch);
}
if (ebc < 0) return; if (ebc < 0) return;
if (insertMode) { if (insertMode) {
@@ -229,6 +325,9 @@ public class InputProcessor {
ExtendedAttribute ea = screen.getCell(baddr); ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) ebc; ea.ec = (byte) ebc;
ea.ucs4 = ch; ea.ucs4 = ch;
if (isAplChar) {
ea.cs = ExtendedAttribute.CS_GE;
}
// Set MDT on field attribute // Set MDT on field attribute
if (screen.isFormatted()) { if (screen.isFormatted()) {
@@ -244,7 +343,7 @@ public class InputProcessor {
int nextAddr = screen.incrementAddress(baddr); int nextAddr = screen.incrementAddress(baddr);
if (screen.getCell(nextAddr).isFieldAttribute()) { if (screen.getCell(nextAddr).isFieldAttribute()) {
byte nextFa = screen.getCell(nextAddr).fa; byte nextFa = screen.getCell(nextAddr).fa;
if (faIsSkip(nextFa & 0xFF)) { if (autoSkipEnabled && faIsSkip(nextFa & 0xFF)) {
// Auto-skip field (Protected + Numeric): jump to next unprotected field // Auto-skip field (Protected + Numeric): jump to next unprotected field
int skipTarget = screen.findNextUnprotected(nextAddr); int skipTarget = screen.findNextUnprotected(nextAddr);
screen.setCursorAddress(skipTarget); screen.setCursorAddress(skipTarget);
@@ -264,6 +363,28 @@ public class InputProcessor {
screen.setCursorAddress((baddr + 1) % size); screen.setCursorAddress((baddr + 1) % size);
} }
// Entry Assist DOC mode: if we typed into or past the right margin, advance to startCol on next row
if (screen != null && screen.isEntryAssistDOCmode() && !screen.isEntryAssistWordWrap() && !isNvtMode()) {
int typedCol = baddr % screen.getCols();
if (typedCol >= screen.getEntryAssistEndColumn()) {
int curRow = baddr / screen.getCols();
int nextRow = (curRow + 1) % screen.getRows();
int targetAddr = nextRow * screen.getCols() + screen.getEntryAssistStartColumn();
if (screen.isFormatted()) {
targetAddr = screen.findNextUnprotected(targetAddr - 1);
}
screen.setCursorAddress(targetAddr);
}
}
// Audible End-of-Line Warning Signal
if (bellEnabled && screen != null) {
int curCol = screen.getCursorCol();
if (curCol == bellColumn && bellListener != null) {
bellListener.onBell();
}
}
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot(); screen.updateDisplaySnapshot();
} }
@@ -523,6 +644,9 @@ public class InputProcessor {
} }
if (aidCode == AID_CLEAR) { if (aidCode == AID_CLEAR) {
if (insertOffOnAid && insertMode) {
setInsertMode(false);
}
screen.clear(); screen.clear();
screen.markAllChanged(); screen.markAllChanged();
if (graphicsPlane != null) { if (graphicsPlane != null) {
@@ -538,6 +662,10 @@ public class InputProcessor {
return; return;
} }
if (insertOffOnAid && insertMode) {
setInsertMode(false);
}
lastAid = aidCode; lastAid = aidCode;
setKeyboardLocked(true); setKeyboardLocked(true);
@@ -792,6 +920,10 @@ public class InputProcessor {
} }
public void tab() { public void tab() {
if (screen != null && screen.isEntryAssistDOCmode()) {
screen.processWordTab(true);
return;
}
int addr = screen.findNextUnprotected(screen.getCursorAddress()); int addr = screen.findNextUnprotected(screen.getCursorAddress());
screen.setCursorAddress(addr); screen.setCursorAddress(addr);
screen.updateDisplaySnapshot(); screen.updateDisplaySnapshot();
@@ -806,6 +938,10 @@ public class InputProcessor {
} }
public void backTab() { public void backTab() {
if (screen != null && screen.isEntryAssistDOCmode()) {
screen.processWordTab(false);
return;
}
if (!screen.isFormatted()) return; if (!screen.isFormatted()) return;
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
if (size <= 0) return; if (size <= 0) return;
@@ -1546,6 +1682,22 @@ public class InputProcessor {
case "lightpen": case "lightpen":
processLightPen(); processLightPen();
break; break;
case "docmode":
if (screen != null) {
screen.setEntryAssistDOCmode(!screen.isEntryAssistDOCmode());
if (oia != null) oia.notifyOIAChanged();
}
break;
case "wordwrap":
if (screen != null) {
screen.setEntryAssistWordWrap(!screen.isEntryAssistWordWrap());
if (oia != null) oia.notifyOIAChanged();
}
break;
case "apl":
case "aplmode":
toggleAplKeyboardMode();
break;
default: default:
if (token.startsWith("pf")) { if (token.startsWith("pf")) {
try { try {
@@ -1071,12 +1071,16 @@ public class ScreenBuffer {
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; } public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); } public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); }
public void setLeftMargin(int n) { setEntryAssistStartColumn(n); }
public void setRightMargin(int n) { setEntryAssistEndColumn(n); }
public void setWordTabPositions(int[] stops) { setEntryAssistTabStops(stops); }
/** /**
* Perform Entry Assist word wrap if typing near/past end margin. * Perform Entry Assist word wrap if typing near/past end margin.
* Moves any partial word typed on the current line to the beginning of the next line (docStartCol). * Moves any partial word typed on the current line to the beginning of the next line (docStartCol).
*/ */
public synchronized boolean handleWordWrap(int curAddr, char typedChar) { public synchronized boolean handleWordWrap(int curAddr, char typedChar) {
if (!docMode && !wordWrap) return false; if (!wordWrap) return false;
int size = rows * cols; int size = rows * cols;
if (size <= 0) return false; if (size <= 0) return false;
curAddr = ((curAddr % size) + size) % size; curAddr = ((curAddr % size) + size) % size;
@@ -75,9 +75,6 @@ public class TelnetFSM {
return list; return list;
} }
if (config.isDynamicModel()) { if (config.isDynamicModel()) {
if (config.isExtendedDataStream()) {
list.add("IBM-DYNAMIC-E");
}
list.add("IBM-DYNAMIC"); list.add("IBM-DYNAMIC");
list.add("IBM-3279-4-E"); list.add("IBM-3279-4-E");
list.add("IBM-3279-4"); list.add("IBM-3279-4");
@@ -0,0 +1,215 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class EntryAssistFullModeTest {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor inputProcessor;
private ECLOIA oia;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator); // 24x80
inputProcessor = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, inputProcessor, null);
inputProcessor.setOIA(oia);
}
@Test
public void testDocModeCursorAdvanceWithoutWordWrap() {
// Document Mode ON, Word Wrap OFF
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(false);
screen.setLeftMargin(0); // Col 1 (0-based 0)
screen.setRightMargin(72); // Col 73 (0-based 72)
assertTrue(screen.isEntryAssistDOCmode());
assertFalse(screen.isEntryAssistWordWrap());
assertEquals(0, screen.getEntryAssistStartColumn());
assertEquals(72, screen.getEntryAssistEndColumn());
// Setup open cursor position
screen.setCursorPosition(0, 72); // Row 0, Col 72 (at right margin)
inputProcessor.typeCharacter('X');
// Should advance to next row at left margin (Row 1, Col 0)
assertEquals(1, screen.getCursorRow());
assertEquals(0, screen.getCursorCol());
assertEquals('X', (char) screen.getCell(72).ucs4);
}
@Test
public void testWordWrapTransfersPartialWord() {
// Document Mode ON, Word Wrap ON
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
screen.setLeftMargin(5); // Start col = 5
screen.setRightMargin(20); // End col = 20
// Type "HELLO " starting at col 10
screen.setCursorPosition(0, 10);
for (char c : "HELLO ".toCharArray()) {
inputProcessor.typeCharacter(c);
}
// Now cursor is at col 16. Type "TEST" which crosses col 20
screen.setCursorPosition(0, 18);
inputProcessor.typeCharacter('W');
inputProcessor.typeCharacter('O');
inputProcessor.typeCharacter('R');
inputProcessor.typeCharacter('D'); // At col 21, past end col 20
// Word wrap should have moved "WORD" to next line starting at left margin 5
assertEquals(1, screen.getCursorRow());
assertTrue(screen.getCursorCol() >= 5);
}
@Test
public void testDocModeTabStopsNavigation() {
screen.setEntryAssistDOCmode(true);
screen.setLeftMargin(0);
screen.setRightMargin(79);
screen.setEntryAssistTabStops(new int[]{0, 10, 20, 30});
screen.setCursorPosition(0, 0);
// Word tab forward
screen.processWordTab(true);
assertEquals(10, screen.getCursorCol());
assertEquals(0, screen.getCursorRow());
screen.processWordTab(true);
assertEquals(20, screen.getCursorCol());
// Word back tab
screen.processWordTab(false);
assertEquals(10, screen.getCursorCol());
screen.processWordTab(false);
assertEquals(0, screen.getCursorCol());
}
@Test
public void testAudibleEndOfLineBell() {
inputProcessor.setBellEnabled(true);
inputProcessor.setBellColumn(74);
AtomicInteger bellCount = new AtomicInteger(0);
inputProcessor.setBellListener(bellCount::incrementAndGet);
screen.setCursorPosition(0, 70);
inputProcessor.typeCharacter('A'); // 71
inputProcessor.typeCharacter('B'); // 72
inputProcessor.typeCharacter('C'); // 73
assertEquals(0, bellCount.get());
inputProcessor.typeCharacter('D'); // 74 -> triggers bell
assertEquals(1, bellCount.get());
inputProcessor.typeCharacter('E'); // 75 -> does not retrigger on same line
assertEquals(1, bellCount.get());
// Move to next line at col 73 and type across bell column
screen.setCursorPosition(1, 73);
inputProcessor.typeCharacter('Z'); // advances to 74 -> triggers bell on next line
assertEquals(2, bellCount.get());
}
@Test
public void testInsertModeResetOnAid() {
inputProcessor.setInsertOffOnAid(true);
inputProcessor.setInsertMode(true);
assertTrue(inputProcessor.isInsertMode());
// Send AID_ENTER
inputProcessor.sendAid(AID_ENTER);
assertFalse(inputProcessor.isInsertMode(), "Insert mode should be reset after AID key");
// When insertOffOnAid is disabled
inputProcessor.setInsertOffOnAid(false);
inputProcessor.setInsertMode(true);
inputProcessor.sendAid(AID_ENTER);
assertTrue(inputProcessor.isInsertMode(), "Insert mode should be preserved when insertOffOnAid is false");
}
@Test
public void testAplKeyboardModeAndOia() {
inputProcessor.setAplKeyboardMode(true);
assertTrue(inputProcessor.isAplKeyboardMode());
// OIA status flag includes STATE_APL
long statusEx = oia.GetStatusFlagsEx();
assertEquals(ECLOIA.STATE_APL, statusEx & ECLOIA.STATE_APL);
assertTrue(oia.isApl());
// Test APL translation for key 'a' (alpha -> 0x41 with CS_GE)
screen.setCursorPosition(0, 0);
inputProcessor.typeCharacter('a');
var cell = screen.getCell(0);
assertEquals(CS_GE, cell.cs, "APL character must be stored with CS_GE (Graphic Escape) charset");
// Toggle APL mode off
inputProcessor.toggleAplKeyboardMode();
assertFalse(inputProcessor.isAplKeyboardMode());
assertFalse(oia.isApl());
assertEquals(0, oia.GetStatusFlagsEx() & ECLOIA.STATE_APL);
}
@Test
public void testNumericFieldLockSetting() {
// Field: pos 0 is numeric FA, pos 1..5 unprotected, pos 6 protect FA
screen.setFieldAttribute(0, (byte) FA_NUMERIC);
screen.setFieldAttribute(6, (byte) FA_PROTECT);
screen.setCursorAddress(1);
// Case 1: NumericFieldLock is true
inputProcessor.setNumericFieldLock(true);
inputProcessor.typeCharacter('X'); // invalid numeric character
assertTrue(inputProcessor.isKeyboardLocked(), "Keyboard should be locked on non-numeric char");
assertEquals(ECLConstants.INHIBIT_NUMERIC_ONLY, oia.getInputInhibited());
// Reset keyboard
inputProcessor.reset();
assertFalse(inputProcessor.isKeyboardLocked());
// Case 2: NumericFieldLock is false
inputProcessor.setNumericFieldLock(false);
screen.setCursorAddress(1);
inputProcessor.typeCharacter('X');
assertFalse(inputProcessor.isKeyboardLocked(), "Keyboard should NOT be locked when NumericFieldLock is false");
}
@Test
public void testOiaModeIndicators() {
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
inputProcessor.setAplKeyboardMode(true);
inputProcessor.setInsertMode(true);
long flags = oia.GetStatusFlagsEx();
assertTrue((flags & ECLOIA.STATE_DOC_MODE) != 0);
assertTrue((flags & ECLOIA.STATE_WORDWRAP) != 0);
assertTrue((flags & ECLOIA.STATE_APL) != 0);
assertTrue((flags & ECLOIA.STATE_INSERT) != 0);
assertTrue(oia.isDocMode());
assertTrue(oia.isWordWrap());
assertTrue(oia.isApl());
assertTrue(oia.isInsertMode());
}
}
@@ -76,8 +76,9 @@ public class DynamicScreenBufferTest {
assertTrue(model.isDynamic()); assertTrue(model.isDynamic());
assertEquals(0, model.getModelNumber()); assertEquals(0, model.getModelNumber());
assertTrue(model.isColor()); assertTrue(model.isColor());
assertEquals("IBM-DYNAMIC-E", model.getTerminalType()); assertEquals("IBM-DYNAMIC", model.getTerminalType());
assertEquals("IBM-DYNAMIC", model.getBaseTerminalType()); assertEquals("IBM-DYNAMIC", model.getBaseTerminalType());
assertEquals("IBM-DYNAMIC", model.toString());
ScreenBuffer buffer = new ScreenBuffer(model, translator); ScreenBuffer buffer = new ScreenBuffer(model, translator);
assertEquals(24, buffer.getDefRows()); assertEquals(24, buffer.getDefRows());
@@ -65,12 +65,14 @@ public class DynamicTelnetFSMTest {
// Host requests TTYPE: IAC SB TTYPE SEND IAC SE // Host requests TTYPE: IAC SB TTYPE SEND IAC SE
feedBytes(255, 250, 24, 1, 255, 240); feedBytes(255, 250, 24, 1, 255, 240);
// Verify sent response is IBM-DYNAMIC-E // Verify sent response is IBM-DYNAMIC
assertFalse(connection.sentData.isEmpty()); assertFalse(connection.sentData.isEmpty());
byte[] lastSent = connection.sentData.get(connection.sentData.size() - 1); byte[] lastSent = connection.sentData.get(connection.sentData.size() - 1);
String s = new String(lastSent); String s = new String(lastSent);
assertTrue(s.contains("IBM-DYNAMIC-E") || s.contains("IBM-DYNAMIC"), assertTrue(s.contains("IBM-DYNAMIC"),
"Expected terminal type negotiation to send IBM-DYNAMIC, got: " + s); "Expected terminal type negotiation to send IBM-DYNAMIC, got: " + s);
assertFalse(s.contains("IBM-DYNAMIC-E"),
"Terminal type negotiation should not contain IBM-DYNAMIC-E, got: " + s);
} }
@Test @Test