Codepage, ECL, and some UI updates
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m29s

This commit is contained in:
2026-09-22 13:38:12 +00:00
parent d05f1a4c45
commit 3d3a939ece
29 changed files with 1773 additions and 92 deletions
@@ -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 {
@@ -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,13 +759,25 @@ 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);
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 {
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
if (ch <= 0x20 || ch == 0xFF) {
line.append(' ');
} else {
line.append(ch);
}
}
}
// Trim trailing whitespace on copied lines per HoD CutPasteOptions
int end = line.length();
while (end > 0 && line.charAt(end - 1) == ' ') {
@@ -782,13 +813,25 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
prevRow = curRow;
}
ExtendedAttribute ea = sb.getCell(addr);
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 {
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
if (ch <= 0x20 || ch == 0xFF) {
line.append(' ');
} else {
line.append(ch);
}
}
}
int end = line.length();
while (end > 0 && line.charAt(end - 1) == ' ') {
end--;
@@ -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;
}
}
@@ -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<SavedHost> 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<SavedHost> 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<SavedHost> 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<Settings.StartupBehavior> sbBox = (JComboBox<Settings.StartupBehavior>) 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) {}
}
}
@@ -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);
}
}
@@ -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; }
}
@@ -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); }
}
@@ -27,12 +27,16 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
int[] sbcsMapping, String nioCharsetName) {
super(id, description, cpgid, cgcsgid, sbcsMapping);
if (nioCharsetName != null) {
for (String candidate : nioCharsetName.split(",")) {
candidate = candidate.trim();
try {
if (Charset.isSupported(nioCharsetName)) {
this.nioCharset = Charset.forName(nioCharsetName);
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;
}
}
}
@@ -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");
@@ -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);
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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);
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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
// =========================================================================
@@ -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);
}
@@ -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]);
}
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();
}
@@ -5,13 +5,27 @@ package haus.nightmare.lib3270j.ecl;
*/
public interface ECLConstants {
// Presentation space logical planes
// 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
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;
@@ -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;
@@ -25,9 +25,22 @@ public class ECLFieldList extends Vector<ECLField> {
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.
*/
@@ -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:
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));
@@ -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
@@ -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;
if (screen.isFormatted()) {
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);
}
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();
}
@@ -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;
@@ -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;
}
@@ -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");
}
}
@@ -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
@@ -87,10 +87,10 @@ public class QueryReplyPhase2Test {
assertNotNull(reply);
List<ParsedSF> 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<ParsedSF> 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[] {
@@ -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");
}
}