diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index bb0e177..4332463 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -145,12 +145,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate // 3. View menu JMenu viewMenu = createMenu("View"); - viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2))); - viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2))); + viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2), true)); + viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2), true)); viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> { terminalPanel.setFontSize(16); terminalPanel.guardedPack(); - })); + }, true)); viewMenu.addSeparator(); // UI Theme Submenu @@ -234,17 +234,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate client.sendEnter(); terminalPanel.repaint(); } - })); + }, true)); actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> { if (client != null) client.sendClear(); terminalPanel.repaint(); - })); + }, true)); actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> { if (client != null) client.reset(); terminalPanel.repaint(); - })); + }, true)); actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> { if (client != null && client.getConnectionState().isFullSession()) { client.eraseInput(); @@ -272,7 +272,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate }, true)); actionsMenu.addSeparator(); actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); - actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog)); + actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true)); menuBar.add(actionsMenu); // 5. Help menu @@ -706,18 +706,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate "Insert — Toggle insert mode\n" + "Escape — Reset\n" + "PageUp/Down — PF7/PF8\n" + - "Alt+C / Ctrl+K — Clear\n" + + "Alt+C / Alt+K — Clear\n" + "Alt+E — Erase Input\n" + "Alt+A — Attention\n" + "Alt+S — System Request\n" + "Alt+Q — Cursor Select\n" + "Alt+L — Toggle Light Pen\n" + + "Alt+T — File Transfer\n" + + "Alt+=/-/0 — Font size +/-/reset\n" + "Cmd/Ctrl+F — Find on Screen\n" + "Cmd/Ctrl+G / F3— Find Next\n" + - "Cmd/Ctrl+T — File Transfer\n" + "Cmd/Ctrl+D — Disconnect\n" + - "Cmd/Ctrl+Q — Quit\n" + - "Cmd/Ctrl+=/-/0 — Font size +/-/reset"; + "Cmd/Ctrl+Q — Quit"; JTextArea area = new JTextArea(text); area.setEditable(false); diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java new file mode 100644 index 0000000..43316e2 --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java @@ -0,0 +1,85 @@ +package haus.nightmare.j3270.ui; + +import haus.nightmare.j3270.J3270App; +import org.junit.jupiter.api.Test; + +import javax.swing.*; +import java.awt.event.KeyEvent; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class MenuBarShortcutsTest { + + private Map collectMenuItems(JMenu menu) { + Map map = new HashMap<>(); + for (int i = 0; i < menu.getItemCount(); i++) { + JMenuItem item = menu.getItem(i); + if (item != null) { + map.put(item.getText(), item); + } + } + return map; + } + + @Test + public void testActionAndViewShortcutsUseAlt() { + J3270App app = new J3270App(); + JMenuBar mb = app.getJMenuBar(); + assertNotNull(mb, "JMenuBar should be present"); + + JMenu viewMenu = null; + JMenu actionsMenu = null; + JMenu fileMenu = null; + JMenu editMenu = null; + + for (int i = 0; i < mb.getMenuCount(); i++) { + JMenu m = mb.getMenu(i); + if (m != null) { + if ("View".equals(m.getText())) viewMenu = m; + else if ("Actions".equals(m.getText())) actionsMenu = m; + else if ("File".equals(m.getText())) fileMenu = m; + else if ("Edit".equals(m.getText())) editMenu = m; + } + } + + assertNotNull(viewMenu, "View menu should exist"); + assertNotNull(actionsMenu, "Actions menu should exist"); + assertNotNull(fileMenu, "File menu should exist"); + assertNotNull(editMenu, "Edit menu should exist"); + + // Verify View Menu items use ALT_DOWN_MASK + Map viewItems = collectMenuItems(viewMenu); + assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS); + assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS); + assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0); + + // Verify Actions Menu items use ALT_DOWN_MASK + Map actionItems = collectMenuItems(actionsMenu); + assertAcceleratorUsesAlt(actionItems.get("Send Enter"), KeyEvent.VK_ENTER); + assertAcceleratorUsesAlt(actionItems.get("Clear"), KeyEvent.VK_K); + assertAcceleratorUsesAlt(actionItems.get("Reset"), KeyEvent.VK_R); + assertAcceleratorUsesAlt(actionItems.get("Erase Input"), KeyEvent.VK_E); + assertAcceleratorUsesAlt(actionItems.get("Attention"), KeyEvent.VK_A); + assertAcceleratorUsesAlt(actionItems.get("System Request"), KeyEvent.VK_S); + assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q); + assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L); + assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T); + + // Dispose frame + app.dispose(); + } + + private void assertAcceleratorUsesAlt(JMenuItem item, int expectedKeyCode) { + assertNotNull(item, "Menu item must exist"); + KeyStroke ks = item.getAccelerator(); + assertNotNull(ks, "Menu item " + item.getText() + " should have an accelerator"); + assertEquals(expectedKeyCode, ks.getKeyCode(), "Key code mismatch for " + item.getText()); + int mods = ks.getModifiers(); + assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0, + "Menu item " + item.getText() + " accelerator must have ALT modifier"); + assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK), + "Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers"); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConstants.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConstants.java index dfbf4a5..f0dbf72 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConstants.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConstants.java @@ -40,6 +40,8 @@ public interface ECLConstants { // Search directions int SEARCH_FORWARD = 1; int SEARCH_BACKWARD = 2; + int DIR_FORWARD = 1; + int DIR_BACKWARD = 2; // OIA Input Inhibited Reason Codes int INHIBIT_NOT_INHIBITED = 0; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java index 81f4096..f78f3bd 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java @@ -28,35 +28,43 @@ public class ECLField { /** Buffer address of the field attribute character. */ public int getStart() { return startPos; } + public int GetStart() { return getStart(); } /** First buffer address of the field data (start + 1). */ public int getDataStart() { return dataStart; } + public int GetDataStart() { return getDataStart(); } /** Last buffer address of the field data inclusive. */ public int getEnd() { return endPos; } + public int GetEnd() { return getEnd(); } /** Number of data characters in the field. */ public int getLength() { return length; } + public int GetLength() { return getLength(); } public int getStartRow() { int cols = ps.getCols(); return cols > 0 ? startPos / cols : 0; } + public int GetStartRow() { return getStartRow(); } public int getStartCol() { int cols = ps.getCols(); return cols > 0 ? startPos % cols : 0; } + public int GetStartCol() { return getStartCol(); } public int getEndRow() { int cols = ps.getCols(); return cols > 0 ? endPos / cols : 0; } + public int GetEndRow() { return getEndRow(); } public int getEndCol() { int cols = ps.getCols(); return cols > 0 ? endPos % cols : 0; } + public int GetEndCol() { return getEndCol(); } private byte getLiveAttribute() { if (ps != null && ps.getScreenBuffer() != null) { @@ -71,34 +79,42 @@ public class ECLField { public boolean isModified() { return faIsModified(getLiveAttribute() & 0xFF); } + public boolean IsModified() { return isModified(); } public boolean isProtected() { return faIsProtected(getLiveAttribute() & 0xFF); } + public boolean IsProtected() { return isProtected(); } public boolean isNumeric() { return faIsNumeric(getLiveAttribute() & 0xFF); } + public boolean IsNumeric() { return isNumeric(); } public boolean isHighIntensity() { return faIsHigh(getLiveAttribute() & 0xFF); } + public boolean IsHighIntensity() { return isHighIntensity(); } public boolean isHidden() { return faIsZero(getLiveAttribute() & 0xFF); } + public boolean IsHidden() { return isHidden(); } public boolean isDisplay() { return !isHidden(); } + public boolean IsDisplay() { return isDisplay(); } public boolean isPenSelectable() { return faIsSelectable(getLiveAttribute() & 0xFF); } + public boolean IsPenSelectable() { return isPenSelectable(); } public short getAttribute() { return (short) (getLiveAttribute() & 0xFF); } + public short GetAttribute() { return getAttribute(); } /** * Get the text contents of this field as a String. @@ -107,6 +123,7 @@ public class ECLField { if (length <= 0) return ""; return ps.getString(dataStart, length); } + public String GetText() { return getText(); } /** * Set the text contents of this field. @@ -115,6 +132,7 @@ public class ECLField { if (isProtected() || length <= 0) return; ps.setText(text, dataStart); } + public void SetText(String text) { setText(text); } /** * Get selector light pen type. @@ -129,6 +147,7 @@ public class ECLField { } return ' '; } + public char GetSelectorPenType() { return getSelectorPenType(); } /** * Actuate lightpen selection on this field ('?' -> '>'). @@ -140,6 +159,7 @@ public class ECLField { setText(">" + (t.length() > 1 ? t.substring(1) : "")); } } + public void SelectField() { selectField(); } /** * Deselect lightpen selection on this field ('>' -> '?'). @@ -151,16 +171,19 @@ public class ECLField { setText("?" + (t.length() > 1 ? t.substring(1) : "")); } } + public void DeSelectField() { deSelectField(); } /** Return true if this field wraps from bottom of screen to top. */ public boolean isWrapped() { return startPos > endPos; } + public boolean IsWrapped() { return isWrapped(); } /** Last data buffer address (same as getEnd). */ public int getDataEnd() { return endPos; } + public int GetDataEnd() { return getDataEnd(); } /** * Check if the specified buffer address is contained within this field (including its FA). @@ -178,6 +201,7 @@ public class ECLField { return pos >= startPos || pos <= endPos; } } + public boolean Contains(int pos) { return contains(pos); } /** * Check if the specified row and column is contained within this field. @@ -187,6 +211,7 @@ public class ECLField { int cols = ps.getCols(); return contains(row * cols + col); } + public boolean Contains(int row, int col) { return contains(row, col); } /** * Set the Modified Data Tag (MDT) for this field. @@ -205,6 +230,7 @@ public class ECLField { } } } + public void SetModified(boolean modified) { setModified(modified); } /** * Erase all character data within this field to nulls. @@ -225,6 +251,20 @@ public class ECLField { sb.markAllChanged(); sb.updateDisplaySnapshot(); } + public void Erase() { erase(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ECLField)) return false; + ECLField other = (ECLField) o; + return this.startPos == other.startPos && this.endPos == other.endPos && this.length == other.length; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(startPos, endPos, length); + } @Override public String toString() { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java index d5f0485..a70aea6 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java @@ -70,27 +70,38 @@ public class ECLFieldList { } } + public void Refresh() { refresh(); } + public synchronized int getFieldCount() { return fields.size(); } + public int GetFieldCount() { return getFieldCount(); } public synchronized List getFields() { return Collections.unmodifiableList(new ArrayList<>(fields)); } + public List GetFields() { return getFields(); } public synchronized ECLField getFirstField() { if (fields.isEmpty()) return null; return fields.get(0); } + public ECLField GetFirstField() { return getFirstField(); } public synchronized ECLField getNextField(ECLField prev) { if (prev == null || fields.isEmpty()) return getFirstField(); - int idx = fields.indexOf(prev); - if (idx >= 0 && idx + 1 < fields.size()) { - return fields.get(idx + 1); + for (int i = 0; i < fields.size(); i++) { + ECLField f = fields.get(i); + if (f.equals(prev) || f.getStart() == prev.getStart()) { + if (i + 1 < fields.size()) { + return fields.get(i + 1); + } + return null; + } } return null; } + public ECLField GetNextField(ECLField prev) { return getNextField(prev); } /** * Find the field that contains the specified buffer position. @@ -113,6 +124,7 @@ public class ECLFieldList { } return null; } + public ECLField FindField(int pos) { return findField(pos); } /** * Find the field at the specified 0-indexed row and column. @@ -122,20 +134,26 @@ public class ECLFieldList { int cols = screen.getCols(); return findField(row * cols + col); } + public ECLField FindField(int row, int col) { return findField(row, col); } /** * Get the field preceding the given field in the field list. */ public synchronized ECLField getPreviousField(ECLField next) { if (next == null || fields.isEmpty()) return null; - int idx = fields.indexOf(next); - if (idx > 0) { - return fields.get(idx - 1); - } else if (idx == 0) { - return fields.get(fields.size() - 1); + for (int i = 0; i < fields.size(); i++) { + ECLField f = fields.get(i); + if (f.equals(next) || f.getStart() == next.getStart()) { + if (i > 0) { + return fields.get(i - 1); + } else { + return fields.get(fields.size() - 1); + } + } } return null; } + public ECLField GetPreviousField(ECLField next) { return getPreviousField(next); } /** * Find the field at the given buffer position (alias for findField). @@ -143,6 +161,7 @@ public class ECLFieldList { public ECLField findFieldAt(int pos) { return findField(pos); } + public ECLField FindFieldAt(int pos) { return findFieldAt(pos); } /** * Find the field at the given row and column. @@ -150,6 +169,7 @@ public class ECLFieldList { public ECLField findFieldAt(int row, int col) { return findField(row, col); } + public ECLField FindFieldAt(int row, int col) { return findFieldAt(row, col); } /** * Find the field preceding the one at the given buffer position. @@ -159,6 +179,7 @@ public class ECLFieldList { if (curr == null) return null; return getPreviousField(curr); } + public ECLField FindPrevField(int pos) { return findPrevField(pos); } /** * Find the field succeeding the one at the given buffer position. @@ -168,6 +189,7 @@ public class ECLFieldList { if (curr == null) return null; return getNextField(curr); } + public ECLField FindNextField(int pos) { return findNextField(pos); } /** * Find field containing the given text string. @@ -202,4 +224,5 @@ public class ECLFieldList { } return null; } + public ECLField FindField(String text, int startPos) { return findField(text, startPos); } } 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 1db9643..caa5c90 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLOIA.java @@ -85,24 +85,52 @@ public class ECLOIA implements ECLConstants { return inputProcessor != null && inputProcessor.isInsertMode(); } + public boolean IsInsertMode() { + return isInsertMode(); + } + public boolean isNumeric() { if (screen == null || !screen.isFormatted()) return false; byte fa = screen.getFieldAttributeAt(screen.getCursorAddress()); return faIsNumeric(fa & 0xFF); } + public boolean IsNumeric() { + return isNumeric(); + } + public boolean isAlphanumeric() { return !isNumeric(); } + public boolean IsAlphanumeric() { + return isAlphanumeric(); + } + + public boolean isDBCS() { + return getAlphanumericType() == TYPE_DBCS; + } + + public boolean IsDBCS() { + return isDBCS(); + } + public boolean isMessageWaiting() { return false; } + public boolean IsMessageWaiting() { + return isMessageWaiting(); + } + public boolean isCommError() { return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected(); } + public boolean IsCommError() { + return isCommError(); + } + private int inhibitOverride = -1; public void setInputInhibited(int reason) { @@ -112,6 +140,10 @@ public class ECLOIA implements ECLConstants { } } + public void SetInputInhibited(int reason) { + setInputInhibited(reason); + } + /** * Get the alphanumeric character entry type allowed at current cursor position. * Returns TYPE_ALPHANUMERIC (0), TYPE_NUMERIC (1), or TYPE_DBCS (2). @@ -132,6 +164,10 @@ public class ECLOIA implements ECLConstants { return TYPE_ALPHANUMERIC; } + public int GetAlphanumericType() { + return getAlphanumericType(); + } + public String getAlphanumericTypeString() { switch (getAlphanumericType()) { case TYPE_NUMERIC: return "N"; @@ -141,38 +177,74 @@ public class ECLOIA implements ECLConstants { } } + public String GetAlphanumericTypeString() { + return getAlphanumericTypeString(); + } + public boolean isXSystem() { return getInputInhibited() == INHIBIT_SYSTEM_LOCK; } + public boolean IsXSystem() { + return isXSystem(); + } + public boolean isXProt() { return getInputInhibited() == INHIBIT_PROTECTED_FIELD; } + public boolean IsXProt() { + return isXProt(); + } + public boolean isXNum() { return getInputInhibited() == INHIBIT_NUMERIC_ONLY; } + public boolean IsXNum() { + return isXNum(); + } + public boolean isXWait() { return isXSystem(); } + public boolean IsXWait() { + return isXWait(); + } + public boolean isXInsert() { return isInsertMode(); } + public boolean IsXInsert() { + return isXInsert(); + } + public boolean isXComm() { return isCommError() || getInputInhibited() == INHIBIT_COMM_CHECK; } + public boolean IsXComm() { + return isXComm(); + } + public boolean isXOverflow() { return getInputInhibited() == INHIBIT_OVERFLOW; } + public boolean IsXOverflow() { + return isXOverflow(); + } + public boolean isXOperatorDue() { return getInputInhibited() == INHIBIT_OPERATOR_DUE; } + public boolean IsXOperatorDue() { + return isXOperatorDue(); + } + public String getStatusString() { int inhibit = getInputInhibited(); switch (inhibit) { @@ -188,6 +260,10 @@ public class ECLOIA implements ECLConstants { } } + public String GetStatusString() { + return getStatusString(); + } + /** * Get the current Input Inhibited code. * Returns one of INHIBIT_* constants from ECLConstants. @@ -205,6 +281,18 @@ public class ECLOIA implements ECLConstants { return INHIBIT_NOT_INHIBITED; } + public int GetInputInhibited() { + return getInputInhibited(); + } + + public int getInputInhibitedType() { + return getInputInhibited(); + } + + public int GetInputInhibitedType() { + return getInputInhibited(); + } + /** * Block until the keyboard becomes unlocked and ready for user input, or timeout expires. * @return true if keyboard unlocked, false if timeout occurred. diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java index 738acd1..3220cb1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java @@ -205,21 +205,75 @@ public class ECLPS implements ECLConstants { return SearchPSExt(text, 1, getSize(), SEARCH_FORWARD, false, true); } + public int searchPS(String text) { + return SearchPS(text); + } + + public int SearchPS(String text, int startPos) { + return SearchPSExt(text, startPos, getSize(), SEARCH_FORWARD, false, true); + } + + public int searchPS(String text, int startPos) { + return SearchPS(text, startPos); + } + public int SearchPS(String text, int startRow, int startCol) { int pos = (startRow - 1) * getCols() + startCol; return SearchPSExt(text, pos, getSize(), SEARCH_FORWARD, false, true); } + public int searchPS(String text, int startRow, int startCol) { + return SearchPS(text, startRow, startCol); + } + public int SearchPS(String text, int startRow, int startCol, int dir) { int pos = (startRow - 1) * getCols() + startCol; return SearchPSExt(text, pos, getSize(), dir, false, true); } + public int searchPS(String text, int startRow, int startCol, int dir) { + return SearchPS(text, startRow, startCol, dir); + } + public int SearchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) { int pos = (startRow - 1) * getCols() + startCol; return SearchPSExt(text, pos, getSize(), dir, ignoreCase, true); } + public int searchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) { + return SearchPS(text, startRow, startCol, dir, ignoreCase); + } + + public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol) { + int sPos = (startRow - 1) * getCols() + startCol; + int ePos = (endRow - 1) * getCols() + endCol; + return SearchPSExt(text, sPos, ePos, SEARCH_FORWARD, false, false); + } + + public int searchPS(String text, int startRow, int startCol, int endRow, int endCol) { + return SearchPS(text, startRow, startCol, endRow, endCol); + } + + public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir) { + int sPos = (startRow - 1) * getCols() + startCol; + int ePos = (endRow - 1) * getCols() + endCol; + return SearchPSExt(text, sPos, ePos, dir, false, false); + } + + public int searchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir) { + return SearchPS(text, startRow, startCol, endRow, endCol, dir); + } + + public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase) { + int sPos = (startRow - 1) * getCols() + startCol; + int ePos = (endRow - 1) * getCols() + endCol; + return SearchPSExt(text, sPos, ePos, dir, ignoreCase, false); + } + + public int searchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase) { + return SearchPS(text, startRow, startCol, endRow, endCol, dir, ignoreCase); + } + /** * SearchPS extended method conforming to IBM ECL specification. * Uses 1-based positions and returns 1-based index (or 0 if not found). @@ -263,6 +317,21 @@ public class ECLPS implements ECLConstants { return 0; } + public int searchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) { + return SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap); + } + + public int SearchPSExt(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase, boolean wrap) { + int cols = getCols(); + int sPos = (startRow - 1) * cols + startCol; + int ePos = (endRow - 1) * cols + endCol; + return SearchPSExt(text, sPos, ePos, dir, ignoreCase, wrap); + } + + public int searchPSExt(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase, boolean wrap) { + return SearchPSExt(text, startRow, startCol, endRow, endCol, dir, ignoreCase, wrap); + } + private boolean matchesAt(String screenText, String target, int pos, int size) { int len = target.length(); for (int j = 0; j < len; j++) { @@ -303,6 +372,10 @@ public class ECLPS implements ECLConstants { return sb.toString(); } + public String CopyString(int sRow, int sCol, int eRow, int eCol) { + return copyString(sRow, sCol, eRow, eCol); + } + /** * Paste a multi-line rectangular block of text starting at (row, col). */ @@ -320,6 +393,12 @@ public class ECLPS implements ECLConstants { String line = lines[i]; for (int c = 0; c < line.length() && (col + c) < cols; c++) { int pos = targetRow * cols + (col + c); + if (screen.isFormatted()) { + byte fa = screen.getFieldAttributeAt(pos); + if (faIsProtected(fa & 0xFF) || screen.getCell(pos).isFieldAttribute()) { + continue; + } + } setCursorPos(pos); if (inputProcessor != null) { inputProcessor.typeCharacter(line.charAt(c)); @@ -330,6 +409,18 @@ public class ECLPS implements ECLConstants { return count; } + public int PasteString(String text, int row, int col) { + return pasteString(text, row, col); + } + + public int pasteRectangular(String text, int row, int col) { + return pasteString(text, row, col); + } + + public int PasteRectangular(String text, int row, int col) { + return pasteString(text, row, col); + } + /** * Paste text with line wrapping across unprotected fields. */ @@ -365,6 +456,108 @@ public class ECLPS implements ECLConstants { return charsPasted; } + public int PasteLineWrap(String text, int startPos, int endCol, boolean wordWrap) { + return pasteLineWrap(text, startPos, endCol, wordWrap); + } + + // ========== Entry Assist & DOC Mode Operations ========== + + public boolean isEntryAssistDOCmode() { return screen != null && screen.isEntryAssistDOCmode(); } + public boolean IsEntryAssistDOCmode() { return isEntryAssistDOCmode(); } + public void setEntryAssistDOCmode(boolean bl) { if (screen != null) screen.setEntryAssistDOCmode(bl); } + public void SetEntryAssistDOCmode(boolean bl) { setEntryAssistDOCmode(bl); } + + public boolean isEntryAssistWordWrap() { return screen != null && screen.isEntryAssistWordWrap(); } + public boolean IsEntryAssistWordWrap() { return isEntryAssistWordWrap(); } + public void setEntryAssistWordWrap(boolean bl) { if (screen != null) screen.setEntryAssistWordWrap(bl); } + public void SetEntryAssistWordWrap(boolean bl) { setEntryAssistWordWrap(bl); } + + public int getEntryAssistStartColumn() { return screen != null ? screen.getEntryAssistStartColumn() : 0; } + public int GetEntryAssistStartColumn() { return getEntryAssistStartColumn(); } + public void setEntryAssistStartColumn(int n) { if (screen != null) screen.setEntryAssistStartColumn(n); } + public void SetEntryAssistStartColumn(int n) { setEntryAssistStartColumn(n); } + + public int getEntryAssistEndColumn() { return screen != null ? screen.getEntryAssistEndColumn() : 0; } + public int GetEntryAssistEndColumn() { return getEntryAssistEndColumn(); } + public void setEntryAssistEndColumn(int n) { if (screen != null) screen.setEntryAssistEndColumn(n); } + public void SetEntryAssistEndColumn(int n) { setEntryAssistEndColumn(n); } + + public int[] getEntryAssistTabStops() { return screen != null ? screen.getEntryAssistTabStops() : null; } + public int[] GetEntryAssistTabStops() { return getEntryAssistTabStops(); } + public void setEntryAssistTabStops(int[] stops) { if (screen != null) screen.setEntryAssistTabStops(stops); } + public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); } + + public void processWordTab(boolean forward) { if (inputProcessor != null) inputProcessor.processWordTab(forward); else if (screen != null) screen.processWordTab(forward); } + public void ProcessWordTab(boolean forward) { processWordTab(forward); } + public void wordTab(boolean forward) { processWordTab(forward); } + public void WordTab(boolean forward) { processWordTab(forward); } + + public void processDeleteWord() { if (inputProcessor != null) inputProcessor.processDeleteWord(); else if (screen != null) screen.processDeleteWord(); } + public void ProcessDeleteWord() { processDeleteWord(); } + public void deleteWord() { processDeleteWord(); } + public void DeleteWord() { processDeleteWord(); } + + public void processWordLeft() { if (inputProcessor != null) inputProcessor.processWordLeft(); } + public void ProcessWordLeft() { processWordLeft(); } + public void wordLeft() { processWordLeft(); } + public void WordLeft() { processWordLeft(); } + + public void processWordRight() { if (inputProcessor != null) inputProcessor.processWordRight(); } + public void ProcessWordRight() { processWordRight(); } + public void wordRight() { processWordRight(); } + public void WordRight() { processWordRight(); } + + public void processFieldEnd() { if (inputProcessor != null) inputProcessor.processFieldEnd(); } + public void ProcessFieldEnd() { processFieldEnd(); } + public void fieldEnd() { processFieldEnd(); } + public void FieldEnd() { processFieldEnd(); } + + // ========== Field Management Accessors ========== + + public ECLField getField(int pos) { + return getFieldList().findField(pos); + } + + public ECLField GetField(int pos) { + return getField(pos); + } + + public ECLField getField(int row, int col) { + return getFieldList().findField(row, col); + } + + public ECLField GetField(int row, int col) { + return getField(row, col); + } + + public ECLField getFirstField() { + return getFieldList().getFirstField(); + } + + public ECLField GetFirstField() { + return getFirstField(); + } + + public ECLField getNextField(ECLField prev) { + return getFieldList().getNextField(prev); + } + + public ECLField GetNextField(ECLField prev) { + return getNextField(prev); + } + + public ECLField getPreviousField(ECLField next) { + return getFieldList().getPreviousField(next); + } + + public ECLField GetPreviousField(ECLField next) { + return getPreviousField(next); + } + + public ECLFieldList GetFieldList() { + return getFieldList(); + } + // ========== ECLPS Event Listener Management ========== private final java.util.List psListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); @@ -455,6 +648,29 @@ public class ECLPS implements ECLConstants { /** * Send keystrokes with configurable inter-character or inter-mnemonic delay. */ + public void sendCharacters(String keys) { + sendCharacters(keys, 0); + } + + public void SendCharacters(String keys) { + sendCharacters(keys, 0); + } + + public void SendCharacters(String keys, int delayMs) { + sendCharacters(keys, delayMs); + } + + public void sendCharacters(String keys, int row, int col, int delayMs) { + if (row > 0 && col > 0) { + setCursorPos(row - 1, col - 1); + } + sendCharacters(keys, delayMs); + } + + public void SendCharacters(String keys, int row, int col, int delayMs) { + sendCharacters(keys, row, col, delayMs); + } + public void sendCharacters(String keys, int delayMs) { if (keys == null || keys.isEmpty()) return; if (delayMs <= 0) { @@ -466,14 +682,20 @@ public class ECLPS implements ECLConstants { int len = keys.length(); while (i < len) { if (keys.charAt(i) == '[') { - int close = keys.indexOf(']', i); - if (close > i) { - String mnemonic = keys.substring(i, close + 1); - sendKeys(mnemonic); - i = close + 1; + if (i + 1 < len && keys.charAt(i + 1) == '[') { + // Escaped bracket "[[" + sendKeys("["); + i += 2; } else { - sendKeys(keys.substring(i, i + 1)); - i++; + int close = keys.indexOf(']', i); + if (close > i) { + String mnemonic = keys.substring(i, close + 1); + sendKeys(mnemonic); + i = close + 1; + } else { + sendKeys(keys.substring(i, i + 1)); + i++; + } } } else { sendKeys(keys.substring(i, i + 1)); 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 e42b257..77a685b 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -131,6 +131,22 @@ 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()) { + int curCol = baddr % screen.getCols(); + int endCol = screen.getEntryAssistEndColumn(); + int startCol = screen.getEntryAssistStartColumn(); + if (curCol >= endCol) { + screen.handleWordWrap(baddr, ch); + baddr = screen.getCursorAddress(); + if (ch == ' ' && (baddr % screen.getCols()) == startCol) { + screen.markAllChanged(); + screen.updateDisplaySnapshot(); + return; + } + } + } + if (screen.isFormatted()) { // Check if cursor is at a field attribute ExtendedAttribute ea = screen.getCell(baddr); 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 1f3a529..0bdc903 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java @@ -909,19 +909,108 @@ public class ScreenBuffer { // ========== Entry Assist & DOC Mode Operations ========== public boolean isEntryAssistDOCmode() { return docMode; } + public boolean IsEntryAssistDOCmode() { return isEntryAssistDOCmode(); } public void setEntryAssistDOCmode(boolean bl) { this.docMode = bl; } + public void SetEntryAssistDOCmode(boolean bl) { setEntryAssistDOCmode(bl); } public boolean isEntryAssistWordWrap() { return wordWrap; } + public boolean IsEntryAssistWordWrap() { return isEntryAssistWordWrap(); } public void setEntryAssistWordWrap(boolean bl) { this.wordWrap = bl; } + public void SetEntryAssistWordWrap(boolean bl) { setEntryAssistWordWrap(bl); } public int getEntryAssistStartColumn() { return docStartCol; } + public int GetEntryAssistStartColumn() { return getEntryAssistStartColumn(); } public void setEntryAssistStartColumn(int n) { this.docStartCol = Math.max(0, n); } + public void SetEntryAssistStartColumn(int n) { setEntryAssistStartColumn(n); } public int getEntryAssistEndColumn() { return docEndCol >= 0 ? docEndCol : (cols - 1); } + public int GetEntryAssistEndColumn() { return getEntryAssistEndColumn(); } public void setEntryAssistEndColumn(int n) { this.docEndCol = n; } + public void SetEntryAssistEndColumn(int n) { setEntryAssistEndColumn(n); } public int[] getEntryAssistTabStops() { return tabStops; } + public int[] GetEntryAssistTabStops() { return getEntryAssistTabStops(); } public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; } + public void SetEntryAssistTabStops(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; + int size = rows * cols; + if (size <= 0) return false; + curAddr = ((curAddr % size) + size) % size; + + int curRow = curAddr / cols; + int curCol = curAddr % cols; + int endCol = getEntryAssistEndColumn(); + int startCol = getEntryAssistStartColumn(); + + if (curCol < endCol) return false; + + if (typedChar == ' ') { + int nextRow = (curRow + 1) % rows; + setCursorPosition(nextRow, startCol); + return true; + } + + // Scan backwards to find the start of the current word on this row + int rowStartAddr = curRow * cols + startCol; + int scan = curAddr - 1; + while (scan >= rowStartAddr) { + ExtendedAttribute ea = getCell(scan); + if (ea.isFieldAttribute()) break; + int ec = ea.ec & 0xFF; + if (ec == 0 || ec == 0x40 || ea.ucs4 == ' ' || ea.ucs4 == 0) { + break; + } + scan--; + } + + int wordStartAddr = scan + 1; + int wordStartCol = wordStartAddr % cols; + + if (wordStartCol > startCol && wordStartAddr < curAddr) { + int wordLen = curAddr - wordStartAddr; + ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen]; + for (int i = 0; i < wordLen; i++) { + wordCells[i] = new ExtendedAttribute(); + wordCells[i].copyFrom(getCell(wordStartAddr + i)); + getCell(wordStartAddr + i).clear(); + } + + int nextRow = (curRow + 1) % rows; + int targetAddr = nextRow * cols + startCol; + if (formatted) { + targetAddr = findNextUnprotected(targetAddr - 1); + } + + for (int i = 0; i < wordLen; i++) { + int dst = (targetAddr + i) % size; + if (!getCell(dst).isFieldAttribute()) { + getCell(dst).copyFrom(wordCells[i]); + } + } + + setCursorAddress((targetAddr + wordLen) % size); + screenChanged = true; + updateDisplaySnapshot(); + return true; + } else { + // Word spans entire line or starts at startCol, wrap to next line + int nextRow = (curRow + 1) % rows; + int targetAddr = nextRow * cols + startCol; + if (formatted) { + targetAddr = findNextUnprotected(targetAddr - 1); + } + setCursorAddress(targetAddr); + screenChanged = true; + updateDisplaySnapshot(); + return true; + } + } public synchronized void processWordTab(boolean forward) { int size = rows * cols; @@ -1060,4 +1149,12 @@ public class ScreenBuffer { screenChanged = true; updateDisplaySnapshot(); } + + /** + * Inspect and balance Shift-Out (0x0E) and Shift-In (0x0F) markers, ensuring DBCS integrity. + */ + public synchronized void balanceSOSI() { + cleanAdjacentSISO(0); + processSOSI(); + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetConnection.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetConnection.java index faa2e8e..8bd9d28 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetConnection.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetConnection.java @@ -44,11 +44,41 @@ public class TelnetConnection { */ public void connect() throws IOException { ConnectionConfig.ProxyType proxyType = config.getProxyType(); - boolean hasProxy = proxyType != null && proxyType != ConnectionConfig.ProxyType.NONE && - config.getProxyHost() != null && !config.getProxyHost().trim().isEmpty(); + String proxyHost = config.getProxyHost(); + int proxyPort = config.getProxyPort(); + String proxyUser = config.getProxyUsername(); + String proxyPass = config.getProxyPassword(); - String connectHost = hasProxy ? config.getProxyHost().trim() : config.getHost(); - int connectPort = hasProxy ? (config.getProxyPort() > 0 ? config.getProxyPort() : (proxyType == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080)) : config.getPort(); + if ((proxyType == null || proxyType == ConnectionConfig.ProxyType.NONE) && (proxyHost == null || proxyHost.trim().isEmpty())) { + // Check JVM system properties (matching IBM HoD browser/system default fallback) + String sysSocks = System.getProperty("socksProxyHost"); + String sysHttp = System.getProperty("http.proxyHost"); + if (sysSocks != null && !sysSocks.trim().isEmpty()) { + proxyType = ConnectionConfig.ProxyType.SOCKS5; + proxyHost = sysSocks.trim(); + String portStr = System.getProperty("socksProxyPort"); + if (portStr != null) { + try { proxyPort = Integer.parseInt(portStr.trim()); } catch (NumberFormatException ignored) {} + } + proxyUser = System.getProperty("java.net.socks.username"); + proxyPass = System.getProperty("java.net.socks.password"); + } else if (sysHttp != null && !sysHttp.trim().isEmpty()) { + proxyType = ConnectionConfig.ProxyType.HTTP; + proxyHost = sysHttp.trim(); + String portStr = System.getProperty("http.proxyPort"); + if (portStr != null) { + try { proxyPort = Integer.parseInt(portStr.trim()); } catch (NumberFormatException ignored) {} + } + proxyUser = System.getProperty("http.proxyUser"); + proxyPass = System.getProperty("http.proxyPassword"); + } + } + + boolean hasProxy = proxyType != null && proxyType != ConnectionConfig.ProxyType.NONE && + proxyHost != null && !proxyHost.trim().isEmpty(); + + String connectHost = hasProxy ? proxyHost.trim() : config.getHost(); + int connectPort = hasProxy ? (proxyPort > 0 ? proxyPort : (proxyType == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080)) : config.getPort(); log.info("Connecting TCP socket to " + connectHost + ":" + connectPort + (hasProxy ? " (via " + proxyType + " proxy for " + config.getHost() + ":" + config.getPort() + ")" : "") + @@ -67,13 +97,13 @@ public class TelnetConnection { if (hasProxy) { switch (proxyType) { case HTTP: - establishHttpProxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword()); + establishHttpProxy(rawSocket, config.getHost(), config.getPort(), proxyUser, proxyPass); break; case SOCKS4: - establishSocks4Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername()); + establishSocks4Proxy(rawSocket, config.getHost(), config.getPort(), proxyUser); break; case SOCKS5: - establishSocks5Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword()); + establishSocks5Proxy(rawSocket, config.getHost(), config.getPort(), proxyUser, proxyPass); break; default: break; diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLOIAPhase4Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLOIAPhase4Test.java new file mode 100644 index 0000000..c0b6f35 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLOIAPhase4Test.java @@ -0,0 +1,113 @@ +package haus.nightmare.lib3270j.ecl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ExtendedAttribute; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import static haus.nightmare.lib3270j.ecl.ECLConstants.*; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +public class ECLOIAPhase4Test { + + private ScreenBuffer screen; + private EbcdicTranslator translator; + private InputProcessor input; + private ECLOIA oia; + + @BeforeEach + public void setup() { + translator = new EbcdicTranslator(); + screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator); + input = new InputProcessor(screen, translator, null); + oia = new ECLOIA(screen, input, null); + } + + @Test + public void testInhibitionCodesAndStatusStrings() { + screen.erase(false); + assertEquals(INHIBIT_NOT_INHIBITED, oia.GetInputInhibited()); + assertEquals(INHIBIT_NOT_INHIBITED, oia.GetInputInhibitedType()); + assertEquals("READY", oia.GetStatusString()); + + // Test X-SYSTEM + oia.SetInputInhibited(INHIBIT_SYSTEM_LOCK); + assertTrue(oia.IsXSystem()); + assertTrue(oia.IsXWait()); + assertEquals("X-SYSTEM", oia.GetStatusString()); + + // Test X-NUM + oia.SetInputInhibited(INHIBIT_NUMERIC_ONLY); + assertTrue(oia.IsXNum()); + assertEquals("X-NUM", oia.GetStatusString()); + + // Test X-PROT + oia.SetInputInhibited(INHIBIT_PROTECTED_FIELD); + assertTrue(oia.IsXProt()); + assertEquals("X-PROT", oia.GetStatusString()); + + // Test X-OVERFLOW + oia.SetInputInhibited(INHIBIT_OVERFLOW); + assertTrue(oia.IsXOverflow()); + assertEquals("X-OVERFLOW", oia.GetStatusString()); + + // Test X-COMM + oia.SetInputInhibited(INHIBIT_COMM_CHECK); + assertTrue(oia.IsXComm()); + assertEquals("X-COMM", oia.GetStatusString()); + + // Test X-OP + oia.SetInputInhibited(INHIBIT_OPERATOR_DUE); + assertTrue(oia.IsXOperatorDue()); + assertEquals("X-OP", oia.GetStatusString()); + + // Test X-INSERT + oia.SetInputInhibited(INHIBIT_NOT_INHIBITED); + input.setInsertMode(true); + assertTrue(oia.IsInsertMode()); + assertTrue(oia.isXInsert()); + assertEquals("X-INSERT", oia.GetStatusString()); + } + + @Test + public void testAlphanumericTypeGetters() { + screen.erase(false); + assertEquals(TYPE_ALPHANUMERIC, oia.GetAlphanumericType()); + assertEquals("A", oia.GetAlphanumericTypeString()); + assertTrue(oia.IsAlphanumeric()); + assertFalse(oia.IsNumeric()); + assertFalse(oia.IsDBCS()); + + // Numeric field + screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_NUMERIC)); + screen.setCursorAddress(1); + assertEquals(TYPE_NUMERIC, oia.GetAlphanumericType()); + assertEquals("N", oia.GetAlphanumericTypeString()); + assertTrue(oia.IsNumeric()); + assertFalse(oia.IsAlphanumeric()); + + // DBCS cell + ExtendedAttribute ea = screen.getCell(1); + ea.cs = ExtendedAttribute.CS_DBCS; + assertEquals(TYPE_DBCS, oia.GetAlphanumericType()); + assertEquals("D", oia.GetAlphanumericTypeString()); + assertTrue(oia.IsDBCS()); + } + + @Test + public void testSynchronizationPrimitives() { + screen.erase(false); + assertTrue(oia.WaitForInput(100)); + assertTrue(oia.WaitForSystemAvailable(100)); + assertTrue(oia.WaitForAppAvailable(100)); + + // Lock keyboard, wait should timeout + input.setKeyboardLocked(true); + assertFalse(oia.WaitForInput(50)); + input.setKeyboardLocked(false); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLPSPhase4Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLPSPhase4Test.java new file mode 100644 index 0000000..dfde36e --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/ecl/ECLPSPhase4Test.java @@ -0,0 +1,166 @@ +package haus.nightmare.lib3270j.ecl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import static haus.nightmare.lib3270j.ecl.ECLConstants.*; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +public class ECLPSPhase4Test { + + private ScreenBuffer screen; + private EbcdicTranslator translator; + private InputProcessor input; + private ECLPS ps; + + @BeforeEach + public void setup() { + translator = new EbcdicTranslator(); + screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator); + input = new InputProcessor(screen, translator, null); + ps = new ECLPS(screen, input, translator); + } + + @Test + public void testSearchPSComprehensiveOverloads() { + screen.erase(false); + screen.setText("IBM Host On-Demand ECL Automation Presentation Space"); + + // 1-based indexing overloads + assertEquals(5, ps.SearchPS("Host")); + assertEquals(5, ps.searchPS("Host")); + assertEquals(5, ps.SearchPS("Host", 1)); + assertEquals(5, ps.SearchPS("Host", 1, SEARCH_FORWARD)); + assertEquals(5, ps.SearchPS("Host", 1, 1)); + assertEquals(5, ps.SearchPS("host", 1, 1, SEARCH_FORWARD, true)); + assertEquals(0, ps.SearchPS("host", 1, 1, SEARCH_FORWARD, false)); + + // Bounded range searches + assertEquals(5, ps.SearchPS("Host", 1, 1, 1, 30)); + assertEquals(0, ps.SearchPS("Host", 1, 10, 1, 30)); // starts after "Host" + assertEquals(5, ps.SearchPS("host", 1, 1, 1, 30, DIR_FORWARD, true)); + + // SearchPSExt + int pos1 = ps.SearchPSExt("ECL", 1, 50, SEARCH_FORWARD, false, false); + assertEquals(20, pos1); + + int pos2 = ps.SearchPSExt("ecl", 1, 1, 1, 50, DIR_FORWARD, true, false); + assertEquals(20, pos2); + + // Backward search + screen.setChar(1, 10, 'Z'); + screen.setChar(1, 11, 'O'); + screen.setChar(1, 12, 'O'); + + int backFound = ps.SearchPSExt("ZOO", 200, 1, SEARCH_BACKWARD, false, false); + assertEquals(91, backFound); // row 1 col 10 is 80 + 10 + 1 = 91 + } + + @Test + public void testRectangularCopyAndPaste() { + screen.erase(false); + // Fill a small grid across rows 0, 1, 2 + String r0 = "ROW0_DATA_TEST"; + for (int c = 0; c < r0.length(); c++) screen.setChar(0, c, r0.charAt(c)); + String r1 = "ROW1_DATA_TEST"; + for (int c = 0; c < r1.length(); c++) screen.setChar(1, c, r1.charAt(c)); + String r2 = "ROW2_DATA_TEST"; + for (int c = 0; c < r2.length(); c++) screen.setChar(2, c, r2.charAt(c)); + + // Copy rectangular region rows 0..2, cols 0..3 + String copied = ps.copyString(0, 0, 2, 3); + assertEquals("ROW0\nROW1\nROW2", copied); + + // Test inverted coordinate normalization + String inverted = ps.CopyString(2, 3, 0, 0); + assertEquals("ROW0\nROW1\nROW2", inverted); + + // Paste rectangular text + screen.erase(false); + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.setFieldAttribute(80, (byte) FA_PRINTABLE); + screen.setFieldAttribute(160, (byte) FA_PRINTABLE); + + int pasted = ps.PasteString("ABC\nDEF\nGHI", 0, 1); + assertEquals(9, pasted); + + assertEquals("ABC", ps.getString(1, 3)); + assertEquals("DEF", ps.getString(81, 3)); + assertEquals("GHI", ps.getString(161, 3)); + } + + @Test + public void testSendCharactersWithMnemonicsAndEscaping() { + screen.erase(false); + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.setFieldAttribute(20, (byte) FA_PRINTABLE); + screen.setCursorAddress(1); + + // Test mnemonic [tab] and escaped bracket [[ + ps.sendCharacters("A[[B[tab]C", 1); + + assertEquals('A', screen.getChar(0, 1)); + assertEquals('[', screen.getChar(0, 2)); + assertEquals('B', screen.getChar(0, 3)); + // After tab, at pos 21 + assertEquals('C', screen.getChar(0, 21)); + } + + @Test + public void testEntryAssistPassThroughMethods() { + screen.erase(false); + ps.SetEntryAssistDOCmode(true); + ps.SetEntryAssistWordWrap(true); + ps.SetEntryAssistStartColumn(4); + ps.SetEntryAssistEndColumn(72); + ps.SetEntryAssistTabStops(new int[]{ 8, 16, 24, 32 }); + + assertTrue(ps.IsEntryAssistDOCmode()); + assertTrue(ps.IsEntryAssistWordWrap()); + assertEquals(4, ps.GetEntryAssistStartColumn()); + assertEquals(72, ps.GetEntryAssistEndColumn()); + assertArrayEquals(new int[]{ 8, 16, 24, 32 }, ps.GetEntryAssistTabStops()); + + ps.setCursorPos(0, 0); + ps.WordTab(true); + assertEquals(8, ps.getCursorCol()); + + ps.WordTab(true); + assertEquals(16, ps.getCursorCol()); + + ps.WordTab(false); + assertEquals(8, ps.getCursorCol()); + } + + @Test + public void testFieldManagementAccessors() { + screen.erase(false); + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.setFieldAttribute(50, (byte) (FA_PRINTABLE | FA_PROTECT)); + + assertNotNull(ps.GetFieldList()); + assertEquals(2, ps.GetFieldList().GetFieldCount()); + + ECLField f0 = ps.GetFirstField(); + assertNotNull(f0); + assertEquals(0, f0.GetStart()); + assertEquals(1, f0.GetDataStart()); + assertEquals(49, f0.GetEnd()); + assertEquals(49, f0.GetLength()); + assertFalse(f0.IsProtected()); + + ECLField f1 = ps.GetNextField(f0); + assertNotNull(f1); + assertEquals(50, f1.GetStart()); + assertTrue(f1.IsProtected()); + + assertEquals(f0.GetStart(), ps.GetPreviousField(f1).GetStart()); + assertEquals(f0.GetStart(), ps.GetField(25).GetStart()); + assertEquals(f1.GetStart(), ps.GetField(60).GetStart()); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaPhase3Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaPhase3Test.java new file mode 100644 index 0000000..bb208a1 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaPhase3Test.java @@ -0,0 +1,304 @@ +package haus.nightmare.lib3270j.graphics; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.awt.Point; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verification test suite for Phase 3: Host Graphics (GOCA / 3179G / Programmed Symbols) (GDDM). + * Covers items 3.1 through 3.6 of the PhasedUpdates specification. + */ +public class GocaPhase3Test { + + // ==================================================================================================== + // ITEM 3.1: GOCA Order 0x23 / 0x27 - Viewing Window (Clipping Viewport) + // ==================================================================================================== + @Test + @DisplayName("Item 3.1: Viewing Window (0x23 / 0x27) clipping bounds and raster isolation") + public void testItem3_1_ViewingWindowClipping() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + assertFalse(plane.isViewingWindowActive()); + + // Define Viewing Window: x in [-100, 100], y in [-100, 100] + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(GocaConstants.G_GSVW_DEF); // 0x23 + out.write(0x08); // 8-byte payload + out.write((byte) 0xFF); out.write((byte) 0x9C); // xMin = -100 + out.write((byte) 0xFF); out.write((byte) 0x9C); // yMin = -100 + out.write(0x00); out.write(100); // xMax = 100 + out.write(0x00); out.write(100); // yMax = 100 + + byte[] vwOrder = out.toByteArray(); + decoder.decodeGoca(vwOrder, 0, vwOrder.length); + + assertTrue(plane.isViewingWindowActive()); + assertEquals(-100, plane.getViewingWindowXMin()); + assertEquals(-100, plane.getViewingWindowYMin()); + assertEquals(100, plane.getViewingWindowXMax()); + assertEquals(100, plane.getViewingWindowYMax()); + + // 1. Draw a line entirely outside viewing window: (-300, -150) to (-200, -150) + ByteArrayOutputStream lineOutside = new ByteArrayOutputStream(); + lineOutside.write(GocaConstants.G_GSCOL); lineOutside.write(0x02); // Red + lineOutside.write(GocaConstants.G_GLINE); lineOutside.write(0x08); + lineOutside.write((byte) 0xFE); lineOutside.write((byte) 0xD4); // -300 + lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x6A); // -150 + lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x38); // -200 + lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x6A); // -150 + + byte[] lineOutBytes = lineOutside.toByteArray(); + decoder.decodeGoca(lineOutBytes, 0, lineOutBytes.length); + + int pxClipped = plane.mapX(-300); + int pyClipped = plane.mapY(-150); + int pixelVal = plane.getRgbBuffer()[pyClipped * plane.getCanvasWidth() + pxClipped]; + assertEquals(0, pixelVal, "Pixel outside viewing window must be clipped (0)"); + + // 2. Draw a line inside viewing window: (0, 0) to (50, 50) + ByteArrayOutputStream lineInside = new ByteArrayOutputStream(); + lineInside.write(GocaConstants.G_GSCOL); lineInside.write(0x04); // Green + lineInside.write(GocaConstants.G_GLINE); lineInside.write(0x08); + lineInside.write(0x00); lineInside.write(0); lineInside.write(0x00); lineInside.write(0); + lineInside.write(0x00); lineInside.write(50); lineInside.write(0x00); lineInside.write(50); + + byte[] lineInBytes = lineInside.toByteArray(); + decoder.decodeGoca(lineInBytes, 0, lineInBytes.length); + + int pxIn = plane.mapX(0); + int pyIn = plane.mapY(0); + int pixelInVal = plane.getRgbBuffer()[pyIn * plane.getCanvasWidth() + pxIn]; + assertNotEquals(0, pixelInVal, "Pixel inside viewing window must be rendered"); + + // 3. Clear viewing window via zero-length payload + ByteArrayOutputStream clearVw = new ByteArrayOutputStream(); + clearVw.write(GocaConstants.G_GSVW); + clearVw.write(0x00); + byte[] clearVwBytes = clearVw.toByteArray(); + decoder.decodeGoca(clearVwBytes, 0, clearVwBytes.length); + + assertFalse(plane.isViewingWindowActive()); + } + + // ==================================================================================================== + // ITEM 3.2: GOCA Order 0x22 - Segment Characteristics + // ==================================================================================================== + @Test + @DisplayName("Item 3.2: Segment Characteristics (0x22) flags decoding") + public void testItem3_2_SegmentCharacteristics() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + // Chained (0x80) | Dynamic (0x40) | Visible (0x00) -> 0xC0 + decoder.processSegmentCharacteristics(new byte[]{(byte) 0xC0}); + assertTrue(decoder.isSegChained()); + assertTrue(decoder.isSegDynamic()); + assertTrue(decoder.isSegVisible()); + + // Non-chained (0x00) | Static (0x00) | Invisible (0x20) -> 0x20 + decoder.processSegmentCharacteristics(new byte[]{(byte) 0x20}); + assertFalse(decoder.isSegChained()); + assertFalse(decoder.isSegDynamic()); + assertFalse(decoder.isSegVisible()); + + // Unchained (0x00) | Dynamic (0x40) | Visible (0x00) -> 0x40 + decoder.processSegmentCharacteristics(new byte[]{(byte) 0x40}); + assertFalse(decoder.isSegChained()); + assertTrue(decoder.isSegDynamic()); + assertTrue(decoder.isSegVisible()); + } + + // ==================================================================================================== + // ITEM 3.3: GOCA Order 0x11 - Fractional Line Width Calculation + // ==================================================================================================== + @Test + @DisplayName("Item 3.3: Fractional Line Width (0x11) fixed-point ratio and plane synchronization") + public void testItem3_3_FractionalLineWidth() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + // 1. Fixed point: integer = 3, fraction = 128/256 (0.5) -> 3.5 + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(GocaConstants.G_GSFLW); // 0x11 + out.write(0x03); // Integer part + out.write(0x80); // Fractional part (128 / 256 = 0.5) + + byte[] flwBytes = out.toByteArray(); + decoder.decodeGoca(flwBytes, 0, flwBytes.length); + + assertEquals(3.5, decoder.getFractionalLineWidth(), 0.001); + assertEquals(3.5, plane.getFractionalLineWidth(), 0.001); + + // 2. Single byte operand: integer = 2, frac = 0 -> 2.0 + ByteArrayOutputStream out2 = new ByteArrayOutputStream(); + out2.write(GocaConstants.G_GSFLW); + out2.write(0x02); + + byte[] flwBytes2 = out2.toByteArray(); + decoder.decodeGoca(flwBytes2, 0, flwBytes2.length); + + assertEquals(2.0, decoder.getFractionalLineWidth(), 0.001); + assertEquals(2.0, plane.getFractionalLineWidth(), 0.001); + + // 3. Reset defaults restores 1.0 + decoder.resetDefaults(); + assertEquals(1.0, decoder.getFractionalLineWidth(), 0.001); + assertEquals(1.0, plane.getFractionalLineWidth(), 0.001); + } + + // ==================================================================================================== + // ITEM 3.4: GCS Character Baseline Angle and Shear Vector Transforms + // ==================================================================================================== + @Test + @DisplayName("Item 3.4: Character Baseline Angle (GSCA 0x34) and Shear (GSCR 0x35) transforms") + public void testItem3_4_CharacterAngleAndShear() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + // 1. GSCA 0x34: Character Angle vector (ax=0, ay=10) -> 90 degrees vertical + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(GocaConstants.G_GSCA); + out.write(0x04); + out.write(0x00); out.write(0); // ax = 0 + out.write(0x00); out.write(10); // ay = 10 + + // GSCR 0x35: Character Shear vector (sx=10, sy=10) -> 45 degrees slant + out.write(GocaConstants.G_GSCR); + out.write(0x04); + out.write(0x00); out.write(10); // sx = 10 + out.write(0x00); out.write(10); // sy = 10 + + byte[] stream = out.toByteArray(); + decoder.decodeGoca(stream, 0, stream.length); + + assertEquals(90.0, decoder.getCharAngle(), 0.01); + assertEquals(45.0, decoder.getCharShear(), 0.01); + + // 2. Draw stroked vector text with active angle and shear + ByteArrayOutputStream textStream = new ByteArrayOutputStream(); + textStream.write(GocaConstants.G_GSCS); textStream.write(0xF8); // Vector font + textStream.write(GocaConstants.G_GCHST); // Character String Absolute + textStream.write(0x05); // 4 bytes coord + 1 byte text 'A' (EBCDIC 0xC1) + textStream.write(0x00); textStream.write(50); + textStream.write(0x00); textStream.write(50); + textStream.write((byte) 0xC1); // EBCDIC 'A' + + byte[] textBytes = textStream.toByteArray(); + decoder.decodeGoca(textBytes, 0, textBytes.length); + + assertTrue(plane.hasContent(), "Vector text with angle/shear must render to GraphicsPlane"); + } + + // ==================================================================================================== + // ITEM 3.5: Multi-Plane Programmed Symbols (RWS 4..7) & Multi-Color PS Cells + // ==================================================================================================== + @Test + @DisplayName("Item 3.5: Multi-Plane Programmed Symbols (RWS 4..7) & Triple-Plane Composite Color Cells") + public void testItem3_5_ProgrammedSymbolsTriplePlane() { + ProgramSymbolManager psm = new ProgramSymbolManager(9, 16); + + // 1. Single-plane set (RWS 2) + byte[] loadpsSingle = new byte[4 + 18]; + loadpsSingle[0] = 0x01; // Format 1 + loadpsSingle[1] = 0x41; // LCID 0x41 + loadpsSingle[2] = 0x40; // Start codepoint + loadpsSingle[3] = 0x02; // RWS 2 + for (int i = 0; i < 18; i++) loadpsSingle[4 + i] = (byte) 0x55; + psm.loadps(loadpsSingle); + + ProgramSymbolSet singleSet = psm.getSymbolSet(0x41); + assertNotNull(singleSet); + assertFalse(singleSet.isTriplePlane()); + + // 2. Triple-plane set (RWS 4, LCID 0x42) loaded with all 3 planes simultaneously (54 bytes) + byte[] loadpsTriple = new byte[4 + 54]; + loadpsTriple[0] = 0x01; // Format 1 + loadpsTriple[1] = 0x42; // LCID 0x42 + loadpsTriple[2] = 0x40; // Start codepoint + loadpsTriple[3] = 0x04; // RWS 4 (Triple Plane) + // Red slice: plane 1 (0x01) + for (int i = 0; i < 18; i++) loadpsTriple[4 + i] = (byte) 0xFF; + // Green slice: plane 2 (0x02) + for (int i = 0; i < 18; i++) loadpsTriple[4 + 18 + i] = (byte) 0xFF; + // Blue slice: plane 4 (0x04) + for (int i = 0; i < 18; i++) loadpsTriple[4 + 36 + i] = (byte) 0xFF; + + psm.loadps(loadpsTriple); + + ProgramSymbolSet tripleSet = psm.getSymbolSet(0x42); + assertNotNull(tripleSet); + assertTrue(tripleSet.isTriplePlane()); + + ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x42, 0x40); + assertNotNull(slot); + assertTrue(slot.isTriplePlane()); + + // Verify multi-color RGB mapping: composite plane (1|2|4 = 7 -> White) + int[] rgb = slot.getRgbPixels(GocaConstants.GOCA_COLORS[0], 0); + assertNotNull(rgb); + assertEquals(9 * 16, rgb.length); + assertEquals(0xFFFFFFFF, rgb[0]); // White pixel (all 3 planes set) + } + + // ==================================================================================================== + // ITEM 3.6: Pick Correlation for Graphical Segment Selection + // ==================================================================================================== + @Test + @DisplayName("Item 3.6: Pick Correlation & 56-Byte Graphic Input Structured Field generation") + public void testItem3_6_PickCorrelationAndGraphicInput() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + // 1. Define Segment 100: drawing a line from (-50, -50) to (50, 50) + ByteArrayOutputStream segStream = new ByteArrayOutputStream(); + segStream.write(GocaConstants.G_BEGSEGM); + segStream.write(0x0C); + segStream.write(0x00); segStream.write(0x00); segStream.write(0x00); segStream.write(100); // Seg ID 100 + segStream.write(0x00); segStream.write(0x00); + segStream.write(0x00); segStream.write(0x00); + segStream.write(0x00); segStream.write(0x00); segStream.write(0x00); segStream.write(0x00); + segStream.write(GocaConstants.G_GSCOL); segStream.write(0x06); // Yellow + segStream.write(GocaConstants.G_GLINE); segStream.write(0x08); + segStream.write((byte) 0xFF); segStream.write((byte) 0xCE); // -50 + segStream.write((byte) 0xFF); segStream.write((byte) 0xCE); // -50 + segStream.write(0x00); segStream.write(50); + segStream.write(0x00); segStream.write(50); + segStream.write(GocaConstants.G_ENDSEGM); segStream.write(0x00); + + byte[] segBytes = segStream.toByteArray(); + decoder.decodeGoca(segBytes, 0, segBytes.length); + + // Hit test inside segment bounds: (0, 0) + int pickedSeg = decoder.findPickedSegment(0, 0); + assertEquals(100, pickedSeg, "Segment 100 should be hit at (0, 0)"); + + // Hit test outside segment bounds: (200, 200) + int pickedMiss = decoder.findPickedSegment(200, 200); + assertEquals(0, pickedMiss, "Missed click should default to segId 0"); + + // 2. Build 56-byte Graphic Input Structured Field via GraphicInputBuilder + byte[] inputSF = GraphicInputBuilder.buildPickCorrelation(25, -25, 5); + assertEquals(56, inputSF.length); + // Header check: 0x0034 (52 byte payload length per IBM HOD) + assertEquals(0x00, inputSF[0]); + assertEquals(0x34, inputSF[1]); + assertEquals(0x0F, inputSF[2]); + assertEquals(0x0F, inputSF[3]); + + // Coordinate checks: X = 25, Y = -25 + int gX = ((inputSF[24] & 0xFF) << 8) | (inputSF[25] & 0xFF); + int gY = (short) (((inputSF[26] & 0xFF) << 8) | (inputSF[27] & 0xFF)); + assertEquals(25, gX); + assertEquals(-25, gY); + + // Mouse action flags: trigger class = 4, correlation class = 4, Button 1 = 0x01 + assertEquals(0x04, inputSF[31]); + assertEquals(0x04, inputSF[33]); + assertEquals(0x01, inputSF[35]); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/ScreenBufferPhase4Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/ScreenBufferPhase4Test.java new file mode 100644 index 0000000..cb644b2 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/screen/ScreenBufferPhase4Test.java @@ -0,0 +1,223 @@ +package haus.nightmare.lib3270j.screen; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.AbstractDBCSCodePage; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.ECLField; +import haus.nightmare.lib3270j.ecl.ECLFieldList; +import haus.nightmare.lib3270j.input.InputProcessor; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +public class ScreenBufferPhase4Test { + + private ScreenBuffer screen; + private EbcdicTranslator translator; + private InputProcessor input; + + @BeforeEach + public void setup() { + translator = new EbcdicTranslator(); + screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator); + input = new InputProcessor(screen, translator, null); + } + + @Test + public void testEntryAssistDOCModeAndWordWrap() { + screen.erase(false); + screen.setEntryAssistDOCmode(true); + screen.setEntryAssistWordWrap(true); + screen.setEntryAssistStartColumn(0); + screen.setEntryAssistEndColumn(20); + + assertTrue(screen.isEntryAssistDOCmode()); + assertTrue(screen.isEntryAssistWordWrap()); + assertEquals(0, screen.getEntryAssistStartColumn()); + assertEquals(20, screen.getEntryAssistEndColumn()); + + // Type "HELLO WORLD " near column 20 + screen.setCursorPosition(0, 10); + String s1 = "HELLO "; + for (int i = 0; i < s1.length(); i++) { + input.typeCharacter(s1.charAt(i)); + } + assertEquals(16, screen.getCursorCol()); + assertEquals(0, screen.getCursorRow()); + + // Now type "TESTING" which starts at col 16 and crosses col 20 + String s2 = "TESTING"; + for (int i = 0; i < s2.length(); i++) { + input.typeCharacter(s2.charAt(i)); + } + + // "HELLO " should remain on row 0 (cols 10..15) + assertEquals("HELLO ", screen.getString(10, 6)); + // "TESTING" should be soft-wrapped to row 1 (cols 0..6) + assertEquals("TESTING", screen.getString(80, 7)); + assertEquals(1, screen.getCursorRow()); + assertEquals(7, screen.getCursorCol()); + } + + @Test + public void testEntryAssistWordWrapTypingSpaceAtEndMargin() { + screen.erase(false); + screen.setEntryAssistWordWrap(true); + screen.setEntryAssistStartColumn(5); + screen.setEntryAssistEndColumn(20); + + screen.setCursorPosition(0, 20); + input.typeCharacter(' '); + + // Cursor should wrap to next line at startCol 5 + assertEquals(1, screen.getCursorRow()); + assertEquals(5, screen.getCursorCol()); + } + + @Test + public void testEntryAssistTabStopsAndNavigation() { + screen.erase(false); + screen.setEntryAssistTabStops(new int[]{ 10, 25, 50, 70 }); + assertArrayEquals(new int[]{ 10, 25, 50, 70 }, screen.getEntryAssistTabStops()); + + screen.setCursorPosition(0, 0); + screen.processWordTab(true); + assertEquals(10, screen.getCursorCol()); + + screen.processWordTab(true); + assertEquals(25, screen.getCursorCol()); + + screen.processWordTab(true); + assertEquals(50, screen.getCursorCol()); + + screen.processWordTab(false); + assertEquals(25, screen.getCursorCol()); + + screen.processWordTab(false); + assertEquals(10, screen.getCursorCol()); + } + + @Test + public void testProcessDeleteWord() { + screen.erase(false); + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.setFieldAttribute(40, (byte) (FA_PRINTABLE | FA_PROTECT)); + + // Field contains "ALPHA BRAVO CHARLIE" + String text = "ALPHA BRAVO CHARLIE"; + for (int i = 0; i < text.length(); i++) { + screen.setChar(0, 1 + i, text.charAt(i)); + } + + // Place cursor at "BRAVO" (col 7) + screen.setCursorPosition(0, 7); + screen.processDeleteWord(); + + // "BRAVO " should be deleted, leaving "ALPHA CHARLIE" + assertEquals("ALPHA CHARLIE", screen.getString(1, 13)); + assertTrue(screen.isModified(1)); + } + + @Test + public void testDBCSSOSIPairBalancing() { + EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930"); + AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage(); + cp.registerDbcsPair(0x4341, '\u6771'); + + ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator); + dbcsScreen.erase(false); + dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE); + dbcsScreen.setFieldAttribute(20, (byte) (FA_PRINTABLE | FA_PROTECT)); + + // Insert DBCS character + boolean inserted = dbcsScreen.insertChar(1, '\u6771'); + assertTrue(inserted); + assertEquals(ExtendedAttribute.CS_DBCS, dbcsScreen.getCell(1).cs); + assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db); + assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db); + + // Delete DBCS character at left half + boolean deleted = dbcsScreen.deleteChar(1); + assertTrue(deleted); + assertEquals(0, dbcsScreen.getCell(1).ec); + assertEquals(0, dbcsScreen.getCell(2).ec); + + // Test balanceSOSI removes adjacent empty SO SI + dbcsScreen.setCell(3, 0x0E); // SO + dbcsScreen.setCell(4, 0x0F); // SI + dbcsScreen.balanceSOSI(); + assertEquals(0, dbcsScreen.getCell(3).ec); + assertEquals(0, dbcsScreen.getCell(4).ec); + } + + @Test + public void testWrappedFieldListHandling() { + screen.erase(false); + // Field 1 at pos 100..499 + screen.setFieldAttribute(100, (byte) FA_PRINTABLE); + // Field 2 at pos 500..99 (wrapping around end of buffer) + screen.setFieldAttribute(500, (byte) (FA_PRINTABLE | FA_PROTECT)); + + ECLFieldList list = screen.buildFieldList(); + assertEquals(2, list.getFieldCount()); + + ECLField f1 = list.getFirstField(); + assertNotNull(f1); + assertEquals(100, f1.getStart()); + assertEquals(101, f1.getDataStart()); + assertEquals(499, f1.getEnd()); + assertEquals(399, f1.getLength()); + assertFalse(f1.isWrapped()); + assertFalse(f1.isProtected()); + + ECLField f2 = list.getNextField(f1); + assertNotNull(f2); + assertEquals(500, f2.getStart()); + assertEquals(501, f2.getDataStart()); + assertEquals(99, f2.getEnd()); + assertEquals(1920 - 500 - 1 + 100, f2.getLength()); + assertTrue(f2.isWrapped()); + assertTrue(f2.isProtected()); + + // Test containment across wrap + assertTrue(f2.contains(500)); + assertTrue(f2.contains(501)); + assertTrue(f2.contains(1919)); + assertTrue(f2.contains(0)); + assertTrue(f2.contains(99)); + assertFalse(f2.contains(100)); + assertFalse(f2.contains(200)); + + // Test field lookup by position + assertEquals(f2.getStart(), list.findField(50).getStart()); + assertEquals(f1.getStart(), list.findField(200).getStart()); + assertEquals(f2.getStart(), list.findField(600).getStart()); + + // Test navigation + assertEquals(f2.getStart(), list.getPreviousField(f1).getStart()); + assertNull(list.getNextField(f2)); + } + + @Test + public void testSingleFieldFullBufferWrap() { + screen.erase(false); + screen.setFieldAttribute(250, (byte) FA_PRINTABLE); + + ECLFieldList list = screen.buildFieldList(); + assertEquals(1, list.getFieldCount()); + + ECLField f = list.getFirstField(); + assertNotNull(f); + assertEquals(250, f.getStart()); + assertEquals(251, f.getDataStart()); + assertEquals(249, f.getEnd()); + assertEquals(1919, f.getLength()); + assertTrue(f.isWrapped()); + + assertTrue(f.contains(0)); + assertTrue(f.contains(250)); + assertTrue(f.contains(1919)); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/ProxyConnectionTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/ProxyConnectionTest.java index 058d972..b3e7240 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/ProxyConnectionTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/telnet/ProxyConnectionTest.java @@ -241,4 +241,51 @@ public class ProxyConnectionTest { connection.disconnect(); } } + + @Test + public void testSystemPropertiesProxyFallback() throws Exception { + try (ServerSocket proxyServer = new ServerSocket(0)) { + int proxyPort = proxyServer.getLocalPort(); + CountDownLatch serverHandshakeDone = new CountDownLatch(1); + + Thread serverThread = new Thread(() -> { + try (Socket clientSock = proxyServer.accept()) { + BufferedReader reader = new BufferedReader(new InputStreamReader(clientSock.getInputStream(), StandardCharsets.US_ASCII)); + String line; + while ((line = reader.readLine()) != null) { + if (line.isEmpty()) break; + } + OutputStream out = clientSock.getOutputStream(); + out.write("HTTP/1.1 200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII)); + out.flush(); + serverHandshakeDone.countDown(); + } catch (Exception ignored) {} + }); + serverThread.setDaemon(true); + serverThread.start(); + + String origHost = System.getProperty("http.proxyHost"); + String origPort = System.getProperty("http.proxyPort"); + try { + System.setProperty("http.proxyHost", "127.0.0.1"); + System.setProperty("http.proxyPort", String.valueOf(proxyPort)); + + ConnectionConfig config = new ConnectionConfig("test.fallback.org", 23); + config.setConnectTimeoutMs(5000); + + TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null); + TelnetConnection connection = new TelnetConnection(config, fsm); + + connection.connect(); + assertTrue(connection.isConnected()); + assertTrue(serverHandshakeDone.await(3, TimeUnit.SECONDS)); + connection.disconnect(); + } finally { + if (origHost != null) System.setProperty("http.proxyHost", origHost); + else System.clearProperty("http.proxyHost"); + if (origPort != null) System.setProperty("http.proxyPort", origPort); + else System.clearProperty("http.proxyPort"); + } + } + } }