From 74e30d1eadf4010f3f3d4732761373fa9a9c93fc Mon Sep 17 00:00:00 2001 From: Rudi Date: Fri, 4 Sep 2026 20:58:19 -0400 Subject: [PATCH] IBM-DYNAMIC tweaking --- .../java/haus/nightmare/j3270/J3270App.java | 85 ++++++- .../haus/nightmare/j3270/config/Settings.java | 172 ++++++++++++++ .../j3270/ft/FileTransferDialog.java | 174 +++++++++++++- .../nightmare/j3270/ui/SettingsDialog.java | 162 ++++++++++++- .../haus/nightmare/j3270/ui/StatusBar.java | 96 +++++++- .../nightmare/j3270/ui/TerminalPanel.java | 147 ++++++++---- .../ft/FileTransferSessionAndCmsTest.java | 102 +++++++++ .../j3270/ui/FontSettingsPersistenceTest.java | 80 +++++++ .../nightmare/lib3270j/ConnectionConfig.java | 2 +- .../nightmare/lib3270j/Telnet3270Client.java | 19 +- .../nightmare/lib3270j/TerminalModel.java | 4 +- .../haus/nightmare/lib3270j/ecl/ECLOIA.java | 18 +- .../lib3270j/input/InputProcessor.java | 166 +++++++++++++- .../lib3270j/screen/ScreenBuffer.java | 6 +- .../nightmare/lib3270j/telnet/TelnetFSM.java | 3 - .../lib3270j/ecl/EntryAssistFullModeTest.java | 215 ++++++++++++++++++ .../screen/DynamicScreenBufferTest.java | 3 +- .../lib3270j/telnet/DynamicTelnetFSMTest.java | 6 +- 18 files changed, 1374 insertions(+), 86 deletions(-) create mode 100644 j3270/src/test/java/haus/nightmare/j3270/ft/FileTransferSessionAndCmsTest.java create mode 100644 j3270/src/test/java/haus/nightmare/j3270/ui/FontSettingsPersistenceTest.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/EntryAssistFullModeTest.java diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index 0171608..97c404e 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -92,6 +92,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate private void buildUI() { terminalPanel = new TerminalPanel(); statusBar = new StatusBar(); + terminalPanel.setStatusBar(statusBar); getContentPane().setLayout(new BorderLayout()); getContentPane().setBackground(Color.BLACK); @@ -259,7 +260,89 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog)); 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"); actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> { if (client != null && client.getConnectionState().isFullSession()) { diff --git a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java index 9f6fc56..7192983 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java +++ b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java @@ -33,6 +33,7 @@ public class Settings { public static void setFontFamily(String family) { prefs.put("fontFamily", family); + flushPrefs(); } public static int getFontSize() { @@ -41,6 +42,14 @@ public class Settings { public static void setFontSize(int size) { prefs.putInt("fontSize", size); + flushPrefs(); + } + + private static void flushPrefs() { + try { + prefs.flush(); + } catch (Exception ignored) { + } } public static StartupBehavior getStartupBehavior() { @@ -269,6 +278,101 @@ public class Settings { 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 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) { switch (section) { case "appearance": @@ -365,6 +469,55 @@ public class Settings { } 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: // Allow bare keys outside any section — treat as raw prefs log.fine("Setting raw preference: " + key + " = " + value); @@ -412,6 +565,25 @@ public class Settings { w.println("blockSelectMode = " + getBlockSelectMode()); 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] w.println("[graphics]"); w.println("graphicsMode = " + getGraphicsMode().name()); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java index f64620e..529889d 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java @@ -12,6 +12,50 @@ import java.io.File; 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 Frame owner; @@ -66,6 +110,16 @@ public class FileTransferDialog extends JDialog { // Host Type 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)); // Direction @@ -88,6 +142,15 @@ public class FileTransferDialog extends JDialog { // Local File 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..."); ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT); browseLocalButton.addActionListener(e -> browseLocalFile()); @@ -211,13 +274,47 @@ public class FileTransferDialog extends JDialog { setContentPane(mainPanel); + prefillFromLastTransfer(); ThemeManager.applyThemeToWindow(this); 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() { boolean isSend = sendRadio.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); overwriteCheck.setEnabled(!isSend); @@ -225,10 +322,11 @@ public class FileTransferDialog extends JDialog { crCheck.setEnabled(isAscii); remapCheck.setEnabled(isAscii); - recfmField.setEnabled(isSend); - lreclField.setEnabled(isSend); - blksizeField.setEnabled(isSend); - spaceField.setEnabled(isSend); + recfmField.setEnabled(isSend && isTso); + lreclField.setEnabled(isSend && isTso); + blksizeField.setEnabled(isSend && isTso); + spaceField.setEnabled(isSend && isTso); + optionsField.setEnabled(isCms); } private JPanel createRow(String labelText, Component comp) { @@ -245,9 +343,13 @@ public class FileTransferDialog extends JDialog { private void browseLocalFile() { JFileChooser chooser = new JFileChooser(); if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { - localFileField.setText(chooser.getSelectedFile().getAbsolutePath()); - if (hostFileField.getText().trim().isEmpty()) { - hostFileField.setText(chooser.getSelectedFile().getName()); + File selected = chooser.getSelectedFile(); + localFileField.setText(selected.getAbsolutePath()); + 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) { 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("\"", ""); - int lastDot = cleanName.lastIndexOf('.'); - if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1); + 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('.'); + if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1); + } localFileField.setText(cleanName); } } @@ -298,6 +411,8 @@ public class FileTransferDialog extends JDialog { config.setSpace(spaceField.getText().trim()); config.setOptions(optionsField.getText().trim()); + recordSessionTransfer(); + String error = coordinator.startTransfer(config); if (error != null) { 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 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 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) { Color fg = new Color(200, 200, 200); Color bg = new Color(40, 40, 40); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java index 8625350..1e2f62e 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java @@ -34,6 +34,19 @@ public class SettingsDialog extends JDialog { private JSpinner dynamicRowsSpinner; 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 private final Color[] tempHostColors = new Color[16]; private final Map tempMonoColors = new HashMap<>(); @@ -45,7 +58,7 @@ public class SettingsDialog extends JDialog { this.parentApp = parent; initComponents(); - setSize(560, 480); + setSize(600, 520); setLocationRelativeTo(parent); } @@ -55,6 +68,7 @@ public class SettingsDialog extends JDialog { tabbedPane.addTab("Appearance", createAppearancePanel()); tabbedPane.addTab("Behavior", createBehaviorPanel()); + tabbedPane.addTab("Entry Assist & Modes", createEntryAssistPanel()); tabbedPane.addTab("Advanced", createAdvancedPanel()); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8)); @@ -133,11 +147,31 @@ public class SettingsDialog extends JDialog { panel.add(fontLabel, gbc); String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); - fontBox = new JComboBox<>(fonts); - // Find default + java.util.List fontList = new java.util.ArrayList<>(); + boolean hasMonospaced = false; String currentFont = Settings.getFontFamily(); - for (int i = 0; i < fonts.length; i++) { - if (fonts[i].equalsIgnoreCase(currentFont)) { + for (String f : fonts) { + 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); break; } @@ -267,6 +301,105 @@ public class SettingsDialog extends JDialog { 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() { JPanel panel = new JPanel(new BorderLayout()); @@ -585,6 +718,22 @@ public class SettingsDialog extends JDialog { 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 // Save Colors for (int i=0; i<16; i++) { @@ -600,6 +749,9 @@ public class SettingsDialog extends JDialog { } parentApp.getTerminalPanel().reloadSettings(); + if (parentApp != null && (parentApp.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) { + parentApp.getTerminalPanel().guardedPack(); + } ThemeManager.applyThemeToWindow(this); return true; diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java index 0a5c14c..731fa91 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java @@ -18,7 +18,11 @@ public class StatusBar extends JPanel { private final JLabel tlsStatus; private final JLabel luName; private final JLabel lockStatus; + private final JLabel insertStatus; + private final JLabel aplStatus; private final JLabel fieldTypeStatus; + private final JLabel docModeStatus; + private final JLabel wordWrapStatus; private final JLabel codePageInfo; private final JLabel modelInfo; private final JLabel cursorPosition; @@ -38,7 +42,35 @@ public class StatusBar extends JPanel { tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); 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()); + 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()); modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal()); @@ -51,9 +83,17 @@ public class StatusBar extends JPanel { add(luName); add(Box.createHorizontalStrut(12)); add(lockStatus); + add(Box.createHorizontalStrut(8)); + add(insertStatus); + add(Box.createHorizontalStrut(8)); + add(aplStatus); add(Box.createHorizontalStrut(10)); add(fieldTypeStatus); add(Box.createHorizontalGlue()); + add(docModeStatus); + add(Box.createHorizontalStrut(8)); + add(wordWrapStatus); + add(Box.createHorizontalStrut(12)); add(codePageInfo); add(Box.createHorizontalStrut(12)); add(modelInfo); @@ -69,6 +109,23 @@ public class StatusBar extends JPanel { 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) { this.client = client; this.terminalPanel = terminalPanel; @@ -90,7 +147,11 @@ public class StatusBar extends JPanel { luName.setText(""); lockStatus.setText(""); lockStatus.setForeground(ThemeManager.getOiaInputInhibited(theme)); + insertStatus.setText(""); + aplStatus.setText(""); fieldTypeStatus.setText(""); + docModeStatus.setText(""); + wordWrapStatus.setText(""); codePageInfo.setText(""); modelInfo.setText(""); cursorPosition.setText("001/001"); @@ -203,13 +264,42 @@ public class StatusBar extends JPanel { break; } lockStatus.setForeground(lockFg); - } else if (client.getInputProcessor().isInsertMode()) { - lockStatus.setText("INSERT"); - lockStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme)); } else { 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 if (state.isFullSession() && client.getScreenBuffer().isFormatted()) { if (client.getOIA().isNumeric()) { diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java index 8567b83..0b47f66 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -84,6 +84,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { private long lastGraphicsUpdateCount = -1; private java.awt.image.BufferedImage cachedGraphicsImage = null; private boolean resizeGuard = false; + private StatusBar statusBar = null; /** * 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); addMouseMotionListener(mouseHandler); - // Handle resize: auto-fit font to window size + // Handle resize: keep user configured font persistent without auto-fit reset addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { 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() { 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, "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 int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); 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-TAB", createAction(() -> handleTab(false))); 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-DOWN", createAction(() -> handleCursor("down"))); 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(); ip.setInsertMode(!ip.isInsertMode()); 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() { if (client != null && client.getConnectionState().isFullSession()) { 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() { if (client != null) { client.getScreenBuffer().updateDisplaySnapshot(); } repaint(); + if (statusBar != null) { + statusBar.updateStatus(); + } Container parent = getParent(); while (parent != null) { if (parent instanceof JFrame) { @@ -1088,6 +1131,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { setupColors(); setupKeyBindings(); setupFont(); + applyModeSettings(); blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode(); crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler(); 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) { setupGraphicsPlaneRenderer(); updateCellSize(); + applyModeSettings(); + + if (client.getInputProcessor() != null) { + client.getInputProcessor().setBellListener(() -> { + SwingUtilities.invokeLater(() -> Toolkit.getDefaultToolkit().beep()); + }); + } client.setNvtClipboardHandler(new haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler() { @Override @@ -1564,10 +1615,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { } private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) { - int fg = ea.fg != 0 ? (ea.fg & 0xFF) - : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0); - if (fg >= 0xf0 && fg <= 0xff) { - return hostColors[fg - 0xf0]; + if (!haus.nightmare.j3270.config.Settings.getFourColorOverride()) { + int fg = ea.fg != 0 ? (ea.fg & 0xFF) + : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0); + if (fg >= 0xf0 && fg <= 0xff) { + return hostColors[fg - 0xf0]; + } } if (faIsProtected(currentFA & 0xFF)) { return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_TURQUOISE]; diff --git a/j3270/src/test/java/haus/nightmare/j3270/ft/FileTransferSessionAndCmsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ft/FileTransferSessionAndCmsTest.java new file mode 100644 index 0000000..76164c2 --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/ft/FileTransferSessionAndCmsTest.java @@ -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 + } + } +} diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/FontSettingsPersistenceTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/FontSettingsPersistenceTest.java new file mode 100644 index 0000000..23691b6 --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/FontSettingsPersistenceTest.java @@ -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 + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionConfig.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionConfig.java index 3bc65a6..270e490 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionConfig.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ConnectionConfig.java @@ -434,7 +434,7 @@ public class ConnectionConfig { return terminalName; } if (isDynamicModel()) { - return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC"; + return "IBM-DYNAMIC"; } return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType(); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java index aefe639..20b5e6d 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/Telnet3270Client.java @@ -462,8 +462,23 @@ public class Telnet3270Client { public void processFieldMark() { inputProcessor.processFieldMark(); } /** Toggle Insert Mode. */ - public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); } - public void processToggleInsert() { inputProcessor.processToggleInsert(); } + public boolean isInsertMode() { return inputProcessor != null && inputProcessor.isInsertMode(); } + 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. */ public void processWordLeft() { inputProcessor.processWordLeft(); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/TerminalModel.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/TerminalModel.java index 8cb26d1..3501d5e 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/TerminalModel.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/TerminalModel.java @@ -46,11 +46,11 @@ public enum TerminalModel { /** * Returns the terminal type string for TN3270E negotiation. * 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() { if (modelNumber == 0) { - return "IBM-DYNAMIC-E"; + return "IBM-DYNAMIC"; } return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java index 6b9f5d7..bce760e 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java @@ -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(), getAlphanumericType(), isInsertMode(), getStatusString()); for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) { @@ -484,6 +484,7 @@ public class ECLOIA implements ECLConstants { if (isNumeric()) s |= STATE_NUMFIELD; if (screen != null && screen.isEntryAssistDOCmode()) s |= STATE_DOC_MODE; if (screen != null && screen.isEntryAssistWordWrap()) s |= STATE_WORDWRAP; + if (isApl()) s |= STATE_APL; if (isXSystem()) s |= STATE_SYS_LOCK; if (isXComm()) s |= STATE_COMM_CHECK; return s; @@ -494,6 +495,21 @@ public class ECLOIA implements ECLConstants { public int getStatusFlags() { return GetStatusFlags(); } 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) { this.previousState = this.state; if (on) { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java index 86ab4d9..4d0c84b 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -118,7 +118,91 @@ public class InputProcessor { } } 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. @@ -142,8 +226,8 @@ public class InputProcessor { int baddr = screen.getCursorAddress(); baddr = ((baddr % size) + size) % size; - // Entry Assist DOC mode / Word Wrap handling - if ((screen.isEntryAssistDOCmode() || screen.isEntryAssistWordWrap()) && !isNvtMode()) { + // Entry Assist Word Wrap handling + if (screen.isEntryAssistWordWrap() && !isNvtMode()) { int curCol = baddr % screen.getCols(); int endCol = screen.getEntryAssistEndColumn(); int startCol = screen.getEntryAssistStartColumn(); @@ -178,7 +262,7 @@ public class InputProcessor { } // 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 == ' ' || ch == '*' || ch == ';' || ch == (char) FCORDER_DUP || ch == (char) FCORDER_FM; if (!isValidNumeric) { @@ -191,8 +275,20 @@ public class InputProcessor { } } - // Translate character to EBCDIC - int ebc = translator.unicodeToEbcdic(ch); + // Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode) + 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 (insertMode) { @@ -229,6 +325,9 @@ public class InputProcessor { ExtendedAttribute ea = screen.getCell(baddr); ea.ec = (byte) ebc; ea.ucs4 = ch; + if (isAplChar) { + ea.cs = ExtendedAttribute.CS_GE; + } // Set MDT on field attribute if (screen.isFormatted()) { @@ -244,7 +343,7 @@ public class InputProcessor { int nextAddr = screen.incrementAddress(baddr); if (screen.getCell(nextAddr).isFieldAttribute()) { 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 int skipTarget = screen.findNextUnprotected(nextAddr); screen.setCursorAddress(skipTarget); @@ -264,6 +363,28 @@ public class InputProcessor { 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.updateDisplaySnapshot(); } @@ -523,6 +644,9 @@ public class InputProcessor { } if (aidCode == AID_CLEAR) { + if (insertOffOnAid && insertMode) { + setInsertMode(false); + } screen.clear(); screen.markAllChanged(); if (graphicsPlane != null) { @@ -538,6 +662,10 @@ public class InputProcessor { return; } + if (insertOffOnAid && insertMode) { + setInsertMode(false); + } + lastAid = aidCode; setKeyboardLocked(true); @@ -792,6 +920,10 @@ public class InputProcessor { } public void tab() { + if (screen != null && screen.isEntryAssistDOCmode()) { + screen.processWordTab(true); + return; + } int addr = screen.findNextUnprotected(screen.getCursorAddress()); screen.setCursorAddress(addr); screen.updateDisplaySnapshot(); @@ -806,6 +938,10 @@ public class InputProcessor { } public void backTab() { + if (screen != null && screen.isEntryAssistDOCmode()) { + screen.processWordTab(false); + return; + } if (!screen.isFormatted()) return; int size = screen.getRows() * screen.getCols(); if (size <= 0) return; @@ -1546,6 +1682,22 @@ public class InputProcessor { case "lightpen": processLightPen(); 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: if (token.startsWith("pf")) { try { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java index 843fc12..d670bde 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java @@ -1071,12 +1071,16 @@ public class ScreenBuffer { public void setEntryAssistTabStops(int[] stops) { this.tabStops = 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. * 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) { - if (!docMode && !wordWrap) return false; + if (!wordWrap) return false; int size = rows * cols; if (size <= 0) return false; curAddr = ((curAddr % size) + size) % size; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java index 3615c1e..7f90c0f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java @@ -75,9 +75,6 @@ public class TelnetFSM { return list; } if (config.isDynamicModel()) { - if (config.isExtendedDataStream()) { - list.add("IBM-DYNAMIC-E"); - } list.add("IBM-DYNAMIC"); list.add("IBM-3279-4-E"); list.add("IBM-3279-4"); diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/EntryAssistFullModeTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/EntryAssistFullModeTest.java new file mode 100644 index 0000000..38dcb93 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/EntryAssistFullModeTest.java @@ -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()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/DynamicScreenBufferTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/DynamicScreenBufferTest.java index 6a5dfe4..31fdf63 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/DynamicScreenBufferTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/DynamicScreenBufferTest.java @@ -76,8 +76,9 @@ public class DynamicScreenBufferTest { assertTrue(model.isDynamic()); assertEquals(0, model.getModelNumber()); assertTrue(model.isColor()); - assertEquals("IBM-DYNAMIC-E", model.getTerminalType()); + assertEquals("IBM-DYNAMIC", model.getTerminalType()); assertEquals("IBM-DYNAMIC", model.getBaseTerminalType()); + assertEquals("IBM-DYNAMIC", model.toString()); ScreenBuffer buffer = new ScreenBuffer(model, translator); assertEquals(24, buffer.getDefRows()); diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/DynamicTelnetFSMTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/DynamicTelnetFSMTest.java index a6d9ec8..eeafd13 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/DynamicTelnetFSMTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/DynamicTelnetFSMTest.java @@ -65,12 +65,14 @@ public class DynamicTelnetFSMTest { // Host requests TTYPE: IAC SB TTYPE SEND IAC SE feedBytes(255, 250, 24, 1, 255, 240); - // Verify sent response is IBM-DYNAMIC-E + // Verify sent response is IBM-DYNAMIC assertFalse(connection.sentData.isEmpty()); byte[] lastSent = connection.sentData.get(connection.sentData.size() - 1); 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); + assertFalse(s.contains("IBM-DYNAMIC-E"), + "Terminal type negotiation should not contain IBM-DYNAMIC-E, got: " + s); } @Test