From 3d3a939ecef856a668a1644a64e7ed70a44ba272 Mon Sep 17 00:00:00 2001 From: Rudi Date: Tue, 22 Sep 2026 13:38:12 +0000 Subject: [PATCH] Codepage, ECL, and some UI updates --- .../haus/nightmare/j3270/ui/StatusBar.java | 7 +- .../nightmare/j3270/ui/TerminalPanel.java | 163 +++++++++++- ...nnectSettingsAndTerminalCenteringTest.java | 242 ++++++++++++++++++ .../j3270/ui/DBCSInputAndClipboardTest.java | 153 +++++++++++ .../java/com/ibm/eNetwork/ECL/ECLField.java | 13 +- .../main/java/com/ibm/eNetwork/ECL/ECLPS.java | 57 +++++ .../charset/AbstractDBCSCodePage.java | 88 ++++++- .../lib3270j/charset/CodePageRegistry.java | 16 +- .../nightmare/lib3270j/charset/Cp1027.java | 43 ++++ .../nightmare/lib3270j/charset/Cp1390.java | 23 ++ .../nightmare/lib3270j/charset/Cp1399.java | 23 ++ .../nightmare/lib3270j/charset/Cp290.java | 43 ++++ .../nightmare/lib3270j/charset/Cp930.java | 2 +- .../nightmare/lib3270j/charset/Cp939.java | 2 +- .../lib3270j/charset/EbcdicTranslator.java | 29 +++ .../datastream/DataStreamProcessor.java | 159 +++++++++++- .../datastream/QueryReplyBuilder.java | 72 +++++- .../nightmare/lib3270j/ecl/ECLConstants.java | 28 +- .../haus/nightmare/lib3270j/ecl/ECLField.java | 48 ++++ .../nightmare/lib3270j/ecl/ECLFieldList.java | 13 + .../haus/nightmare/lib3270j/ecl/ECLPS.java | 140 +++++++++- .../haus/nightmare/lib3270j/ft/FTDft.java | 9 +- .../lib3270j/input/InputProcessor.java | 98 ++++--- .../lib3270j/screen/ExtendedAttribute.java | 5 + .../lib3270j/screen/ScreenBuffer.java | 13 +- .../datastream/DBCSDataStreamFullTest.java | 204 +++++++++++++++ .../datastream/QueryReplyBuilderTest.java | 17 +- .../datastream/QueryReplyPhase2Test.java | 12 +- .../TransparencyDataStreamTest.java | 143 +++++++++++ 29 files changed, 1773 insertions(+), 92 deletions(-) create mode 100644 j3270/src/test/java/haus/nightmare/j3270/ui/AutoConnectSettingsAndTerminalCenteringTest.java create mode 100644 j3270/src/test/java/haus/nightmare/j3270/ui/DBCSInputAndClipboardTest.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1027.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1390.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1399.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp290.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DBCSDataStreamFullTest.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/TransparencyDataStreamTest.java 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 54876ed..51900e2 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java @@ -357,9 +357,12 @@ public class StatusBar extends JPanel { wordWrapStatus.setText(""); } - // Field status: Numeric vs Alphanumeric + // Field status: Numeric vs Alphanumeric vs DBCS if (state.isFullSession() && client.getScreenBuffer().isFormatted()) { - if (client.getOIA().isNumeric()) { + if (client.getOIA().isDBCS()) { + fieldTypeStatus.setText("DBCS"); + fieldTypeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme)); + } else if (client.getOIA().isNumeric()) { fieldTypeStatus.setText("NUM"); fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme)); } else { 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 e5fceda..c6e2d06 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -10,6 +10,12 @@ import javax.swing.*; import java.awt.*; import java.awt.datatransfer.*; import java.awt.event.*; +import java.awt.event.InputMethodEvent; +import java.awt.event.InputMethodListener; +import java.awt.font.TextHitInfo; +import java.awt.im.InputMethodRequests; +import java.text.AttributedCharacterIterator; +import java.text.CharacterIterator; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; @@ -18,7 +24,8 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; * Supports colors, bold, underline, reverse, blink, GOCA vector graphics, * programmed symbols, search highlighting, and all 3278/3279 attributes. */ -public class TerminalPanel extends JPanel implements java.awt.print.Printable { +public class TerminalPanel extends JPanel implements java.awt.print.Printable, InputMethodListener, InputMethodRequests { + private Telnet3270Client client; @@ -216,6 +223,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle(); cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK; + enableInputMethods(true); + addInputMethodListener(this); + + // Handle mouse clicks to position cursor, selection, and grab focus MouseAdapter mouseHandler = new MouseAdapter() { @Override @@ -715,6 +726,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { repaint(); } + public void select(int startRow, int startCol, int endRow, int endCol) { + selectionStartRow = startRow; + selectionStartCol = startCol; + selectionEndRow = endRow; + selectionEndCol = endCol; + repaint(); + } + public boolean hasSelection() { return selectionStartRow >= 0 && selectionEndRow >= 0 && !(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol); @@ -740,11 +759,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { for (int c = c1; c <= c2; c++) { int baddr = r * cols + c; ExtendedAttribute ea = sb.getCell(baddr); - char ch = ea.ucs4; - if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) { + if (ea.isFieldAttribute()) { + line.append(' '); + } else if (ea.db == ExtendedAttribute.DB_RIGHT) { + if (c > c1) { + continue; // Character already added by DB_LEFT + } else { + line.append(' '); + } + } else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) { line.append(' '); } else { - line.append(ch); + char ch = ea.ucs4; + if (ch <= 0x20 || ch == 0xFF) { + line.append(' '); + } else { + line.append(ch); + } } } // Trim trailing whitespace on copied lines per HoD CutPasteOptions @@ -782,11 +813,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { prevRow = curRow; } ExtendedAttribute ea = sb.getCell(addr); - char ch = ea.ucs4; - if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) { + if (ea.isFieldAttribute()) { + line.append(' '); + } else if (ea.db == ExtendedAttribute.DB_RIGHT) { + if (addr > startAddr) { + continue; // Character already added by DB_LEFT + } else { + line.append(' '); + } + } else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) { line.append(' '); } else { - line.append(ch); + char ch = ea.ucs4; + if (ch <= 0x20 || ch == 0xFF) { + line.append(' '); + } else { + line.append(ch); + } } } int end = line.length(); @@ -2099,13 +2142,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { continue; } + byte tr = ea.tr != 0 ? ea.tr : (currentFieldEa != null ? currentFieldEa.tr : 0); + boolean isOpaque = ((tr & 0xFF) == XAT_OPAQUE); + if (reverse) { Color tmp = fgColor; fgColor = (rawBg == 0) ? this.bgColor : bgColor; bgColor = tmp; g2.setColor(bgColor); g2.fillRect(x, y, cellWidth, cellHeight); - } else if (bgIsExplicit || !bgColor.equals(this.bgColor)) { + } else if (isOpaque || bgIsExplicit || !bgColor.equals(this.bgColor)) { g2.setColor(bgColor); g2.fillRect(x, y, cellWidth, cellHeight); } @@ -2120,7 +2166,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { if (cs >= 0x40 && client.getProgramSymbolManager() != null && !suppressGlyph) { haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF); if (slot != null) { - int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0; + int symBg = (bgIsExplicit || reverse || isOpaque) ? bgColor.getRGB() : 0; haus.nightmare.lib3270j.graphics.PixelBuffer symPb = slot.getScaledPixelBuffer(cellWidth, cellHeight, fgColor.getRGB(), symBg); java.awt.image.BufferedImage img = AwtPixelBufferBridge.toBufferedImage(symPb); if (img != null) { @@ -2130,7 +2176,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { } } - if (!drawnAsPs && !suppressGlyph) { + if (!drawnAsPs && !suppressGlyph && ea.db != ExtendedAttribute.DB_RIGHT + && ea.db != ExtendedAttribute.DB_SO && ea.db != ExtendedAttribute.DB_SI) { char ch = ea.ucs4; if (ch > 0x20 && ch != 0xFF) { Font f = bold ? boldTerminalFont : terminalFont; @@ -2269,11 +2316,17 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { boolean isUnderline = (cursorStyle == CursorStyle.UNDERLINE) || (client.getInputProcessor() != null && client.getInputProcessor().isInsertMode()); g2.setColor(CURSOR_COLOR); + ExtendedAttribute curEa = (curAddr >= 0 && curAddr < sb.getSize()) ? sb.getDisplayCell(curAddr) : null; + boolean isDbcs = curEa != null && (curEa.db == ExtendedAttribute.DB_LEFT || curEa.db == ExtendedAttribute.DB_RIGHT); + int cWidth = isDbcs ? (cellWidth * 2) : cellWidth; + if (isDbcs && curEa.db == ExtendedAttribute.DB_RIGHT && curCol > 0) { + cx -= cellWidth; + } if (isUnderline) { int ulH = Math.max(2, cellHeight / 6); - g2.fillRect(cx, cy + cellHeight - ulH, cellWidth, ulH); + g2.fillRect(cx, cy + cellHeight - ulH, cWidth, ulH); } else { - g2.fillRect(cx, cy, cellWidth, cellHeight); + g2.fillRect(cx, cy, cWidth, cellHeight); } } } @@ -2462,4 +2515,90 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { paint(g2d); return PAGE_EXISTS; } + + // ========================================================================= + // Java Input Method Framework (IMF / IME) Support + // ========================================================================= + + @Override + public InputMethodRequests getInputMethodRequests() { + return this; + } + + @Override + public void inputMethodTextChanged(InputMethodEvent event) { + if (client == null || !client.getConnectionState().isFullSession()) { + event.consume(); + return; + } + AttributedCharacterIterator text = event.getText(); + if (text != null) { + int committedCount = event.getCommittedCharacterCount(); + char c = text.first(); + for (int i = 0; i < committedCount && c != CharacterIterator.DONE; i++) { + client.typeCharacter(c); + c = text.next(); + } + refreshScreen(); + } + event.consume(); + } + + @Override + public void caretPositionChanged(InputMethodEvent event) { + // No-op + } + + @Override + public Rectangle getTextLocation(TextHitInfo offset) { + Point p; + try { + p = getLocationOnScreen(); + } catch (IllegalComponentStateException e) { + p = new Point(0, 0); + } + int curRow = 0, curCol = 0; + if (client != null && client.getScreenBuffer() != null) { + int cols = client.getScreenBuffer().getCols(); + int curAddr = client.getScreenBuffer().getDisplayCursorAddress(); + if (cols > 0) { + curRow = curAddr / cols; + curCol = curAddr % cols; + } + } + int cx = p.x + getRenderOffsetX() + curCol * cellWidth; + int cy = p.y + getRenderOffsetY() + curRow * cellHeight; + return new Rectangle(cx, cy, cellWidth, cellHeight); + } + + @Override + public TextHitInfo getLocationOffset(int x, int y) { + return TextHitInfo.leading(0); + } + + @Override + public int getInsertPositionOffset() { + return 0; + } + + @Override + public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + @Override + public int getCommittedTextLength() { + return 0; + } + + @Override + public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } + + @Override + public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { + return null; + } } + diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/AutoConnectSettingsAndTerminalCenteringTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/AutoConnectSettingsAndTerminalCenteringTest.java new file mode 100644 index 0000000..ed285c8 --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/AutoConnectSettingsAndTerminalCenteringTest.java @@ -0,0 +1,242 @@ +package haus.nightmare.j3270.ui; + +import haus.nightmare.j3270.J3270App; +import haus.nightmare.j3270.config.Settings; +import haus.nightmare.j3270.storage.HostStorage; +import haus.nightmare.j3270.storage.SavedHost; +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.ConnectionState; +import haus.nightmare.lib3270j.Telnet3270Client; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.swing.*; +import java.awt.*; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class AutoConnectSettingsAndTerminalCenteringTest { + + private List originalSavedHosts; + private Settings.StartupBehavior originalStartupBehavior; + private String originalAutoConnectHost; + private int originalAutoConnectPort; + + @BeforeEach + public void setUp() { + originalSavedHosts = new ArrayList<>(HostStorage.getSavedHosts()); + originalStartupBehavior = Settings.getStartupBehavior(); + originalAutoConnectHost = Settings.getAutoConnectHost(); + originalAutoConnectPort = Settings.getAutoConnectPort(); + } + + @AfterEach + public void tearDown() { + HostStorage.saveAll(originalSavedHosts); + Settings.setStartupBehavior(originalStartupBehavior); + Settings.setAutoConnectHost(originalAutoConnectHost); + Settings.setAutoConnectPort(originalAutoConnectPort); + } + + @Test + @DisplayName("TerminalPanel centering: left bound is flush when constrained, centered with left <= right when too big") + public void testTerminalPanelCenteringLogic() { + TerminalPanel panel = new TerminalPanel(); + panel.cellWidth = 10; + panel.cellHeight = 20; + + Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23)); + client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270); + ScreenBuffer sb = client.getScreenBuffer(); + sb.erase(false); + panel.setClient(client); + + int cols = sb.getDisplayCols(); // 80 + int rows = sb.getDisplayRows(); // 24 + int gridW = cols * panel.cellWidth; // 800 + int gridH = rows * panel.cellHeight; // 480 + + // 1. Panel is smaller than grid: left bound must be 0 (flush left cells), vertical must be 0 + panel.setSize(gridW - 100, gridH - 50); + assertEquals(0, panel.getRenderOffsetX(), "Left bound should be 0 when window width is constrained"); + assertEquals(0, panel.getRenderOffsetY(), "Top bound should be 0 when window height is constrained"); + + // 2. Panel exactly matches grid: offset must be 0 + panel.setSize(gridW, gridH); + assertEquals(0, panel.getRenderOffsetX(), "Offset X must be 0 when width exactly matches grid"); + assertEquals(0, panel.getRenderOffsetY(), "Offset Y must be 0 when height exactly matches grid"); + + // 3. Panel is larger with odd extra (extra = 7) + panel.setSize(gridW + 7, gridH + 11); + int ox = panel.getRenderOffsetX(); + int leftSpace = ox; + int rightSpace = (gridW + 7) - (ox + gridW); + assertTrue(leftSpace <= rightSpace, "Left blank space (" + leftSpace + ") must be <= right space (" + rightSpace + ")"); + assertEquals(3, leftSpace); + assertEquals(4, rightSpace); + + int oy = panel.getRenderOffsetY(); + int topSpace = oy; + int bottomSpace = (gridH + 11) - (oy + gridH); + assertTrue(topSpace <= bottomSpace, "Top space must be <= bottom space"); + assertEquals(5, topSpace); + assertEquals(6, bottomSpace); + + // 4. Panel is larger with even extra (extra = 6) + panel.setSize(gridW + 6, gridH + 8); + assertEquals(3, panel.getRenderOffsetX()); + assertEquals(4, panel.getRenderOffsetY()); + + panel.dispose(); + } + + @Test + @DisplayName("guardedPack does not resize window when window is too small") + public void testGuardedPackDoesNotResizeWhenTooSmall() { + try { + JFrame frame = new JFrame(); + TerminalPanel panel = new TerminalPanel(); + panel.cellWidth = 10; + panel.cellHeight = 20; + + Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23)); + client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270); + panel.setClient(client); + + frame.getContentPane().add(panel); + frame.setSize(400, 300); + frame.addNotify(); // makes frame displayable + + // Set size explicitly smaller than preferred size (800+ x 480+) + frame.setSize(400, 300); + Dimension sizeBefore = frame.getSize(); + + // Run guardedPack + panel.guardedPack(); + + // Window should not have expanded + Dimension sizeAfter = frame.getSize(); + assertEquals(sizeBefore.width, sizeAfter.width, "Window width should NOT be enlarged when too small"); + assertEquals(sizeBefore.height, sizeAfter.height, "Window height should NOT be enlarged when too small"); + + frame.dispose(); + panel.dispose(); + } catch (HeadlessException ignored) { + // Environment without X display + } + } + + @Test + @DisplayName("SettingsDialog Startup & Auto-Connect presents saved connections dropdown and updates auto-connect host") + public void testSettingsDialogSavedConnectionsDropdown() throws Exception { + SavedHost host1 = new SavedHost("id-test-1", "Host Alpha", "alpha.mainframe.test", 23, 2, 24, 80, "LU1", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", ""); + SavedHost host2 = new SavedHost("id-test-2", "Host Beta", "beta.mainframe.test", 992, 4, 43, 80, "LU2", false, "VM", true, false, true, "BOTH", "037", "NONE", "", 0, "", ""); + + List testHosts = new ArrayList<>(); + testHosts.add(host1); + testHosts.add(host2); + HostStorage.saveAll(testHosts); + Settings.setStartupBehavior(Settings.StartupBehavior.AUTO_CONNECT); + + J3270App app; + try { + app = new J3270App(); + } catch (HeadlessException e) { + return; + } + + try { + SettingsDialog dialog = new SettingsDialog(app); + + JComboBox combo = dialog.getSavedHostBox(); + assertNotNull(combo, "savedHostBox should not be null"); + assertEquals(2, combo.getItemCount(), "Dropdown should contain all saved hosts"); + + // Host Alpha was autoConnect=true, so it should be initially selected + SavedHost initialSel = (SavedHost) combo.getSelectedItem(); + assertNotNull(initialSel); + assertEquals("id-test-1", initialSel.getId()); + + // Switch to Host Beta + combo.setSelectedIndex(1); + + // Trigger applySettings via reflection + java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings"); + applyMethod.setAccessible(true); + applyMethod.invoke(dialog); + + // Verify Host Beta is now the auto-connect host + SavedHost auto = HostStorage.getAutoConnectHost(); + assertNotNull(auto, "Auto-connect host should exist"); + assertEquals("id-test-2", auto.getId()); + assertEquals("Host Beta", auto.getName()); + assertEquals("beta.mainframe.test", auto.getHost()); + assertEquals(992, auto.getPort()); + + SavedHost loadedAlpha = HostStorage.findById("id-test-1"); + assertNotNull(loadedAlpha); + assertFalse(loadedAlpha.isAutoConnect(), "Host Alpha should no longer have autoConnect = true"); + + // Verify Settings autoConnectHost/Port synchronized + assertEquals("beta.mainframe.test", Settings.getAutoConnectHost()); + assertEquals(992, Settings.getAutoConnectPort()); + + // Switch startup behavior to SHOW_CONNECT and apply + Field sbField = SettingsDialog.class.getDeclaredField("startupBehaviorBox"); + sbField.setAccessible(true); + @SuppressWarnings("unchecked") + JComboBox sbBox = (JComboBox) sbField.get(dialog); + sbBox.setSelectedItem(Settings.StartupBehavior.SHOW_CONNECT); + + applyMethod.invoke(dialog); + assertNull(HostStorage.getAutoConnectHost(), "When startup behavior is SHOW_CONNECT, auto-connect host should be cleared"); + + dialog.dispose(); + app.dispose(); + } finally { + HostStorage.saveAll(originalSavedHosts); + } + } + + @Test + @DisplayName("Window size is adjusted once on connect for selected model and left untouched on onScreenSizeChanged") + public void testWindowSizedOnceForModelAndNotOnScreenSizeChanged() { + try { + JFrame frame = new JFrame(); + TerminalPanel panel = new TerminalPanel(); + panel.cellWidth = 10; + panel.cellHeight = 20; + + frame.getContentPane().add(panel); + frame.addNotify(); + + // Pack for Model 4 (43 rows, 80 cols) + panel.packForModel(43, 80); + Dimension model4Size = frame.getSize(); + assertTrue(model4Size.height >= 43 * 20, "Height should accommodate 43 rows"); + assertTrue(model4Size.width >= 80 * 10, "Width should accommodate 80 cols"); + + // Pack for Model 5 (27 rows, 132 cols) + panel.packForModel(27, 132); + Dimension model5Size = frame.getSize(); + assertTrue(model5Size.width >= 132 * 10, "Width should accommodate 132 cols"); + + // Set custom size and verify guardedPack leaves the window alone + frame.setSize(600, 400); + Dimension fixedSize = frame.getSize(); + + panel.guardedPack(); + assertEquals(fixedSize.width, frame.getWidth()); + assertEquals(fixedSize.height, frame.getHeight()); + + frame.dispose(); + panel.dispose(); + } catch (HeadlessException ignored) {} + } +} diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/DBCSInputAndClipboardTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/DBCSInputAndClipboardTest.java new file mode 100644 index 0000000..ae6ddd0 --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/DBCSInputAndClipboardTest.java @@ -0,0 +1,153 @@ +package haus.nightmare.j3270.ui; + +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.ConnectionState; +import haus.nightmare.lib3270j.Telnet3270Client; +import haus.nightmare.lib3270j.input.InputProcessor; +import haus.nightmare.lib3270j.screen.ExtendedAttribute; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.awt.event.InputMethodEvent; +import java.text.AttributedString; + +import static org.junit.jupiter.api.Assertions.*; + +public class DBCSInputAndClipboardTest { + + @Test + @DisplayName("Clipboard selection skips DB_RIGHT cells to avoid duplicating wide characters") + public void testDBCSClipboardCopyNoDoubling() { + TerminalPanel panel = new TerminalPanel(); + panel.setSize(800, 600); + + ConnectionConfig config = new ConnectionConfig("localhost", 23); + config.setCodePage("930"); + Telnet3270Client client = new Telnet3270Client(config); + client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270); + + ScreenBuffer sb = client.getScreenBuffer(); + sb.erase(false); + + // Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'A' at cell 2 + ExtendedAttribute cell0 = sb.getCell(0); + cell0.db = ExtendedAttribute.DB_LEFT; + cell0.ucs4 = '\u3000'; + + ExtendedAttribute cell1 = sb.getCell(1); + cell1.db = ExtendedAttribute.DB_RIGHT; + cell1.ucs4 = '\u3000'; + + ExtendedAttribute cell2 = sb.getCell(2); + cell2.db = ExtendedAttribute.DB_NONE; + cell2.ucs4 = 'A'; + + panel.setClient(client); + + // Stream selection covering cells 0..2 (row 0, col 0..2) + panel.select(0, 0, 0, 2); + String selected = panel.getSelectedText(); + + assertEquals("\u3000A", selected, "Stream selection must not duplicate the DBCS character"); + } + + @Test + @DisplayName("Block selection skips DB_RIGHT cells to avoid duplicating wide characters") + public void testDBCSBlockSelectionNoDoubling() { + TerminalPanel panel = new TerminalPanel(); + panel.setSize(800, 600); + panel.setBlockSelectMode(true); + + ConnectionConfig config = new ConnectionConfig("localhost", 23); + config.setCodePage("930"); + Telnet3270Client client = new Telnet3270Client(config); + client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270); + + ScreenBuffer sb = client.getScreenBuffer(); + sb.erase(false); + + // Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'B' at cell 2 + ExtendedAttribute cell0 = sb.getCell(0); + cell0.db = ExtendedAttribute.DB_LEFT; + cell0.ucs4 = '\u3000'; + + ExtendedAttribute cell1 = sb.getCell(1); + cell1.db = ExtendedAttribute.DB_RIGHT; + cell1.ucs4 = '\u3000'; + + ExtendedAttribute cell2 = sb.getCell(2); + cell2.db = ExtendedAttribute.DB_NONE; + cell2.ucs4 = 'B'; + + panel.setClient(client); + + panel.select(0, 0, 0, 2); + String selected = panel.getSelectedText(); + + assertEquals("\u3000B", selected, "Block selection must not duplicate the DBCS character"); + } + + @Test + @DisplayName("InputMethodRequests returns cursor bounds and processes committed text") + public void testDBCSInputMethodRequestsAndEvents() { + TerminalPanel panel = new TerminalPanel(); + panel.setSize(800, 600); + + ConnectionConfig config = new ConnectionConfig("localhost", 23); + config.setCodePage("930"); + Telnet3270Client client = new Telnet3270Client(config); + client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270); + + panel.setClient(client); + + assertNotNull(panel.getInputMethodRequests(), "getInputMethodRequests must return this"); + + Rectangle loc = panel.getTextLocation(null); + assertNotNull(loc, "Text location must not be null"); + assertTrue(loc.width > 0, "Cursor width must be positive"); + assertTrue(loc.height > 0, "Cursor height must be positive"); + + // Simulate committed IME text '\u3000' + AttributedString as = new AttributedString("\u3000"); + InputMethodEvent imeEvent = new InputMethodEvent(panel, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, + as.getIterator(), 1, null, null); + panel.inputMethodTextChanged(imeEvent); + + ScreenBuffer sb = client.getScreenBuffer(); + assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db); + assertEquals('\u3000', sb.getCell(0).ucs4); + assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db); + assertEquals('\u3000', sb.getCell(1).ucs4); + assertEquals(2, sb.getCursorAddress(), "Cursor should advance by 2 after typing 2-cell DBCS"); + } + + @Test + @DisplayName("InputProcessor backspace over DBCS character deletes both cells") + public void testInputProcessorDBCSTypingAndBackspace() { + ConnectionConfig config = new ConnectionConfig("localhost", 23); + config.setCodePage("930"); + Telnet3270Client client = new Telnet3270Client(config); + client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270); + + InputProcessor input = client.getInputProcessor(); + ScreenBuffer sb = client.getScreenBuffer(); + sb.erase(false); + sb.setCursorAddress(0); + + // Type DBCS character + input.typeCharacter('\u3000'); + assertEquals(2, sb.getCursorAddress()); + assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db); + assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db); + + // Backspace over DBCS character + input.backspace(); + assertEquals(0, sb.getCursorAddress(), "Cursor should back up 2 cells"); + assertEquals(0, sb.getCell(0).ucs4, "Cell 0 should be deleted (null)"); + assertEquals(0, sb.getCell(1).ucs4, "Cell 1 should be deleted (null)"); + assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(0).db); + assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(1).db); + } +} diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLField.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLField.java index 8b28284..6433413 100644 --- a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLField.java +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLField.java @@ -131,7 +131,18 @@ public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements EC return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0; } - public short getAttribute() { return GetAttribute(); } + public boolean IsDBCS() { + return delegate != null && delegate.isDBCS(); + } + + public boolean isDBCS() { return IsDBCS(); } + + public boolean IsDBCSOnly() { + return delegate != null && delegate.isDBCSOnly(); + } + + public boolean isDBCSOnly() { return IsDBCSOnly(); } public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; } } + diff --git a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPS.java b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPS.java index a8135e3..778cf7e 100644 --- a/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPS.java +++ b/lib3270j/src/main/java/com/ibm/eNetwork/ECL/ECLPS.java @@ -511,4 +511,61 @@ public class ECLPS implements ECLConstants, Serializable, Cloneable { public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) { return delegate.GetScreen(buffer, len, row, col, length, plane); } + + // ========== DBCS Character Status ========== + + public boolean IsDBCSChar(int pos) { + return delegate != null && delegate.IsDBCSChar(pos); + } + + public boolean isDBCSChar(int pos) { return IsDBCSChar(pos); } + + public boolean IsDBCSChar(int row, int col) { + return delegate != null && delegate.IsDBCSChar(row, col); + } + + public boolean isDBCSChar(int row, int col) { return IsDBCSChar(row, col); } + + public boolean IsDBCS1stChar(int pos) { + return delegate != null && delegate.IsDBCS1stChar(pos); + } + + public boolean isDBCS1stChar(int pos) { return IsDBCS1stChar(pos); } + + public boolean IsDBCS1stChar(int row, int col) { + return delegate != null && delegate.IsDBCS1stChar(row, col); + } + + public boolean isDBCS1stChar(int row, int col) { return IsDBCS1stChar(row, col); } + + public boolean IsDBCS2ndChar(int pos) { + return delegate != null && delegate.IsDBCS2ndChar(pos); + } + + public boolean isDBCS2ndChar(int pos) { return IsDBCS2ndChar(pos); } + + public boolean IsDBCS2ndChar(int row, int col) { + return delegate != null && delegate.IsDBCS2ndChar(row, col); + } + + public boolean isDBCS2ndChar(int row, int col) { return IsDBCS2ndChar(row, col); } + + public boolean IsSOChar(int pos) { + return delegate != null && delegate.IsSOChar(pos); + } + + public boolean isSOChar(int pos) { return IsSOChar(pos); } + + public boolean IsSIChar(int pos) { + return delegate != null && delegate.IsSIChar(pos); + } + + public boolean isSIChar(int pos) { return IsSIChar(pos); } + + public boolean IsSOSIChar(int pos) { + return delegate != null && delegate.IsSOSIChar(pos); + } + + public boolean isSOSIChar(int pos) { return IsSOSIChar(pos); } } + diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/AbstractDBCSCodePage.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/AbstractDBCSCodePage.java index 2f8773a..81e54f1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/AbstractDBCSCodePage.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/AbstractDBCSCodePage.java @@ -27,11 +27,15 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage { int[] sbcsMapping, String nioCharsetName) { super(id, description, cpgid, cgcsgid, sbcsMapping); if (nioCharsetName != null) { - try { - if (Charset.isSupported(nioCharsetName)) { - this.nioCharset = Charset.forName(nioCharsetName); - } - } catch (Exception ignored) {} + for (String candidate : nioCharsetName.split(",")) { + candidate = candidate.trim(); + try { + if (Charset.isSupported(candidate)) { + this.nioCharset = Charset.forName(candidate); + break; + } + } catch (Exception ignored) {} + } } // Initialize ideographic space (0x4040 <-> \u3000) registerDbcsPair(0x4040, '\u3000'); @@ -250,4 +254,78 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage { public byte[] utf8WithSOSIToEbcdic(String s) { return stringToEbcdic(s, true); } + + /** + * Double-byte character set Graphic Character Set Global ID (DBGID/CGCSGID) matching HoD DBGIDTable. + */ + public int getDbCgcsgid() { + switch (getCpgid()) { + case 930: + case 939: + case 290: + return 370; // 0x0172 + case 933: + return 934; // 0x03A6 + case 935: + return 937; // 0x03A9 + case 937: + return 935; // 0x03A7 + case 1390: + case 1399: + case 1364: + case 1371: + case 1388: + return 65535; // 0xFFFF + default: + return 370; + } + } + + /** + * Double-byte character set Code Page Global ID (DBCS CPGID) matching HoD DBGIDTable. + */ + public int getDbCpgid() { + switch (getCpgid()) { + case 930: + case 939: + case 290: + case 1390: + case 1399: + return 300; // 0x012C + case 933: + case 1364: + return 834; // 0x0342 + case 935: + case 1388: + return 837; // 0x0345 + case 937: + case 1371: + return 835; // 0x0343 + default: + return 300; + } + } + + /** + * Double-byte character set section start byte matching HoD DBCSSectTable. + */ + public byte getDbcsSectionStart() { + return 0x41; + } + + /** + * Double-byte character set section end byte matching HoD DBCSSectTable. + */ + public byte getDbcsSectionEnd() { + switch (getCpgid()) { + case 290: + case 930: + case 935: + case 939: + return 0x7F; + default: + return (byte) 0xFE; + } + } } + diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java index 6f6c403..73090aa 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java @@ -311,6 +311,12 @@ public class CodePageRegistry { register(new Cp1388()); register(new Cp1371()); + // Japanese Katakana & Latin SBCS / DBCS Euro + register(new Cp290()); + register(new Cp1027()); + register(new Cp1390()); + register(new Cp1399()); + // Setup Extended Aliases addAlias("ar", "420"); addAlias("arabic", "420"); @@ -360,10 +366,12 @@ public class CodePageRegistry { addAlias("zh-tw-ext", "1371"); // HoD Converter aliases mapping all HoD converter names to CodePage / Charset - addAlias("1390", "930"); - addAlias("1390jis2004", "930"); - addAlias("1399", "939"); - addAlias("1399jis2004", "939"); + addAlias("1390", "1390"); + addAlias("1390jis2004", "1390"); + addAlias("1399", "1399"); + addAlias("1399jis2004", "1399"); + addAlias("290", "290"); + addAlias("1027", "1027"); addAlias("937macau", "937"); addAlias("1364", "933"); addAlias("1379", "937"); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1027.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1027.java new file mode 100644 index 0000000..d008816 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1027.java @@ -0,0 +1,43 @@ +package haus.nightmare.lib3270j.charset; + +/** + * IBM Code Page 1027 (Japanese Latin Extended SBCS). + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp939.singleByteToChar. + * CCSID / CPGID: 1027, GCSGID: 1172. + */ +public class Cp1027 extends AbstractCodePage { + + public static final String ID = "1027"; + public static final String DESCRIPTION = "Japanese Latin Extended - EBCDIC"; + public static final int CPGID = 1027; + public static final int GCSGID = 1172; + + private static final String SBCS_STR = + "\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" + + "\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" + + "\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" + + "\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" + + " \ufffd\uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\u00a2.<(+|" + + "&\uff69\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\uff70\uff71!$*);\u00ac" + + "-/\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\ufffd,%_>?" + + "\uff7a\uff7b\uff7c\uff7d\uff7e\uff7f\uff80\uff81\uff82`:#@'=\"" + + "\ufffdabcdefghi\uff83\uff84\uff85\uff86\uff87\uff88" + + "\ufffdjklmnopqr\uff89\uff8a\uff8b\uff8c\uff8d\uff8e" + + "\u203e~stuvwxyz\uff8f\uff90\uff91[\uff92\uff93^" + + "\u00a3\u00a5\uff94\uff95\uff96\uff97\uff98\uff99\uff9a\uff9b\uff9c\uff9d]\uff9e\uff9f" + + "{ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "}JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "\\\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f"; + + public static final int[] MAPPING = new int[256]; + static { + for (int i = 0; i < 256; i++) { + MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i; + } + } + + public Cp1027() { + super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1390.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1390.java new file mode 100644 index 0000000..319577e --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1390.java @@ -0,0 +1,23 @@ +package haus.nightmare.lib3270j.charset; + +/** + * IBM Code Page 1390 (Japanese Katakana Extended Mixed DBCS with Euro support: host SBCS CP290+Euro / DBCS CP16684). + * CCSID / CPGID: 1390, GCSGID: 1172. + */ +public class Cp1390 extends AbstractDBCSCodePage { + + public static final String ID = "1390"; + public static final String DESCRIPTION = "Japanese Katakana Extended Mixed DBCS (with Euro)"; + public static final int CPGID = 1390; + public static final int GCSGID = 1172; + + public static final int[] MAPPING = Cp290.MAPPING.clone(); + static { + // Euro symbol at 0xE1 + MAPPING[0xE1] = 0x20AC; + } + + public Cp1390() { + super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1390,IBM1390,Cp1390,x-IBM930,IBM930,Cp930"); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1399.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1399.java new file mode 100644 index 0000000..be8fb08 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp1399.java @@ -0,0 +1,23 @@ +package haus.nightmare.lib3270j.charset; + +/** + * IBM Code Page 1399 (Japanese Latin Extended Mixed DBCS with Euro support: host SBCS CP1027+Euro / DBCS CP16684). + * CCSID / CPGID: 1399, GCSGID: 1172. + */ +public class Cp1399 extends AbstractDBCSCodePage { + + public static final String ID = "1399"; + public static final String DESCRIPTION = "Japanese Latin Extended Mixed DBCS (with Euro)"; + public static final int CPGID = 1399; + public static final int GCSGID = 1172; + + public static final int[] MAPPING = Cp1027.MAPPING.clone(); + static { + // Euro symbol at 0xE1 + MAPPING[0xE1] = 0x20AC; + } + + public Cp1399() { + super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1399,IBM1399,Cp1399,x-IBM939,IBM939,Cp939"); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp290.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp290.java new file mode 100644 index 0000000..56ae3ed --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp290.java @@ -0,0 +1,43 @@ +package haus.nightmare.lib3270j.charset; + +/** + * IBM Code Page 290 (Japanese Katakana Extended SBCS). + * Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp290. + * CCSID / CPGID: 290, GCSGID: 1172. + */ +public class Cp290 extends AbstractCodePage { + + public static final String ID = "290"; + public static final String DESCRIPTION = "Japanese Katakana Extended - EBCDIC"; + public static final int CPGID = 290; + public static final int GCSGID = 1172; + + private static final String SBCS_STR = + "\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" + + "\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" + + "\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" + + "\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" + + " \uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\uff69\u00a3.<(+|" + + "&\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\ufffd\uff70\ufffd!\u00a5*);\u00ac" + + "-/\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ufffd,%_>?" + + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000:#@'=\"" + + "\u0000\uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\uff7a\u0000\uff7b\uff7c\uff7d\uff7e" + + "\uff7f\uff80\uff81\uff82\uff83\uff84\uff85\uff86\uff87\uff88\uff89\u0000\ufffd\uff8a\uff8b\uff8c" + + "\u0000\u203e\uff8d\uff8e\uff8f\uff90\uff91\uff92\uff93\uff94\uff95\u0000\uff96\uff97\uff98\uff99" + + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\uff9a\uff9b\uff9c\uff9d\uff9e\uff9f" + + "\u0000ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "\u0000JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "$\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f"; + + public static final int[] MAPPING = new int[256]; + static { + for (int i = 0; i < 256; i++) { + MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i; + } + } + + public Cp290() { + super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING); + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp930.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp930.java index 185687f..5e16ab8 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp930.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp930.java @@ -12,6 +12,6 @@ public class Cp930 extends AbstractDBCSCodePage { public static final int GCSGID = 1172; public Cp930() { - super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM930"); + super(ID, DESCRIPTION, CPGID, GCSGID, Cp290.MAPPING, "x-IBM930,IBM930,Cp930"); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp939.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp939.java index c5b11db..3aef429 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp939.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/Cp939.java @@ -12,6 +12,6 @@ public class Cp939 extends AbstractDBCSCodePage { public static final int GCSGID = 1172; public Cp939() { - super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM939"); + super(ID, DESCRIPTION, CPGID, GCSGID, Cp1027.MAPPING, "x-IBM939,IBM939,Cp939"); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java index 4a08155..a528417 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/EbcdicTranslator.java @@ -90,6 +90,35 @@ public class EbcdicTranslator { return isDBCSCodePage(); } + public synchronized int getDbCgcsgid() { + if (activeCodePage instanceof AbstractDBCSCodePage) { + return ((AbstractDBCSCodePage) activeCodePage).getDbCgcsgid(); + } + return 370; + } + + public synchronized int getDbCpgid() { + if (activeCodePage instanceof AbstractDBCSCodePage) { + return ((AbstractDBCSCodePage) activeCodePage).getDbCpgid(); + } + return 300; + } + + public synchronized byte getDbcsSectionStart() { + if (activeCodePage instanceof AbstractDBCSCodePage) { + return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionStart(); + } + return 0x41; + } + + public synchronized byte getDbcsSectionEnd() { + if (activeCodePage instanceof AbstractDBCSCodePage) { + return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionEnd(); + } + return 0x7F; + } + + // ========================================================================= // Custom Character Translation Overrides // ========================================================================= diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java index 70a8c54..2cac4d4 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -60,6 +60,8 @@ public class DataStreamProcessor { private byte currentGr = 0; private byte currentCs = 0; private byte currentOl = 0; + private byte currentTr = 0; + private boolean inDBCS = false; /** Functional interface for sending output back through the telnet stack. */ @FunctionalInterface @@ -252,6 +254,7 @@ public class DataStreamProcessor { currentGr = 0; currentCs = 0; currentOl = 0; + currentTr = 0; break; case CMD_WSF: case SNA_CMD_WSF: @@ -395,6 +398,8 @@ public class DataStreamProcessor { currentGr = 0; currentCs = 0; currentOl = 0; + currentTr = 0; + inDBCS = false; } if (eraseFirst) { @@ -405,6 +410,8 @@ public class DataStreamProcessor { currentGr = 0; currentCs = 0; currentOl = 0; + currentTr = 0; + inDBCS = false; } // Process orders and data starting at byte 2 @@ -435,6 +442,7 @@ public class DataStreamProcessor { } case ORDER_SF: { // Start Field + inDBCS = false; if (pos + 1 >= end) { pos = end; break; @@ -456,6 +464,7 @@ public class DataStreamProcessor { } case ORDER_SFE: { // Start Field Extended + inDBCS = false; if (pos + 1 >= end) { pos = end; break; @@ -502,6 +511,7 @@ public class DataStreamProcessor { currentGr = 0; currentCs = 0; currentOl = 0; + currentTr = 0; break; case XA_FOREGROUND: currentFg = (byte) attrValue; @@ -530,6 +540,9 @@ public class DataStreamProcessor { case 0x84: currentOl = (byte) attrValue; break; + case XA_TRANSPARENCY: + currentTr = (byte) attrValue; + break; } pos += 3; lastWasOrder = true; @@ -591,6 +604,44 @@ public class DataStreamProcessor { if (toAddr >= size) toAddr = toAddr % size; + boolean isDbcsSession = (translator != null && translator.isDBCS()); + if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS) && pos + 4 < end) { + int fillChar2 = data[pos + 4] & 0xFF; + char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(fillChar, fillChar2) : '?'; + do { + ExtendedAttribute ea1 = screen.getCell(baddr); + ea1.fa = 0; + ea1.ec = (byte) fillChar; + ea1.ucs4 = uni; + ea1.fg = currentFg; + ea1.bg = currentBg; + ea1.gr = (byte) currentGr; + ea1.cs = ExtendedAttribute.CS_DBCS; + ea1.db = ExtendedAttribute.DB_LEFT; + ea1.ol = currentOl; + ea1.tr = currentTr; + baddr = (baddr + 1) % size; + if (baddr == toAddr) break; + + ExtendedAttribute ea2 = screen.getCell(baddr); + ea2.fa = 0; + ea2.ec = (byte) fillChar2; + ea2.ucs4 = uni; + ea2.fg = currentFg; + ea2.bg = currentBg; + ea2.gr = (byte) currentGr; + ea2.cs = ExtendedAttribute.CS_DBCS; + ea2.db = ExtendedAttribute.DB_RIGHT; + ea2.ol = currentOl; + ea2.tr = currentTr; + baddr = (baddr + 1) % size; + } while (baddr != toAddr); + screen.setBufferAddress(baddr); + pos += 5; + lastWasOrder = true; + break; + } + // Handle GE (graphic escape) prefix byte fillCs = currentCs; char fillUcs4 = 0; @@ -614,7 +665,9 @@ public class DataStreamProcessor { ea.bg = currentBg; ea.gr = (byte) currentGr; ea.cs = fillCs; + ea.db = ExtendedAttribute.DB_NONE; ea.ol = currentOl; + ea.tr = currentTr; baddr = (baddr + 1) % size; } while (baddr != toAddr); screen.setBufferAddress(baddr); @@ -644,6 +697,7 @@ public class DataStreamProcessor { ea.gr = 0; ea.cs = 0; ea.ol = 0; + ea.tr = 0; } } baddr = (baddr + 1) % size; @@ -669,6 +723,7 @@ public class DataStreamProcessor { ea.bg = currentBg; ea.gr = (byte) currentGr; ea.ol = currentOl; + ea.tr = currentTr; baddr = (baddr + 1) % size; screen.setBufferAddress(baddr); pos += 2; @@ -677,6 +732,98 @@ public class DataStreamProcessor { } default: { + boolean isDbcsSession = (translator != null && translator.isDBCS()); + if (isDbcsSession && b == 0x0E) { // Shift-Out (enter DBCS mode) + inDBCS = true; + ExtendedAttribute ea = screen.getCell(baddr); + ea.fa = 0; + ea.ec = (byte) 0x0E; + ea.ucs4 = ' '; + ea.cs = ExtendedAttribute.CS_BASE; + ea.db = ExtendedAttribute.DB_SO; + ea.fg = currentFg; + ea.bg = currentBg; + ea.gr = (byte) currentGr; + ea.ol = currentOl; + ea.tr = currentTr; + baddr = (baddr + 1) % size; + screen.setBufferAddress(baddr); + pos += 1; + lastWasOrder = false; + break; + } else if (isDbcsSession && b == 0x0F) { // Shift-In (exit DBCS mode) + inDBCS = false; + ExtendedAttribute ea = screen.getCell(baddr); + ea.fa = 0; + ea.ec = (byte) 0x0F; + ea.ucs4 = ' '; + ea.cs = ExtendedAttribute.CS_BASE; + ea.db = ExtendedAttribute.DB_SI; + ea.fg = currentFg; + ea.bg = currentBg; + ea.gr = (byte) currentGr; + ea.ol = currentOl; + ea.tr = currentTr; + baddr = (baddr + 1) % size; + screen.setBufferAddress(baddr); + pos += 1; + lastWasOrder = false; + break; + } else if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS)) { + if (pos + 1 < end) { + int b2 = data[pos + 1] & 0xFF; + if (b2 == 0x0F) { // Orphaned single byte before SI + ExtendedAttribute ea = screen.getCell(baddr); + ea.fa = 0; + ea.ec = (byte) b; + ea.ucs4 = translator.ebcdicToUnicode(b); + ea.fg = currentFg; + ea.bg = currentBg; + ea.gr = (byte) currentGr; + ea.cs = currentCs; + ea.ol = currentOl; + ea.tr = currentTr; + ea.db = ExtendedAttribute.DB_NONE; + baddr = (baddr + 1) % size; + screen.setBufferAddress(baddr); + pos += 1; + lastWasOrder = false; + break; + } + char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(b, b2) : '?'; + ExtendedAttribute ea1 = screen.getCell(baddr); + ea1.fa = 0; + ea1.ec = (byte) b; + ea1.ucs4 = uni; + ea1.fg = currentFg; + ea1.bg = currentBg; + ea1.gr = (byte) currentGr; + ea1.cs = ExtendedAttribute.CS_DBCS; + ea1.db = ExtendedAttribute.DB_LEFT; + ea1.ol = currentOl; + ea1.tr = currentTr; + + int nextAddr = (baddr + 1) % size; + ExtendedAttribute ea2 = screen.getCell(nextAddr); + ea2.fa = 0; + ea2.ec = (byte) b2; + ea2.ucs4 = uni; + ea2.fg = currentFg; + ea2.bg = currentBg; + ea2.gr = (byte) currentGr; + ea2.cs = ExtendedAttribute.CS_DBCS; + ea2.db = ExtendedAttribute.DB_RIGHT; + ea2.ol = currentOl; + ea2.tr = currentTr; + + baddr = (baddr + 2) % size; + screen.setBufferAddress(baddr); + pos += 2; + lastWasOrder = false; + break; + } + } + // Regular data byte (EBCDIC character) // Format control codes display as nulls/blanks per 3270 spec boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF || @@ -688,14 +835,18 @@ public class DataStreamProcessor { if (isFCOrder) { ea.ec = 0; // Display as null/blank + ea.ucs4 = 0; } else { ea.ec = (byte) b; + ea.ucs4 = (translator != null) ? translator.ebcdicToUnicode(b) : (char) b; } ea.fg = currentFg; ea.bg = currentBg; ea.gr = (byte) currentGr; ea.cs = currentCs; + ea.db = ExtendedAttribute.DB_NONE; ea.ol = currentOl; + ea.tr = currentTr; baddr = (baddr + 1) % size; screen.setBufferAddress(baddr); @@ -796,7 +947,7 @@ public class DataStreamProcessor { // Buffer contents depending on Reply Mode if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) { // Extended Field or Character Mode - byte curFg = 0, curBg = 0, curGr = 0, curCs = 0; + byte curFg = 0, curBg = 0, curGr = 0, curCs = 0, curTr = 0; for (int i = 0; i < size; i++) { ExtendedAttribute ea = screen.getCell(i); if (ea.isFieldAttribute()) { @@ -807,6 +958,7 @@ public class DataStreamProcessor { if (ea.gr != 0) count++; if (ea.cs != 0) count++; if (ea.ol != 0) count++; + if (ea.tr != 0) count++; outputWrite(ORDER_SFE); outputWrite(count); @@ -825,6 +977,7 @@ public class DataStreamProcessor { } if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); } if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); } + if (ea.tr != 0) { outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF); } } else { if (mode == SF_SRM_CHAR) { // In character mode, output SA if character attributes differ @@ -850,6 +1003,10 @@ public class DataStreamProcessor { outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); curCs = ea.cs; } + if (ea.tr != curTr) { + outputWrite(ORDER_SA); outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF); + curTr = ea.tr; + } } outputWrite(ea.ec & 0xFF); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java index cda0062..a008c11 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java @@ -129,6 +129,7 @@ public class QueryReplyBuilder { QR_DDM, // 0x95 - Distributed Data Management (file transfer) QR_AUXDA, // 0x99 - Auxiliary Devices QR_IMP_PART, // 0xa6 - Implicit Partition Sizes + QR_TRANSPARENCY, // 0xa8 - Background Transparency }; // Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING @@ -218,6 +219,9 @@ public class QueryReplyBuilder { // Implicit Partition (0xA6) appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); + // Background Transparency (0xA8) + appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); + log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")"); return out.toByteArray(); } @@ -245,9 +249,9 @@ public class QueryReplyBuilder { appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); + appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8 if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8 appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0 appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1 appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2 @@ -317,11 +321,7 @@ public class QueryReplyBuilder { appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); break; case QR_TRANSPARENCY: // 0xA8 - if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); - } else { - appendQueryReply(out, QR_NULL, new byte[0]); - } + appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); break; case QR_SEGMENT: // 0xB0 if (graphicsMode.isVectorGraphicsEnabled()) { @@ -390,10 +390,17 @@ public class QueryReplyBuilder { ByteArrayOutputStream out = new ByteArrayOutputStream(); int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE; boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); + boolean wroteDbcs = false; for (int code : codes) { + if (isDbcs && !wroteDbcs && code == QR_DDM) { + out.write(QR_OUTLINING); + out.write(QR_DBCS_ASIA); + wroteDbcs = true; + } out.write(code); - if (isDbcs && code == QR_OUTLINING) { + if (isDbcs && code == QR_OUTLINING && !wroteDbcs) { out.write(QR_DBCS_ASIA); // 0x91 + wroteDbcs = true; } } return out.toByteArray(); @@ -441,7 +448,7 @@ public class QueryReplyBuilder { public int getCharWidth(int rows) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { - return 12; + return 12; // 0x0C } return SW_3279_2; // 9 } @@ -452,6 +459,9 @@ public class QueryReplyBuilder { } public int getCharHeight(int rows) { + if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { + return 24; // 0x18 + } return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; } @@ -477,9 +487,54 @@ public class QueryReplyBuilder { int cgcsgid = 0x02B9; int cpgid = 0x0025; + boolean isDbcs = false; if (screen != null && screen.getTranslator() != null) { cgcsgid = screen.getTranslator().getCgcsgid(); cpgid = screen.getTranslator().getCpgid(); + isDbcs = screen.getTranslator().isDBCS(); + } + + if (isDbcs) { + int dbCgcsgid = screen.getTranslator().getDbCgcsgid(); + int dbCpgid = screen.getTranslator().getDbCpgid(); + byte secStart = screen.getTranslator().getDbcsSectionStart(); + byte secEnd = screen.getTranslator().getDbcsSectionEnd(); + + // 42-byte DBCS Character Sets QR payload (total 46 bytes structured field, matching HoD DS3270.java xxxqrcs) + ByteArrayOutputStream out = new ByteArrayOutputStream(42); + out.write(0x8E); // flags: GE | DBCS / 2-byte charsets (0x8E) + out.write(0x00); // flags 2 + out.write(charW); // SDW (12) + out.write(charH); // SDH (24) + out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); // 4 reserved bytes + out.write(0x0B); // DL = 11 bytes per descriptor + + // Descriptor 1: Base SBCS (11 bytes) + out.write(0x00); out.write(0x00); out.write(0x00); + out.write(0x00); out.write(0x00); + out.write(0x00); out.write(0x00); + out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF); + out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF); + + // Descriptor 2: DBCS Set 1 (11 bytes) + out.write(0x01); // Set ID = 1 + out.write(0x00); // Flags + out.write(0x80); // Local ID = 0x80 (DBCS) + out.write((charW * 2) & 0xFF); // Double cell width (24) + out.write(charH & 0xFF); // Cell height (24) + out.write(secStart); // Section start (e.g. 0x41) + out.write(secEnd); // Section end (e.g. 0xFE or 0x7F) + out.write((dbCgcsgid >> 8) & 0xFF); out.write(dbCgcsgid & 0xFF); + out.write((dbCpgid >> 8) & 0xFF); out.write(dbCpgid & 0xFF); + + // Descriptor 3: APL / Text (11 bytes) + out.write(0x01); out.write(0x00); out.write(0xF1); + out.write(0x00); out.write(0x00); + out.write(0x00); out.write(0x00); + out.write(0x03); out.write(0xC3); // CGCSGID 963 + out.write(0x01); out.write(0x36); // CPGID 310 + + return out.toByteArray(); } if (graphicsMode.isProgrammedSymbolsEnabled()) { @@ -531,6 +586,7 @@ public class QueryReplyBuilder { return out.toByteArray(); } + public byte[] buildColor() { return STATIC_QR_COLOR.clone(); } 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 f0dbf72..7884867 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConstants.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLConstants.java @@ -5,13 +5,27 @@ package haus.nightmare.lib3270j.ecl; */ public interface ECLConstants { - // Presentation space logical planes - int PLANE_TEXT = 1; // Unicode/EBCDIC characters - int PLANE_COLOR = 2; // Color attributes - int PLANE_HILITE = 3; // Extended highlighting - int PLANE_EXTENDED = 4; // Extended character sets / outlining - int PLANE_FIELD = 5; // Field attribute definition bytes - int PLANE_DBCS = 6; // Double-byte character plane + // Presentation space logical planes (Bitmask & ordinal definitions matching IBM Host On-Demand) + int PLANE_TEXT = 1; // Unicode/EBCDIC characters + int PLANE_COLOR = 2; // Color attributes + int PLANE_HILITE = 3; // Extended highlighting + int PLANE_EXTENDED = 4; // Extended character sets / outlining + int PLANE_FIELD = 5; // Field attribute definition bytes + int PLANE_DBCS = 6; // Double-byte character plane (lib3270j convention) + int PLANE_GRID = 8; // Grid lines / outlining plane + + // Canonical IBM Host On-Demand ECLConstants plane bitmasks + int TEXT_PLANE = 1; + int COLOR_PLANE = 2; + int DBCS_PLANE = 4; + int GRID_PLANE = 8; + int FIELD_PLANE = 16; + int EXFIELD_PLANE = 32; + int NLS_PLANE = 64; + int HOST_PLANE = 128; + int RAWTEXT_PLANE = 256; + int COLOR_ATTRIBUTES_PLANE = 512; + int ALL_PLANES = 319; // Color definitions (IBM 3279 standard 16-color palette) char COLOR_NEUTRAL_BLACK = 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 b95bcb0..6f92bf0 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLField.java @@ -295,6 +295,54 @@ public class ECLField { } public void Erase() { erase(); } + /** + * Check whether this field contains any DBCS characters or SO/SI delimiters. + */ + public boolean isDBCS() { + if (ps == null || length <= 0) return false; + ScreenBuffer sb = ps.getScreenBuffer(); + if (sb == null) return false; + int size = sb.getRows() * sb.getCols(); + if (size <= 0) return false; + for (int i = 0; i < length; i++) { + int addr = (dataStart + i) % size; + ExtendedAttribute ea = sb.getCell(addr); + if (ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT + || ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI)) { + return true; + } + } + return false; + } + public boolean IsDBCS() { return isDBCS(); } + + /** + * Check whether this field contains only DBCS characters (excluding nulls / spaces). + */ + public boolean isDBCSOnly() { + if (ps == null || length <= 0) return false; + ScreenBuffer sb = ps.getScreenBuffer(); + if (sb == null) return false; + int size = sb.getRows() * sb.getCols(); + if (size <= 0) return false; + boolean hasDBCS = false; + for (int i = 0; i < length; i++) { + int addr = (dataStart + i) % size; + ExtendedAttribute ea = sb.getCell(addr); + if (ea != null) { + if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT + || ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) { + hasDBCS = true; + } else if (ea.ec != 0 && ea.ec != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') { + return false; + } + } + } + return hasDBCS; + } + public boolean IsDBCSOnly() { return isDBCSOnly(); } + + @Override public boolean equals(Object o) { if (this == o) return true; 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 cb1b24f..1268a02 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLFieldList.java @@ -25,9 +25,22 @@ public class ECLFieldList extends Vector { refresh(); } + public ECLFieldList(ECLPS ps) { + this(ps, ps != null ? ps.getScreenBuffer() : null); + } + + public ECLField getField(int index) { + return (index >= 0 && index < size()) ? get(index) : null; + } + + public ECLField GetField(int index) { + return getField(index); + } + public ECLPS getPS() { return ps; } public ScreenBuffer getScreenBuffer() { return screen; } + /** * Refresh the field list by scanning the presentation space buffer. */ 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 e4ce459..ff637e1 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java @@ -264,10 +264,12 @@ public class ECLPS implements ECLConstants { case PLANE_TEXT: if (ea.isFieldAttribute()) { destBuffer[i] = ' '; + } else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) { + destBuffer[i] = ' '; } else if (ea.ucs4 != 0) { destBuffer[i] = (char) ea.ucs4; } else if (ea.ec != 0) { - destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF); + destBuffer[i] = (translator != null) ? translator.ebcdicToUnicode(ea.ec & 0xFF) : (char) (ea.ec & 0xFF); } else { destBuffer[i] = ' '; } @@ -278,12 +280,43 @@ public class ECLPS implements ECLConstants { case PLANE_HILITE: destBuffer[i] = (char) (ea.gr & 0xFF); break; - case PLANE_EXTENDED: - destBuffer[i] = (char) (ea.cs & 0xFF); + case DBCS_PLANE: // 4 (HoD DBCS_PLANE) or PLANE_EXTENDED for SBCS + if (translator != null && translator.isDBCS()) { + if (ea.db == ExtendedAttribute.DB_LEFT) { + destBuffer[i] = (char) 0xC0; // DB_DBHIGH (first half of DBCS) + } else if (ea.db == ExtendedAttribute.DB_RIGHT) { + destBuffer[i] = (char) 0x80; // DB_DBCS (second half of DBCS) + } else if (ea.db == ExtendedAttribute.DB_SO) { + destBuffer[i] = (char) 0x20; // DB_SO + } else if (ea.db == ExtendedAttribute.DB_SI) { + destBuffer[i] = (char) 0x10; // DB_SI + } else { + destBuffer[i] = 0; + } + } else { + destBuffer[i] = (char) (ea.cs & 0xFF); + } break; case PLANE_FIELD: + case FIELD_PLANE: // 16 destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0; break; + case PLANE_DBCS: // 6 + if (ea.db == ExtendedAttribute.DB_LEFT) { + destBuffer[i] = (char) 0xC0; + } else if (ea.db == ExtendedAttribute.DB_RIGHT) { + destBuffer[i] = (char) 0x80; + } else if (ea.db == ExtendedAttribute.DB_SO) { + destBuffer[i] = (char) 0x20; + } else if (ea.db == ExtendedAttribute.DB_SI) { + destBuffer[i] = (char) 0x10; + } else { + destBuffer[i] = 0; + } + break; + case GRID_PLANE: // 8 (HoD GRID_PLANE / Outlining) + destBuffer[i] = (char) (ea.ol & 0xFF); + break; default: destBuffer[i] = ' '; break; @@ -292,6 +325,107 @@ public class ECLPS implements ECLConstants { return copyLen; } + public synchronized boolean IsDBCSChar(int pos) { + if (screen == null) return false; + int size = getSize(); + if (size <= 0) return false; + pos = ((pos % size) + size) % size; + ExtendedAttribute ea = screen.getCell(pos); + return ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT); + } + + public synchronized boolean isDBCSChar(int pos) { + return IsDBCSChar(pos); + } + + public synchronized boolean IsDBCSChar(int row, int col) { + if (screen == null) return false; + return IsDBCSChar(screen.rowColToAddress(row, col)); + } + + public synchronized boolean isDBCSChar(int row, int col) { + return IsDBCSChar(row, col); + } + + public synchronized boolean IsDBCS1stChar(int pos) { + if (screen == null) return false; + int size = getSize(); + if (size <= 0) return false; + pos = ((pos % size) + size) % size; + ExtendedAttribute ea = screen.getCell(pos); + return ea != null && ea.db == ExtendedAttribute.DB_LEFT; + } + + public synchronized boolean isDBCS1stChar(int pos) { + return IsDBCS1stChar(pos); + } + + public synchronized boolean IsDBCS1stChar(int row, int col) { + if (screen == null) return false; + return IsDBCS1stChar(screen.rowColToAddress(row, col)); + } + + public synchronized boolean isDBCS1stChar(int row, int col) { + return IsDBCS1stChar(row, col); + } + + public synchronized boolean IsDBCS2ndChar(int pos) { + if (screen == null) return false; + int size = getSize(); + if (size <= 0) return false; + pos = ((pos % size) + size) % size; + ExtendedAttribute ea = screen.getCell(pos); + return ea != null && ea.db == ExtendedAttribute.DB_RIGHT; + } + + public synchronized boolean isDBCS2ndChar(int pos) { + return IsDBCS2ndChar(pos); + } + + public synchronized boolean IsDBCS2ndChar(int row, int col) { + if (screen == null) return false; + return IsDBCS2ndChar(screen.rowColToAddress(row, col)); + } + + public synchronized boolean isDBCS2ndChar(int row, int col) { + return IsDBCS2ndChar(row, col); + } + + public synchronized boolean IsSOChar(int pos) { + if (screen == null) return false; + int size = getSize(); + if (size <= 0) return false; + pos = ((pos % size) + size) % size; + ExtendedAttribute ea = screen.getCell(pos); + return ea != null && (ea.db == ExtendedAttribute.DB_SO || (ea.ec & 0xFF) == 0x0E); + } + + public synchronized boolean isSOChar(int pos) { + return IsSOChar(pos); + } + + public synchronized boolean IsSIChar(int pos) { + if (screen == null) return false; + int size = getSize(); + if (size <= 0) return false; + pos = ((pos % size) + size) % size; + ExtendedAttribute ea = screen.getCell(pos); + return ea != null && (ea.db == ExtendedAttribute.DB_SI || (ea.ec & 0xFF) == 0x0F); + } + + public synchronized boolean isSIChar(int pos) { + return IsSIChar(pos); + } + + public synchronized boolean IsSOSIChar(int pos) { + return IsSOChar(pos) || IsSIChar(pos); + } + + public synchronized boolean isSOSIChar(int pos) { + return IsSOSIChar(pos); + } + + public synchronized int GetScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) { if (cArray == null || len <= 0 || screen == null) return 0; int minR = Math.max(1, Math.min(sRow, eRow)); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java index 04c5958..f9d595f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ft/FTDft.java @@ -816,15 +816,14 @@ public class FTDft { for (int i = 0; i < lineStr.length(); i++) { char ch = lineStr.charAt(i); - int ebc = translator != null ? translator.unicodeToEbcdic(ch) : -1; - boolean charIsDbcs = isDbcs && (ch > 0xFF || ebc > 0xFF); - if (charIsDbcs) { + int dbcsVal = (isDbcs && translator != null) ? translator.unicodeToDbcs(ch) : -1; + if (dbcsVal != -1) { if (!inDbcsState && !noSo) { converted.write(0x0E); // Shift-Out inDbcsState = true; } - converted.write((ebc >> 8) & 0xFF); - converted.write(ebc & 0xFF); + converted.write((dbcsVal >> 8) & 0xFF); + converted.write(dbcsVal & 0xFF); } else { if (inDbcsState && !noSo) { converted.write(0x0F); // Shift-In 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 482930c..d78e885 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -292,9 +292,10 @@ public class InputProcessor { } } - // Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode) + // Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode, or DBCS) boolean isAplChar = false; int ebc = -1; + int dbcs = -1; if (aplKeyboardMode) { int aplCode = getAplCodeForChar(ch); if (aplCode >= 0) { @@ -303,10 +304,62 @@ public class InputProcessor { isAplChar = true; } } - if (ebc < 0) { - ebc = translator.unicodeToEbcdic(ch); + if (ebc < 0 && translator != null && translator.isDBCS()) { + dbcs = translator.unicodeToDbcs(ch); + } + if (ebc < 0 && dbcs < 0) { + ebc = (translator != null) ? translator.unicodeToEbcdic(ch) : (int) ch; + } + if (ebc < 0 && dbcs < 0) return; + + if (dbcs >= 0) { + // DBCS character typing + if (insertMode) { + if (!screen.insertChar(baddr, ch)) { + if (oia != null) { + oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW); + } + setKeyboardLocked(true); + return; + } + } else { + int nextPos = screen.incrementAddress(baddr); + if (screen.isFormatted() && screen.getCell(nextPos).isFieldAttribute()) { + // Cannot fit 2-cell DBCS at the last position of the field + if (oia != null) { + oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW); + } + setKeyboardLocked(true); + return; + } + int b1 = (dbcs >> 8) & 0xFF; + int b2 = dbcs & 0xFF; + ExtendedAttribute ea1 = screen.getCell(baddr); + ea1.fa = 0; + ea1.ec = (byte) b1; + ea1.ucs4 = ch; + ea1.cs = ExtendedAttribute.CS_DBCS; + ea1.db = ExtendedAttribute.DB_LEFT; + + ExtendedAttribute ea2 = screen.getCell(nextPos); + ea2.fa = 0; + ea2.ec = (byte) b2; + ea2.ucs4 = ch; + ea2.cs = ExtendedAttribute.CS_DBCS; + ea2.db = ExtendedAttribute.DB_RIGHT; + + if (screen.isFormatted()) { + int faAddr = screen.findFieldAttribute(baddr); + if (faAddr >= 0) { + screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY); + } + } + screen.setCursorAddress(screen.incrementAddress(nextPos)); + } + screen.markAllChanged(); + screen.updateDisplaySnapshot(); + return; } - if (ebc < 0) return; if (insertMode) { // Insert mode: shift characters right from cursor to end of field @@ -1476,38 +1529,12 @@ public class InputProcessor { } public void deleteChar() { - if (!screen.isFormatted()) return; int addr = screen.getCursorAddress(); - int size = screen.getRows() * screen.getCols(); - if (size <= 0) return; - addr = ((addr % size) + size) % size; - - // Cannot delete a field attribute - if (screen.getCell(addr).isFieldAttribute()) return; - - byte faVal = screen.getFieldAttributeAt(addr); - if (faIsProtected(faVal & 0xFF)) return; - - // Shift characters left within the field - int shiftAddr = addr; - int count = 0; - while (count < size) { - int next = screen.incrementAddress(shiftAddr); - if (screen.getCell(next).isFieldAttribute()) { - screen.getCell(shiftAddr).ec = 0; - screen.getCell(shiftAddr).ucs4 = 0; - break; - } - screen.getCell(shiftAddr).ec = screen.getCell(next).ec; - screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4; - shiftAddr = next; - count++; - } - - int faAddr = screen.findFieldAttribute(addr); - if (faAddr >= 0) { - screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY); + if (screen.isFormatted()) { + byte faVal = screen.getFieldAttributeAt(addr); + if (faIsProtected(faVal & 0xFF)) return; } + screen.deleteChar(addr); screen.markAllChanged(); screen.updateDisplaySnapshot(); } @@ -1525,6 +1552,9 @@ public class InputProcessor { return; } } + if (screen.getCell(prev).db == ExtendedAttribute.DB_RIGHT) { + prev = screen.decrementAddress(prev); + } screen.setCursorAddress(prev); deleteChar(); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java index 7948223..b2f6f9c 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java @@ -97,6 +97,11 @@ public class ExtendedAttribute { /** Validation attribute. */ public byte vl; + // Transparency attribute values (0x46 / XA_TRANSPARENCY) + public static final byte TR_DEFAULT = 0x00; + public static final byte TR_OR = (byte) 0xF0; + public static final byte TR_OPAQUE = (byte) 0xFF; + /** Transparency attribute. */ public byte tr; 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 f9d544d..6f7acd3 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java @@ -727,6 +727,14 @@ public class ScreenBuffer { for (int i = 0; i < size; i++) { ExtendedAttribute ea = buffer[i]; if (!ea.isFieldAttribute()) { + if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT) { + // Preserve decoded DBCS character + continue; + } + if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) { + ea.ucs4 = ' '; + continue; + } if (ea.ec == 0) { ea.ucs4 = 0; } else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE @@ -736,6 +744,9 @@ public class ScreenBuffer { } } } + if (translator != null && translator.isDBCS()) { + processSOSI(); + } } private char getAplGraphic(int ec) { @@ -1124,7 +1135,7 @@ public class ScreenBuffer { int count = 0; while (count < size) { int next = incrementAddress(shiftAddr); - if (getCell(next).isFieldAttribute()) { + if (getCell(next).isFieldAttribute() || count == size - 1) { getCell(shiftAddr).clear(); break; } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DBCSDataStreamFullTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DBCSDataStreamFullTest.java new file mode 100644 index 0000000..ae0c05d --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/DBCSDataStreamFullTest.java @@ -0,0 +1,204 @@ +package haus.nightmare.lib3270j.datastream; + +import org.junit.jupiter.api.Test; +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.ECLConstants; +import haus.nightmare.lib3270j.ecl.ECLField; +import haus.nightmare.lib3270j.ecl.ECLFieldList; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.screen.ExtendedAttribute; +import static org.junit.jupiter.api.Assertions.*; +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; + +public class DBCSDataStreamFullTest { + + @Test + public void testDBCSDataStreamShiftOutShiftIn() { + EbcdicTranslator translator = new EbcdicTranslator("930"); + ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + DataStreamProcessor processor = new DataStreamProcessor(screen, translator); + + // Data stream: + // EW, WCC, SBA (0, 0), SO (0x0E), 0x40 0x40 (\u3000 ideographic space), SI (0x0F) + byte[] stream = new byte[] { + (byte) CMD_EW, + (byte) 0xC3, + (byte) ORDER_SBA, + (byte) 0x40, (byte) 0x40, + (byte) 0x0E, // Shift-Out + (byte) 0x40, (byte) 0x40, // DBCS ideographic space + (byte) 0x0F // Shift-In + }; + + processor.processRecord(stream, 0, stream.length, true); + + ExtendedAttribute cell0 = screen.getCell(0); + assertEquals(ExtendedAttribute.DB_SO, cell0.db, "Cell 0 must be DB_SO"); + + ExtendedAttribute cell1 = screen.getCell(1); + assertEquals(ExtendedAttribute.DB_LEFT, cell1.db, "Cell 1 must be DB_LEFT"); + assertEquals('\u3000', cell1.ucs4, "Cell 1 ucs4 must be \\u3000"); + + ExtendedAttribute cell2 = screen.getCell(2); + assertEquals(ExtendedAttribute.DB_RIGHT, cell2.db, "Cell 2 must be DB_RIGHT"); + assertEquals('\u3000', cell2.ucs4, "Cell 2 ucs4 must be \\u3000"); + + ExtendedAttribute cell3 = screen.getCell(3); + assertEquals(ExtendedAttribute.DB_SI, cell3.db, "Cell 3 must be DB_SI"); + } + + @Test + public void testDBCSRepeatToAddressOrder() { + EbcdicTranslator translator = new EbcdicTranslator("930"); + ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + DataStreamProcessor processor = new DataStreamProcessor(screen, translator); + + // In DBCS mode (after SO), ORDER_RA consumes 2 data bytes and fills pairs + byte[] stream = new byte[] { + (byte) CMD_EW, + (byte) 0xC3, + (byte) ORDER_SBA, + (byte) 0x40, (byte) 0x40, + (byte) 0x0E, // SO at 0 + (byte) ORDER_RA, + (byte) 0x40, (byte) 0x4B, // RA up to address 11 (covers cells 1..10) + (byte) 0x40, (byte) 0x40, // 2-byte repeat char (\u3000) + (byte) 0x0F // SI at 11 + }; + + processor.processRecord(stream, 0, stream.length, true); + + assertEquals(ExtendedAttribute.DB_SO, screen.getCell(0).db); + for (int i = 1; i < 11; i += 2) { + assertEquals(ExtendedAttribute.DB_LEFT, screen.getCell(i).db, "Cell " + i + " must be DB_LEFT"); + assertEquals('\u3000', screen.getCell(i).ucs4); + assertEquals(ExtendedAttribute.DB_RIGHT, screen.getCell(i + 1).db, "Cell " + (i + 1) + " must be DB_RIGHT"); + assertEquals('\u3000', screen.getCell(i + 1).ucs4); + } + assertEquals(ExtendedAttribute.DB_SI, screen.getCell(11).db); + } + + @Test + public void testECLPlanesDBCSAndOutlining() { + EbcdicTranslator translator = new EbcdicTranslator("930"); + ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + DataStreamProcessor processor = new DataStreamProcessor(screen, translator); + + byte[] stream = new byte[] { + (byte) CMD_EW, + (byte) 0xC3, + (byte) ORDER_SBA, + (byte) 0x40, (byte) 0x40, + (byte) 0x0E, // SO at 0 + (byte) 0x40, (byte) 0x40, // DBCS at 1, 2 + (byte) 0x0F // SI at 3 + }; + processor.processRecord(stream, 0, stream.length, true); + + // Add outlining to cell 1 + screen.getCell(1).ol = (byte) (ExtendedAttribute.OL_BOX); + + ECLPS eclPS = new ECLPS(screen, null, translator); + + char[] dbcsPlane = new char[4]; + int count = eclPS.getPlane(ECLConstants.DBCS_PLANE, dbcsPlane, 0, 4); + assertEquals(4, count); + assertEquals(0x20, dbcsPlane[0], "SO must be 0x20 in DBCS plane"); + assertEquals(0xC0, dbcsPlane[1], "DB_LEFT must be 0xC0 (DB_DBHIGH)"); + assertEquals(0x80, dbcsPlane[2], "DB_RIGHT must be 0x80 (DB_DBCS)"); + assertEquals(0x10, dbcsPlane[3], "SI must be 0x10 in DBCS plane"); + + char[] gridPlane = new char[4]; + eclPS.getPlane(ECLConstants.GRID_PLANE, gridPlane, 0, 4); + assertEquals((char) (ExtendedAttribute.OL_BOX & 0xFF), gridPlane[1]); + + // Helper methods + assertTrue(eclPS.IsSOChar(0)); + assertFalse(eclPS.IsDBCSChar(0)); + + assertTrue(eclPS.IsDBCSChar(1)); + assertTrue(eclPS.IsDBCS1stChar(1)); + assertFalse(eclPS.IsDBCS2ndChar(1)); + + assertTrue(eclPS.IsDBCSChar(2)); + assertFalse(eclPS.IsDBCS1stChar(2)); + assertTrue(eclPS.IsDBCS2ndChar(2)); + + assertTrue(eclPS.IsSIChar(3)); + assertFalse(eclPS.IsDBCSChar(3)); + } + + @Test + public void testECLFieldDBCSMethods() { + EbcdicTranslator translator = new EbcdicTranslator("930"); + ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + DataStreamProcessor processor = new DataStreamProcessor(screen, translator); + + // Field with SO + DBCS + SI + byte[] stream = new byte[] { + (byte) CMD_EW, + (byte) 0xC3, + (byte) ORDER_SBA, + (byte) 0x40, (byte) 0x40, + (byte) ORDER_SF, + (byte) FA_PRINTABLE, // Field at 0 + (byte) 0x0E, // SO at 1 + (byte) 0x40, (byte) 0x40, // DBCS at 2, 3 + (byte) 0x0F // SI at 4 + }; + processor.processRecord(stream, 0, stream.length, true); + + ECLPS eclPS = new ECLPS(screen, null, translator); + ECLFieldList fieldList = new ECLFieldList(eclPS); + fieldList.Refresh(); + assertEquals(1, fieldList.size()); + + ECLField field = fieldList.getField(0); + assertTrue(field.isDBCS(), "Field should report isDBCS = true"); + assertTrue(field.isDBCSOnly(), "Field should report isDBCSOnly = true"); + } + + @Test + public void testQueryReplyBuilderDBCSCharsets() { + EbcdicTranslator translator = new EbcdicTranslator("930"); + ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator); + QueryReplyBuilder builder = new QueryReplyBuilder(screen); + + byte[] charsets = builder.buildCharsets(); + assertNotNull(charsets); + assertEquals(42, charsets.length, "DBCS Character Sets QR payload must be 42 bytes (matching HoD xxxqrcs)"); + + assertEquals((byte) 0x8E, charsets[0], "Flags 1 must be 0x8E"); + assertEquals((byte) 0x00, charsets[1], "Flags 2 must be 0x00"); + assertEquals(12, charsets[2], "SDW must be 12"); + assertEquals(24, charsets[3], "SDH must be 24"); + assertEquals(0x0B, charsets[8], "Descriptor length DL must be 11 (0x0B)"); + + // Descriptor 2 (DBCS): offset 9 + 11 = 20 + assertEquals(0x01, charsets[20], "Set ID must be 1"); + assertEquals((byte) 0x80, charsets[22], "Local ID must be 0x80 (DBCS)"); + assertEquals(24, charsets[23], "DBCS cell width must be double (24)"); + assertEquals(24, charsets[24], "DBCS cell height must be 24"); + assertEquals(0x41, charsets[25], "Section start must be 0x41"); + assertEquals(0x7F, charsets[26], "Section end for 930 must be 0x7F"); + + // DB CGCSGID = 370 (0x0172), DB CPGID = 300 (0x012C) + int cgcsgid = ((charsets[27] & 0xFF) << 8) | (charsets[28] & 0xFF); + int cpgid = ((charsets[29] & 0xFF) << 8) | (charsets[30] & 0xFF); + assertEquals(370, cgcsgid); + assertEquals(300, cpgid); + + // Summary Query Reply contains Outlining (0x8C) and DBCS Asia (0x91) + byte[] summary = builder.buildSummary(); + boolean hasOutlining = false; + boolean hasDbcsAsia = false; + for (byte b : summary) { + if ((b & 0xFF) == QR_OUTLINING) hasOutlining = true; + if ((b & 0xFF) == QR_DBCS_ASIA) hasDbcsAsia = true; + } + assertTrue(hasOutlining, "Summary must include QR_OUTLINING"); + assertTrue(hasDbcsAsia, "Summary must include QR_DBCS_ASIA"); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java index dce4449..24a2abc 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java @@ -49,9 +49,24 @@ public class QueryReplyBuilderTest { // In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT be advertised in Summary int sumLen = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF); + boolean hasB0 = false; + boolean hasA8 = false; for (int i = 5; i < 1 + sumLen; i++) { - assertNotEquals(QR_SEGMENT, replies[i] & 0xFF); + if ((replies[i] & 0xFF) == QR_SEGMENT) hasB0 = true; + if ((replies[i] & 0xFF) == QR_TRANSPARENCY) hasA8 = true; } + assertFalse(hasB0); + assertTrue(hasA8, "QR_TRANSPARENCY (0xA8) should be advertised even in GraphicsMode.NONE"); + + // And QR_TRANSPARENCY structured field payload must be present in replies + boolean hasA8Payload = false; + for (int i = 0; i < replies.length - 3; i++) { + if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_TRANSPARENCY) { + hasA8Payload = true; + break; + } + } + assertTrue(hasA8Payload, "QR_TRANSPARENCY structured field should be included in buildAllQueryReplies"); } @Test diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyPhase2Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyPhase2Test.java index aabc584..dd125da 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyPhase2Test.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyPhase2Test.java @@ -87,10 +87,10 @@ public class QueryReplyPhase2Test { assertNotNull(reply); List sfs = parseQueryReplies(reply); - // Base query reply must contain exactly 10 structured fields for SBCS: + // Base query reply must contain exactly 11 structured fields for SBCS: // 0x80 (Summary), 0x81 (Usable Area), 0x84 (Alpha Partitions), 0x85 (Character Sets), - // 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part) - assertEquals(10, sfs.size()); + // 0x86 (Color), 0x87 (Highlighting), 0x88 (Reply Modes), 0x95 (DDM), 0x99 (AuxDA), 0xA6 (Implicit Part), 0xA8 (Transparency) + assertEquals(11, sfs.size()); assertEquals(QR_SUMMARY, sfs.get(0).qcode); assertEquals(QR_USABLE_AREA, sfs.get(1).qcode); @@ -102,10 +102,10 @@ public class QueryReplyPhase2Test { assertEquals(QR_DDM, sfs.get(7).qcode); assertEquals(QR_AUXDA, sfs.get(8).qcode); assertEquals(QR_IMP_PART, sfs.get(9).qcode); + assertEquals(QR_TRANSPARENCY, sfs.get(10).qcode); - // Verify that Vector Graphics SF payloads (0xA8, 0xB0..0xB6) are NOT sent in base reply + // Verify that Vector Graphics SF payloads (0xB0..0xB6) are NOT sent in base reply for (ParsedSF sf : sfs) { - assertNotEquals(QR_TRANSPARENCY, sf.qcode); assertNotEquals(QR_SEGMENT, sf.qcode); assertNotEquals(QR_PROCEDURE, sf.qcode); assertNotEquals(QR_LINETYPE, sf.qcode); @@ -246,7 +246,7 @@ public class QueryReplyPhase2Test { assertTrue(sent.length > 0); assertEquals((byte) AID_SF, sent[0]); List sfs = parseQueryReplies(sent); - assertEquals(10, sfs.size()); + assertEquals(11, sfs.size()); // Test SF_RP_QLIST (0x03) with SF_RPQ_LIST (0x00) and codes 0xB0, 0xB4 byte[] queryList = new byte[] { diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/TransparencyDataStreamTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/TransparencyDataStreamTest.java new file mode 100644 index 0000000..3ef0e1c --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/TransparencyDataStreamTest.java @@ -0,0 +1,143 @@ +package haus.nightmare.lib3270j.datastream; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.screen.ExtendedAttribute; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; + +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; +import static org.junit.jupiter.api.Assertions.*; + +public class TransparencyDataStreamTest { + + private ScreenBuffer screen; + private DataStreamProcessor processor; + private ByteArrayOutputStream output; + + @BeforeEach + public void setUp() { + EbcdicTranslator translator = new EbcdicTranslator(); + screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator); + processor = new DataStreamProcessor(screen, translator); + output = new ByteArrayOutputStream(); + processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length)); + } + + @Test + public void testSetAttributeTransparency() { + // Send SFE with XA_TRANSPARENCY = XAT_OPAQUE (0xFF) + byte[] sfeRecord = new byte[] { + (byte) CMD_EW, 0x00, + (byte) ORDER_SFE, 0x02, + (byte) XA_3270, (byte) FA_PRINTABLE, + (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE + }; + processor.processRecord(sfeRecord, 0, sfeRecord.length, true); + + ExtendedAttribute cell0 = screen.getCell(0); + assertTrue(cell0.isFieldAttribute()); + assertEquals(ExtendedAttribute.TR_OPAQUE, cell0.tr); + + // Send SA with XA_TRANSPARENCY = XAT_OPAQUE followed by character 'A' (0xC1) + byte[] saRecord = new byte[] { + (byte) CMD_W, 0x00, + (byte) ORDER_SBA, 0x00, 0x01, + (byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE, + (byte) 0xC1 // 'A' + }; + processor.processRecord(saRecord, 0, saRecord.length, true); + + ExtendedAttribute charCell1 = screen.getCell(1); + assertFalse(charCell1.isFieldAttribute()); + assertEquals((byte) 0xC1, charCell1.ec); + assertEquals(ExtendedAttribute.TR_OPAQUE, charCell1.tr); + + // Send SA with XA_ALL (0x00) followed by character 'B' (0xC2) -> should reset tr to 0 + byte[] resetRecord = new byte[] { + (byte) CMD_W, 0x00, + (byte) ORDER_SBA, 0x00, 0x02, + (byte) ORDER_SA, (byte) XA_ALL, 0x00, + (byte) 0xC2 // 'B' + }; + processor.processRecord(resetRecord, 0, resetRecord.length, true); + + ExtendedAttribute charCell2 = screen.getCell(2); + assertFalse(charCell2.isFieldAttribute()); + assertEquals((byte) 0xC2, charCell2.ec); + assertEquals(ExtendedAttribute.TR_DEFAULT, charCell2.tr); + } + + @Test + public void testRepeatToAddressWithTransparency() { + // Set SA Transparency = Opaque, then RA to address 5 with char 'C' (0xC3) + byte[] raRecord = new byte[] { + (byte) CMD_EW, 0x00, + (byte) ORDER_SBA, 0x00, 0x00, + (byte) ORDER_SA, (byte) XA_TRANSPARENCY, (byte) ExtendedAttribute.TR_OPAQUE, + (byte) ORDER_RA, 0x00, 0x05, (byte) 0xC3 + }; + processor.processRecord(raRecord, 0, raRecord.length, true); + + for (int i = 0; i < 5; i++) { + ExtendedAttribute cell = screen.getCell(i); + assertEquals((byte) 0xC3, cell.ec); + assertEquals(ExtendedAttribute.TR_OPAQUE, cell.tr, "Cell at " + i + " should have TR_OPAQUE"); + } + } + + @Test + public void testReadBufferExtendedFieldIncludesTransparency() { + screen.setReplyMode((byte) SF_SRM_XFIELD); + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE; + + processor.processReadBuffer(); + + byte[] result = output.toByteArray(); + assertTrue(result.length > 3); + assertEquals((byte) AID_NO, result[0]); + + // Find ORDER_SFE + boolean foundTr = false; + for (int i = 3; i < result.length - 2; i++) { + if ((result[i] & 0xFF) == ORDER_SFE) { + int pairCount = result[i + 1] & 0xFF; + int pos = i + 2; + for (int p = 0; p < pairCount; p++) { + int attrType = result[pos] & 0xFF; + int attrVal = result[pos + 1] & 0xFF; + if (attrType == XA_TRANSPARENCY && attrVal == (ExtendedAttribute.TR_OPAQUE & 0xFF)) { + foundTr = true; + } + pos += 2; + } + } + } + assertTrue(foundTr, "Read Buffer in SF_SRM_XFIELD mode should report XA_TRANSPARENCY"); + } + + @Test + public void testReadBufferCharacterModeIncludesTransparency() { + screen.setReplyMode((byte) SF_SRM_CHAR); + screen.getCell(0).ec = (byte) 0xC1; + screen.getCell(0).tr = ExtendedAttribute.TR_OPAQUE; + + processor.processReadBuffer(); + + byte[] result = output.toByteArray(); + assertTrue(result.length > 3); + + boolean foundSaTr = false; + for (int i = 3; i < result.length - 2; i++) { + if ((result[i] & 0xFF) == ORDER_SA && (result[i + 1] & 0xFF) == XA_TRANSPARENCY && (result[i + 2] & 0xFF) == (ExtendedAttribute.TR_OPAQUE & 0xFF)) { + foundSaTr = true; + break; + } + } + assertTrue(foundSaTr, "Read Buffer in SF_SRM_CHAR mode should output ORDER_SA for XA_TRANSPARENCY"); + } +}