Add consts and update tests
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m20s

This commit is contained in:
2026-08-31 10:22:12 -04:00
parent a9956298e2
commit 7b5bc7ec11
25 changed files with 4198 additions and 542 deletions
@@ -54,6 +54,7 @@ public class Telnet3270Client {
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
this.inputProcessor.setOIA(oia);
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
@@ -197,11 +198,17 @@ public class Telnet3270Client {
fsm.sendNVTString(s);
}
// ========== Convenience input methods ==========
// ========== Convenience input methods & HoD compatibility ==========
/** Type a character at the cursor position. */
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
/** Process a character at the current cursor position. */
public void processChar(char ch) { inputProcessor.processChar(ch); }
/** Process a character with explicit position and insert mode flag. */
public void processChar(char ch, int pos, boolean insert) { inputProcessor.processChar(ch, pos, insert); }
/** Type a string at the cursor position. */
public void typeString(String s) {
for (char ch : s.toCharArray()) {
@@ -218,6 +225,7 @@ public class Telnet3270Client {
public void sendEnter() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
}
public void processEnter() { inputProcessor.processEnter(); }
/** Send a PF key (1-24). */
public void sendPF(int number) {
@@ -239,6 +247,7 @@ public class Telnet3270Client {
}
inputProcessor.sendAid(aid);
}
public void processPF(int number) { inputProcessor.processPF(number); }
/** Send a PA key (1-3). */
public void sendPA(int number) {
@@ -251,44 +260,104 @@ public class Telnet3270Client {
}
inputProcessor.sendAid(aid);
}
public void processPA(int number) { inputProcessor.processPA(number); }
/** Send Clear key. */
public void sendClear() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
}
public void processClear() { inputProcessor.processClear(); }
/** Move cursor up. */
public void cursorUp() { inputProcessor.cursorUp(); }
public void cursorUp() { inputProcessor.cursorUp(); }
public void processCursorUp() { inputProcessor.processCursorUp(); }
/** Move cursor down. */
public void cursorDown() { inputProcessor.cursorDown(); }
public void cursorDown() { inputProcessor.cursorDown(); }
public void processCursorDown() { inputProcessor.processCursorDown(); }
/** Move cursor left. */
public void cursorLeft() { inputProcessor.cursorLeft(); }
public void cursorLeft() { inputProcessor.cursorLeft(); }
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
/** Move cursor right. */
public void cursorRight() { inputProcessor.cursorRight(); }
public void cursorRight() { inputProcessor.cursorRight(); }
public void processCursorRight() { inputProcessor.processCursorRight(); }
/** Move cursor to home position. */
public void cursorHome() { inputProcessor.cursorHome(); }
public void cursorHome() { inputProcessor.cursorHome(); }
public void processHome() { inputProcessor.processHome(); }
/** Tab to next unprotected field. */
public void tab() { inputProcessor.tab(); }
public void tab() { inputProcessor.tab(); }
public void processTab() { inputProcessor.processTab(); }
/** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); }
public void backTab() { inputProcessor.backTab(); }
public void processBackTab() { inputProcessor.processBackTab(); }
/** Move cursor to next line. */
public void newline() { inputProcessor.newline(); }
public void newline() { inputProcessor.newline(); }
public void processNewline() { inputProcessor.processNewline(); }
/** Delete character under cursor. */
public void deleteChar() { inputProcessor.deleteChar(); }
public void processDelete() { inputProcessor.processDelete(); }
/** Backspace character before cursor. */
public void backspace() { inputProcessor.backspace(); }
public void processBackspace() { inputProcessor.processBackspace(); }
/** Erase to end of field. */
public void eraseEof() { inputProcessor.eraseEof(); }
public void processEraseEOF() { inputProcessor.processEraseEOF(); }
/** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); }
public void eraseInput() { inputProcessor.eraseInput(); }
public void processEraseInput() { inputProcessor.processEraseInput(); }
/** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); }
public void processDup() { inputProcessor.processDup(); }
/** Insert Field Mark order. */
public void fieldMark() { inputProcessor.fieldMark(); }
public void fieldMark() { inputProcessor.fieldMark(); }
public void processFieldMark() { inputProcessor.processFieldMark(); }
/** Toggle Insert Mode. */
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
public void processToggleInsert() { inputProcessor.processToggleInsert(); }
/** Move word left. */
public void processWordLeft() { inputProcessor.processWordLeft(); }
/** Move word right. */
public void processWordRight() { inputProcessor.processWordRight(); }
/** Move to field end. */
public void processFieldEnd() { inputProcessor.processFieldEnd(); }
/** Attention key. */
public void attn() { inputProcessor.attn(); }
public void processAttn() { inputProcessor.processAttn(); }
/** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); }
public void sysReq() { inputProcessor.sysReq(); }
public void processSysReq() { inputProcessor.processSysReq(); }
/** Reset (unlock keyboard, reset OIA). */
public void reset() { inputProcessor.reset(); }
public void processReset() { inputProcessor.processReset(); }
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
/** Trigger Light Pen selection at the specified screen address. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
public boolean processCurSel() { return inputProcessor.processCurSel(); }
public boolean processCursorSelect() { return inputProcessor.processCursorSelect(); }
/** Trigger Light Pen selection at the specified screen address or cursor position. */
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
public boolean processLightPen() { return inputProcessor.processLightPen(); }
public boolean processLightPen(int addr) { return inputProcessor.processLightPen(addr); }
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return dsProcessor.getProgramSymbolManager();
@@ -164,10 +164,11 @@ public class EbcdicTranslator {
}
/**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
* Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification.
*/
public char getAplGraphic(int ec) {
switch (ec & 0xFF) {
public char mapAPL(int ebcdicCodePoint) {
switch (ebcdicCodePoint & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
@@ -198,7 +199,14 @@ public class EbcdicTranslator {
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return ebcdicToUnicode(ec & 0xFF);
default: return ebcdicToUnicode(ebcdicCodePoint & 0xFF);
}
}
/**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
*/
public char getAplGraphic(int ec) {
return mapAPL(ec);
}
}
@@ -516,11 +516,14 @@ public class DataStreamProcessor {
// Handle GE (graphic escape) prefix
byte fillCs = currentCs;
char fillUcs4 = 0;
if (pos + 4 < end && fillChar == ORDER_GE) {
fillChar = data[pos + 4] & 0xFF;
fillCs = CS_GE;
fillUcs4 = (translator != null) ? translator.mapAPL(fillChar) : (char) fillChar;
pos += 5;
} else {
fillUcs4 = (translator != null) ? translator.ebcdicToUnicode(fillChar) : (char) fillChar;
pos += 4;
}
@@ -529,6 +532,7 @@ public class DataStreamProcessor {
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) fillChar;
ea.ucs4 = fillUcs4;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
@@ -581,6 +585,7 @@ public class DataStreamProcessor {
ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) geChar;
ea.cs = CS_GE;
ea.ucs4 = (translator != null) ? translator.mapAPL(geChar) : (char) geChar;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
@@ -686,7 +691,7 @@ public class DataStreamProcessor {
// ========== Read Buffer ==========
private void processReadBuffer() {
public void processReadBuffer() {
outputPos = 0;
int size = screen.getRows() * screen.getCols();
@@ -786,11 +791,21 @@ public class DataStreamProcessor {
// ========== Read Modified ==========
private void processReadModified(boolean all) {
public void processReadModified(boolean all) {
if (ftDft != null && ftDft.readModified()) {
return;
}
if (inputProcessor != null) {
int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO;
inputProcessor.setLastAid(AID_NO);
byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all);
if (outputSender != null) {
outputSender.send3270Data(data);
}
return;
}
outputPos = 0;
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
@@ -849,10 +864,11 @@ public class DataStreamProcessor {
sendOutput();
}
// ========== Write Structured Field ==========
private void processWriteStructuredField(byte[] data, int offset, int length) {
int pos = offset + 1; // Skip WSF command byte
public void processWriteStructuredField(byte[] data, int offset, int length) {
if (data == null || length <= 0)
return;
int firstByte = data[offset] & 0xFF;
int pos = (firstByte == CMD_WSF || firstByte == SNA_CMD_WSF) ? offset + 1 : offset;
int end = offset + length;
while (pos < end) {
@@ -887,10 +903,19 @@ public class DataStreamProcessor {
case SF_ACTIVATE_PART:
processActivatePartition(data, pos, fieldLen);
break;
case SF_OUTBOUND_DS:
case SF_SET_WINDOW: // 0x0F: Set Window / Modify Partition / 3270 Graphics Viewport
processSFSetWindow(data, pos, fieldLen);
break;
case SF_3270_GRAPHICS: // 0x20: 3270 Graphics / Object Control
processSFObjectControl(data, pos, fieldLen);
break;
case SF_DOCUMENT_DATA: // 0x24: Document Data / embedded SCS / Object Control
processSFDocumentData(data, pos, fieldLen);
break;
case SF_OUTBOUND_DS: // 0x40
processOutbound3270DS(data, pos, fieldLen);
break;
case SF_TRANSFER_DATA:
case SF_TRANSFER_DATA: // 0xD0
if (ftDft != null) {
ftDft.processStructuredField(data, pos, fieldLen);
} else {
@@ -904,95 +929,7 @@ public class DataStreamProcessor {
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
if (fieldLen >= 4) {
int sfSubId = data[pos + 3] & 0xFF;
switch (sfSubId) {
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
if (fieldLen > 4) {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, pos + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
gocaDecoder.setGraphicsCursorActive(true);
notifyScreenUpdated();
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
StringBuilder hexDump = new StringBuilder();
for (int i = 0; i < Math.min(32, fieldLen); i++) {
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
}
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9;
int targetH = screen.getRows() * 16;
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
if (flags == 0x80) { // SPAN_FIRST
gocaAccumulator.reset();
currentGocaSubtype = sfSubId;
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x00) { // SPAN_MIDDLE
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x40) { // SPAN_LAST
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else {
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
}
notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else {
gocaDecoder.decodeStream(data, orderOffset, orderLen);
}
notifyScreenUpdated();
}
break;
}
default:
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
break;
}
}
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3);
notifyScreenUpdated();
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
@@ -1280,11 +1217,275 @@ public class DataStreamProcessor {
return pid >= 0 && pid <= 255;
}
// ========== Missing Structured Field Handlers (Phase 2) ==========
public void processSFSetWindow(byte[] data, int offset, int fieldLen) {
if (fieldLen >= 4) {
int sfSubId = data[offset + 3] & 0xFF;
switch (sfSubId) {
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
if (fieldLen > 4) {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, offset + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
gocaDecoder.setGraphicsCursorActive(true);
notifyScreenUpdated();
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
int flags = (fieldLen >= 6) ? (data[offset + 5] & 0xC0) : 0xC0;
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9;
int targetH = screen.getRows() * 16;
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
if (flags == 0x80) { // SPAN_FIRST
gocaAccumulator.reset();
currentGocaSubtype = sfSubId;
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x00) { // SPAN_MIDDLE
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
} else if (flags == 0x40) { // SPAN_LAST
if (orderLen > 0) {
gocaAccumulator.write(data, orderOffset, orderLen);
}
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else {
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
}
notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else {
gocaDecoder.decodeStream(data, orderOffset, orderLen);
}
notifyScreenUpdated();
}
break;
}
default:
if (fieldLen >= 11) {
int xMin = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
int yMin = ((data[offset + 5] & 0xFF) << 8) | (data[offset + 6] & 0xFF);
int xMax = ((data[offset + 7] & 0xFF) << 8) | (data[offset + 8] & 0xFF);
int yMax = ((data[offset + 9] & 0xFF) << 8) | (data[offset + 10] & 0xFF);
graphicsPlane.setViewingWindow(xMin, yMin, xMax, yMax);
}
log.fine("SF 0x0F Set Window processed (len=" + fieldLen + ")");
break;
}
}
}
public void processSFSetWindow(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFSetWindow(sf, off, sf.length - off);
}
}
public void processSFObjectControl(byte[] data, int offset, int fieldLen) {
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, offset + 3, fieldLen - 3);
notifyScreenUpdated();
}
}
public void processSFObjectControl(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFObjectControl(sf, off, sf.length - off);
}
}
public void processSFDocumentData(byte[] data, int offset, int fieldLen) {
if (fieldLen > 3) {
if (embeddedScsProcessor != null) {
embeddedScsProcessor.processHostData(data, offset + 3, fieldLen - 3);
}
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.processProcedureOrders(data, offset + 3, fieldLen - 3);
notifyScreenUpdated();
}
}
public void processSFDocumentData(byte[] sf) {
if (sf != null && sf.length >= 3) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFDocumentData(sf, off, sf.length - off);
}
}
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
if (fieldLen > 5) {
if (fieldLen >= 4) {
int pid = data[offset + 3] & 0xFF;
screen.setActivePartition(pid);
processRecord(data, offset + 4, fieldLen - 4, false);
if (fieldLen > 4) {
processRecord(data, offset + 4, fieldLen - 4, false);
}
}
}
public void processOutbound3270DS(byte[] sf) {
if (sf != null && sf.length >= 4) {
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processOutbound3270DS(sf, off, sf.length - off);
}
}
// ========== Convenience Overloads ==========
public void processRecord(byte[] data) {
if (data != null && data.length > 0) {
processRecord(data, 0, data.length, false);
}
}
public void processWrite(byte[] data) {
if (data != null && data.length > 0) {
processRecord(data, 0, data.length, false);
}
}
public void processEraseWrite(byte[] data) {
if (data != null && data.length > 0) {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(false);
graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, 0, data.length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
screen.translateToUnicode();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
}
public void processEraseWriteAlternate(byte[] data) {
if (data != null && data.length > 0) {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(true);
graphicsPlane.clear();
if (gocaDecoder != null) {
gocaDecoder.setGraphicsCursorActive(false);
}
processWrite(data, 0, data.length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
screen.translateToUnicode();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
}
public void processEraseAllUnprotected() {
synchronized (screen.getRenderLock()) {
screen.eraseAllUnprotected();
}
}
public void processReadModified() {
processReadModified(false);
}
public void processReadModifiedAll() {
processReadModified(true);
}
public void processWriteStructuredField(byte[] data) {
if (data != null && data.length > 0) {
processWriteStructuredField(data, 0, data.length);
}
}
public void processSFReadPartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSFReadPartition(data, off, data.length - off);
}
}
public void processSFReadPartitionQuery(byte[] data) {
sendAllQueryReplies();
}
public void processSFReadPartitionQueryList(byte[] data) {
if (data != null && data.length >= 6) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
int qlStart = off + 6;
if (data.length > qlStart) {
byte[] codes = new byte[data.length - qlStart];
System.arraycopy(data, qlStart, codes, 0, codes.length);
sendRequestedQueryReplies(codes);
} else {
sendAllQueryReplies();
}
} else {
sendAllQueryReplies();
}
}
public void processSetReplyMode(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processSetReplyMode(data, off, data.length - off);
}
}
public void processCreatePartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processCreatePartition(data, off, data.length - off);
}
}
public void processDestroyPartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processDestroyPartition(data, off, data.length - off);
}
}
public void processActivatePartition(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processActivatePartition(data, off, data.length - off);
}
}
public void processEraseReset(byte[] data) {
if (data != null && data.length >= 3) {
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
processEraseReset(data, off, data.length - off);
}
}
@@ -294,7 +294,7 @@ public class QueryReplyBuilder {
out.write(data, 0, data.length);
}
private byte[] buildSummary() {
public byte[] buildSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
@@ -307,7 +307,13 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
public byte[] buildUsableArea() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildUsableArea(maxCols, maxRows, maxCols * maxRows);
}
public byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
out.write(0x00); // no special character features
@@ -316,12 +322,12 @@ public class QueryReplyBuilder {
out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
out.write((Xr_HOD >> 24) & 0xFF);
out.write((Xr_HOD >> 16) & 0xFF);
out.write((Xr_HOD >> 8) & 0xFF);
out.write(Xr_HOD & 0xFF);
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
out.write((Yr_HOD >> 24) & 0xFF);
out.write((Yr_HOD >> 16) & 0xFF);
out.write((Yr_HOD >> 8) & 0xFF);
@@ -359,8 +365,14 @@ public class QueryReplyBuilder {
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
}
private byte[] buildAlphaPartitions(int maxRows) {
int bufSize = screen.getMaxCols() * screen.getMaxRows();
public byte[] buildAlphaPartitions() {
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildAlphaPartitions(maxRows);
}
public byte[] buildAlphaPartitions(int maxRows) {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int bufSize = maxCols * maxRows;
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
out.write((bufSize >> 8) & 0xFF); // total partition storage high
@@ -369,7 +381,7 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
private byte[] buildCharsets() {
public byte[] buildCharsets() {
int charW = getCharWidth();
int charH = getCharHeight();
@@ -429,7 +441,7 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
private byte[] buildColor() {
public byte[] buildColor() {
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
return new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4,
@@ -443,7 +455,7 @@ public class QueryReplyBuilder {
};
}
private byte[] buildHighlighting() {
public byte[] buildHighlighting() {
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
return new byte[] {
0x04, 0x00, (byte) 0xF0,
@@ -453,11 +465,15 @@ public class QueryReplyBuilder {
};
}
private byte[] buildReplyModes() {
public byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
}
private byte[] buildDdm(int bufferSize) {
public byte[] buildDdm() {
return buildDdm(4096);
}
public byte[] buildDdm(int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
out.write(0x00); // reserved
out.write(0x00); // reserved
@@ -470,7 +486,13 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
public byte[] buildImplicitPartition() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildImplicitPartition(maxCols, maxRows);
}
public byte[] buildImplicitPartition(int maxCols, int maxRows) {
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters
@@ -514,6 +536,12 @@ public class QueryReplyBuilder {
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
}
public byte[] buildSegment() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildSegment(maxCols, maxRows);
}
public byte[] buildSegment(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
return new byte[]{
@@ -524,10 +552,20 @@ public class QueryReplyBuilder {
};
}
public byte[] buildGraphics() {
return buildSegment();
}
public byte[] buildGraphics(int maxCols, int maxRows) {
return buildSegment(maxCols, maxRows);
}
public byte[] buildProcedure() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildProcedure(maxCols, maxRows);
}
public byte[] buildProcedure(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
return new byte[]{
@@ -540,6 +578,10 @@ public class QueryReplyBuilder {
};
}
public byte[] buildGImage() {
return buildProcedure();
}
public byte[] buildGImage(int maxCols, int maxRows) {
return buildProcedure(maxCols, maxRows);
}
@@ -581,6 +623,16 @@ public class QueryReplyBuilder {
appendPort(out);
}
public byte[] buildPort() {
ByteArrayOutputStream out = new ByteArrayOutputStream(70);
appendPort(out);
return out.toByteArray();
}
public byte[] buildOemFormat() {
return buildPort();
}
public byte[] buildGrColor() {
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
@@ -602,6 +654,10 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
public byte[] buildGraphicColor() {
return buildGrColor();
}
public byte[] buildGColor() {
return buildGrColor();
}
@@ -49,4 +49,23 @@ public interface ECLConstants {
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
// Alphanumeric entry types (ECLOIA.getAlphanumericType())
int TYPE_ALPHANUMERIC = 0;
int TYPE_NUMERIC = 1;
int TYPE_DBCS = 2;
int ALPHANUMERIC_NORMAL = 0;
int ALPHANUMERIC_NUMERIC = 1;
int ALPHANUMERIC_DBCS = 2;
// Status condition flags
int STATUS_READY = 0;
int STATUS_X_SYSTEM = 1;
int STATUS_X_NUM = 2;
int STATUS_X_PROT = 3;
int STATUS_X_WAIT = 4;
int STATUS_X_INSERT = 5;
int STATUS_X_COMM = 6;
int STATUS_X_OVERFLOW = 7;
int STATUS_X_OP = 8;
}
@@ -1,5 +1,7 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
@@ -56,24 +58,34 @@ public class ECLField {
return cols > 0 ? endPos % cols : 0;
}
private byte getLiveAttribute() {
if (ps != null && ps.getScreenBuffer() != null) {
ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos);
if (cell.isFieldAttribute()) {
return cell.fa;
}
}
return attribute;
}
public boolean isModified() {
return faIsModified(attribute & 0xFF);
return faIsModified(getLiveAttribute() & 0xFF);
}
public boolean isProtected() {
return faIsProtected(attribute & 0xFF);
return faIsProtected(getLiveAttribute() & 0xFF);
}
public boolean isNumeric() {
return faIsNumeric(attribute & 0xFF);
return faIsNumeric(getLiveAttribute() & 0xFF);
}
public boolean isHighIntensity() {
return faIsHigh(attribute & 0xFF);
return faIsHigh(getLiveAttribute() & 0xFF);
}
public boolean isHidden() {
return faIsZero(attribute & 0xFF);
return faIsZero(getLiveAttribute() & 0xFF);
}
public boolean isDisplay() {
@@ -81,11 +93,11 @@ public class ECLField {
}
public boolean isPenSelectable() {
return faIsSelectable(attribute & 0xFF);
return faIsSelectable(getLiveAttribute() & 0xFF);
}
public short getAttribute() {
return (short) (attribute & 0xFF);
return (short) (getLiveAttribute() & 0xFF);
}
/**
@@ -140,6 +152,80 @@ public class ECLField {
}
}
/** Return true if this field wraps from bottom of screen to top. */
public boolean isWrapped() {
return startPos > endPos;
}
/** Last data buffer address (same as getEnd). */
public int getDataEnd() {
return endPos;
}
/**
* Check if the specified buffer address is contained within this field (including its FA).
*/
public boolean contains(int pos) {
if (ps == null) return false;
int size = ps.getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
if (startPos <= endPos) {
return pos >= startPos && pos <= endPos;
} else {
// Wrapped field across screen boundary
return pos >= startPos || pos <= endPos;
}
}
/**
* Check if the specified row and column is contained within this field.
*/
public boolean contains(int row, int col) {
if (ps == null) return false;
int cols = ps.getCols();
return contains(row * cols + col);
}
/**
* Set the Modified Data Tag (MDT) for this field.
*/
public void setModified(boolean modified) {
if (ps != null && ps.getScreenBuffer() != null) {
ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos);
if (cell.isFieldAttribute()) {
if (modified) {
cell.fa = (byte) (cell.fa | FA_MODIFY);
} else {
cell.fa = (byte) (cell.fa & ~FA_MODIFY);
}
ps.getScreenBuffer().markAllChanged();
ps.getScreenBuffer().updateDisplaySnapshot();
}
}
}
/**
* Erase all character data within this field to nulls.
*/
public void erase() {
if (isProtected() || length <= 0 || ps == null || ps.getScreenBuffer() == null) return;
ScreenBuffer sb = ps.getScreenBuffer();
int size = sb.getRows() * sb.getCols();
if (size <= 0) return;
for (int i = 0; i < length; i++) {
int addr = (dataStart + i) % size;
ExtendedAttribute ea = sb.getCell(addr);
ea.ec = 0;
ea.ucs4 = 0;
}
setModified(false);
sb.markAllChanged();
sb.updateDisplaySnapshot();
}
@Override
public String toString() {
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]",
@@ -123,6 +123,52 @@ public class ECLFieldList {
return findField(row * cols + col);
}
/**
* Get the field preceding the given field in the field list.
*/
public synchronized ECLField getPreviousField(ECLField next) {
if (next == null || fields.isEmpty()) return null;
int idx = fields.indexOf(next);
if (idx > 0) {
return fields.get(idx - 1);
} else if (idx == 0) {
return fields.get(fields.size() - 1);
}
return null;
}
/**
* Find the field at the given buffer position (alias for findField).
*/
public ECLField findFieldAt(int pos) {
return findField(pos);
}
/**
* Find the field at the given row and column.
*/
public ECLField findFieldAt(int row, int col) {
return findField(row, col);
}
/**
* Find the field preceding the one at the given buffer position.
*/
public synchronized ECLField findPrevField(int pos) {
ECLField curr = findField(pos);
if (curr == null) return null;
return getPreviousField(curr);
}
/**
* Find the field succeeding the one at the given buffer position.
*/
public synchronized ECLField findNextField(int pos) {
ECLField curr = findField(pos);
if (curr == null) return null;
return getNextField(curr);
}
/**
* Find field containing the given text string.
*/
@@ -147,6 +193,13 @@ public class ECLFieldList {
return f;
}
}
// Wrap around search to beginning of field list
for (int i = 0; i < startIdx; i++) {
ECLField f = fields.get(i);
if (f.getText().contains(text)) {
return f;
}
}
return null;
}
}
@@ -5,6 +5,7 @@ import java.util.List;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
@@ -73,6 +74,92 @@ public class ECLOIA implements ECLConstants {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
}
private int inhibitOverride = INHIBIT_NOT_INHIBITED;
public void setInputInhibited(int reason) {
if (this.inhibitOverride != reason) {
this.inhibitOverride = reason;
notifyOIAChanged();
}
}
/**
* Get the alphanumeric character entry type allowed at current cursor position.
* Returns TYPE_ALPHANUMERIC (0), TYPE_NUMERIC (1), or TYPE_DBCS (2).
*/
public int getAlphanumericType() {
if (screen == null || !screen.isFormatted()) {
return TYPE_ALPHANUMERIC;
}
int cur = screen.getCursorAddress();
ExtendedAttribute ea = screen.getCell(cur);
if (ea != null && (ea.cs == ExtendedAttribute.CS_DBCS || ea.db != 0)) {
return TYPE_DBCS;
}
byte fa = screen.getFieldAttributeAt(cur);
if (faIsNumeric(fa & 0xFF)) {
return TYPE_NUMERIC;
}
return TYPE_ALPHANUMERIC;
}
public String getAlphanumericTypeString() {
switch (getAlphanumericType()) {
case TYPE_NUMERIC: return "N";
case TYPE_DBCS: return "D";
case TYPE_ALPHANUMERIC:
default: return "A";
}
}
public boolean isXSystem() {
return getInputInhibited() == INHIBIT_SYSTEM_LOCK;
}
public boolean isXProt() {
return getInputInhibited() == INHIBIT_PROTECTED_FIELD;
}
public boolean isXNum() {
return getInputInhibited() == INHIBIT_NUMERIC_ONLY;
}
public boolean isXWait() {
return isXSystem();
}
public boolean isXInsert() {
return isInsertMode();
}
public boolean isXComm() {
return isCommError() || getInputInhibited() == INHIBIT_COMM_CHECK;
}
public boolean isXOverflow() {
return getInputInhibited() == INHIBIT_OVERFLOW;
}
public boolean isXOperatorDue() {
return getInputInhibited() == INHIBIT_OPERATOR_DUE;
}
public String getStatusString() {
if (isCommError()) return "X-COMM";
int inhibit = getInputInhibited();
switch (inhibit) {
case INHIBIT_SYSTEM_LOCK: return "X-SYSTEM";
case INHIBIT_NUMERIC_ONLY: return "X-NUM";
case INHIBIT_PROTECTED_FIELD: return "X-PROT";
case INHIBIT_OVERFLOW: return "X-OVERFLOW";
case INHIBIT_COMM_CHECK: return "X-COMM";
case INHIBIT_OPERATOR_DUE: return "X-OP";
default:
if (isInsertMode()) return "X-INSERT";
return "READY";
}
}
/**
* Get the current Input Inhibited code.
* Returns one of INHIBIT_* constants from ECLConstants.
@@ -81,6 +168,9 @@ public class ECLOIA implements ECLConstants {
if (isCommError()) {
return INHIBIT_COMM_CHECK;
}
if (inhibitOverride != INHIBIT_NOT_INHIBITED) {
return inhibitOverride;
}
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
return INHIBIT_SYSTEM_LOCK;
}
@@ -152,8 +152,8 @@ public class ECLPS implements ECLConstants {
}
/**
* Search for a string in the presentation space.
* Returns 1-based or 0-based position, or -1 if not found.
* Search for a string in the presentation space (0-based indexing).
* Returns 0-based position, or -1 if not found.
*/
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
if (target == null || target.isEmpty() || screen == null) return -1;
@@ -174,7 +174,6 @@ public class ECLPS implements ECLConstants {
int targetLen = target.length();
if (dir == SEARCH_FORWARD) {
// Forward search with wrapping
for (int i = 0; i < size; i++) {
int pos = (startPos + i) % size;
if (matchesAt(screenText, target, pos, size)) {
@@ -182,7 +181,6 @@ public class ECLPS implements ECLConstants {
}
}
} else {
// Backward search with wrapping
for (int i = 0; i < size; i++) {
int pos = (startPos - i + size) % size;
if (matchesAt(screenText, target, pos, size)) {
@@ -193,6 +191,78 @@ public class ECLPS implements ECLConstants {
return -1;
}
public int searchString(String target) {
return searchString(target, 0, 0, SEARCH_FORWARD, false);
}
public int searchString(String target, int startRow, int startCol) {
return searchString(target, startRow, startCol, SEARCH_FORWARD, false);
}
// ========== IBM HoD SearchPS / SearchPSExt (1-based API) ==========
public int SearchPS(String text) {
return SearchPSExt(text, 1, getSize(), SEARCH_FORWARD, false, true);
}
public int SearchPS(String text, int startRow, int startCol) {
int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), SEARCH_FORWARD, false, true);
}
public int SearchPS(String text, int startRow, int startCol, int dir) {
int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), dir, false, true);
}
public int SearchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) {
int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), dir, ignoreCase, true);
}
/**
* SearchPS extended method conforming to IBM ECL specification.
* Uses 1-based positions and returns 1-based index (or 0 if not found).
*/
public synchronized int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
if (text == null || text.isEmpty() || screen == null) return 0;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return 0;
int s0 = Math.max(0, Math.min(startPos - 1, size - 1));
int e0 = Math.max(0, Math.min(endPos - 1, size - 1));
char[] fullScreen = new char[size];
getPlane(PLANE_TEXT, fullScreen, 0, size);
String screenText = new String(fullScreen);
if (ignoreCase) {
screenText = screenText.toLowerCase();
text = text.toLowerCase();
}
int count = wrap ? size : (dir == SEARCH_FORWARD ? (e0 >= s0 ? e0 - s0 + 1 : size - s0 + e0 + 1)
: (s0 >= e0 ? s0 - e0 + 1 : s0 + size - e0 + 1));
if (dir == SEARCH_FORWARD) {
for (int i = 0; i < count; i++) {
int pos = (s0 + i) % size;
if (!wrap && e0 >= s0 && pos > e0) break;
if (matchesAt(screenText, text, pos, size)) {
return pos + 1; // 1-based
}
}
} else {
for (int i = 0; i < count; i++) {
int pos = (s0 - i + size) % size;
if (!wrap && s0 >= e0 && pos < e0) break;
if (matchesAt(screenText, text, pos, size)) {
return pos + 1; // 1-based
}
}
}
return 0;
}
private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length();
for (int j = 0; j < len; j++) {
@@ -204,6 +274,62 @@ public class ECLPS implements ECLConstants {
return true;
}
// ========== Rectangular Block Copy & Paste ==========
/**
* Copy a rectangular text region from (sRow, sCol) to (eRow, eCol) inclusive.
*/
public synchronized String copyString(int sRow, int sCol, int eRow, int eCol) {
if (screen == null) return "";
int rows = screen.getRows();
int cols = screen.getCols();
if (rows <= 0 || cols <= 0) return "";
int minR = Math.max(0, Math.min(sRow, eRow));
int maxR = Math.min(rows - 1, Math.max(sRow, eRow));
int minC = Math.max(0, Math.min(sCol, eCol));
int maxC = Math.min(cols - 1, Math.max(sCol, eCol));
int sliceWidth = maxC - minC + 1;
StringBuilder sb = new StringBuilder();
for (int r = minR; r <= maxR; r++) {
char[] rowBuf = new char[sliceWidth];
getPlane(PLANE_TEXT, rowBuf, r * cols + minC, sliceWidth);
sb.append(rowBuf);
if (r < maxR) {
sb.append("\n");
}
}
return sb.toString();
}
/**
* Paste a multi-line rectangular block of text starting at (row, col).
*/
public synchronized int pasteString(String text, int row, int col) {
if (text == null || text.isEmpty() || screen == null) return 0;
int rows = screen.getRows();
int cols = screen.getCols();
if (rows <= 0 || cols <= 0) return 0;
String[] lines = text.split("\r?\n");
int count = 0;
for (int i = 0; i < lines.length; i++) {
int targetRow = (row + i) % rows;
String line = lines[i];
for (int c = 0; c < line.length() && (col + c) < cols; c++) {
int pos = targetRow * cols + (col + c);
setCursorPos(pos);
if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(c));
}
count++;
}
}
return count;
}
/**
* Paste text with line wrapping across unprotected fields.
*/
@@ -224,15 +350,15 @@ public class ECLPS implements ECLConstants {
int curPos = screen.getCursorAddress();
int curCol = curPos % cols;
if (endCol > 0 && curCol >= endCol) {
// Advance to next row
int nextRow = (curPos / cols + 1) % rows;
setCursorPos(nextRow * cols);
}
inputProcessor.typeCharacter(line.charAt(i));
if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(i));
}
charsPasted++;
}
if (l < lines.length - 1) {
// Newline key between lines
if (l < lines.length - 1 && inputProcessor != null) {
inputProcessor.newline();
}
}
@@ -247,4 +373,103 @@ public class ECLPS implements ECLConstants {
inputProcessor.sendKeys(keys);
}
}
/**
* Send keystrokes with configurable inter-character or inter-mnemonic delay.
*/
public void sendCharacters(String keys, int delayMs) {
if (keys == null || keys.isEmpty()) return;
if (delayMs <= 0) {
sendKeys(keys);
return;
}
int i = 0;
int len = keys.length();
while (i < len) {
if (keys.charAt(i) == '[') {
int close = keys.indexOf(']', i);
if (close > i) {
String mnemonic = keys.substring(i, close + 1);
sendKeys(mnemonic);
i = close + 1;
} else {
sendKeys(keys.substring(i, i + 1));
i++;
}
} else {
sendKeys(keys.substring(i, i + 1));
i++;
}
if (delayMs > 0 && i < len) {
try {
Thread.sleep(delayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
// ========== Synchronization & ECL Automation Waits ==========
/**
* Block until the specified text appears anywhere on the presentation space.
*/
public boolean waitForScreen(String text, long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (searchString(text) >= 0) {
return true;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return searchString(text) >= 0;
}
/**
* Block until the specified text appears at the given (row, col) coordinate.
*/
public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
String onScreen = getString(row, col, text.length());
if (text.equals(onScreen)) {
return true;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return text.equals(getString(row, col, text.length()));
}
/**
* Block until the cursor moves to (row, col).
*/
public boolean waitForCursor(int row, int col, long timeoutMs) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMs) {
if (getCursorRow() == row && getCursorCol() == col) {
return true;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return getCursorRow() == row && getCursorCol() == col;
}
}
@@ -19,21 +19,36 @@ public class FillArea {
public static class Edge {
public final double x1, y1;
public final double x2, y2;
public final int direction; // +1 if y1 < y2 (upward), -1 if y1 > y2 (downward)
public Edge(double x1, double y1, double x2, double y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.direction = (y2 > y1) ? 1 : -1;
}
}
private int fillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private final List<Edge> edges = new ArrayList<>();
private final List<double[]> subpathsX = new ArrayList<>();
private final List<double[]> subpathsY = new ArrayList<>();
public FillArea() {}
public FillArea(int fillRule) {
this.fillRule = fillRule;
}
public synchronized void setFillRule(int fillRule) {
this.fillRule = fillRule;
}
public synchronized int getFillRule() {
return fillRule;
}
/**
* Adds a single directed edge to the edge table.
*/
@@ -104,12 +119,47 @@ public class FillArea {
subpathsY.clear();
}
/**
* Rasterizes and fills a direct polygon on the target GraphicsPlane.
*/
public synchronized void fill(GraphicsPlane plane, int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
clear();
addPolygon(px, py, numPoints);
fill(plane, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, null);
}
/**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
*/
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, ProgramSymbolManager psm) {
fill(plane, fillColorArgb, patternSet, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, this.fillRule, psm);
}
private static class NodeIntersection implements Comparable<NodeIntersection> {
final double x;
final int dir;
NodeIntersection(double x, int dir) {
this.x = x;
this.dir = dir;
}
@Override
public int compareTo(NodeIntersection other) {
return Double.compare(this.x, other.x);
}
}
/**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule.
*/
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, int fillRule, ProgramSymbolManager psm) {
if (plane == null) return;
if (edges.isEmpty() && subpathsX.isEmpty()) return;
@@ -140,7 +190,7 @@ public class FillArea {
int iMinY = Math.max(0, (int) Math.floor(minY));
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
List<Double> nodeX = new ArrayList<>();
List<NodeIntersection> intersections = new ArrayList<>();
byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null;
@@ -156,45 +206,84 @@ public class FillArea {
}
for (int y = iMinY; y <= iMaxY; y++) {
nodeX.clear();
intersections.clear();
double scanY = y + 0.5;
for (Edge e : edges) {
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) {
double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1);
nodeX.add(x);
intersections.add(new NodeIntersection(x, e.direction));
}
}
Collections.sort(nodeX);
Collections.sort(intersections);
for (int i = 0; i < nodeX.size(); i += 2) {
if (i + 1 >= nodeX.size()) break;
int leftX = Math.max(0, (int) Math.round(nodeX.get(i)));
int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1)));
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
// Non-Zero Winding Rule: evaluate winding count
int winding = 0;
for (int i = 0; i < intersections.size() - 1; i++) {
winding += intersections.get(i).dir;
if (winding != 0) {
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
}
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
}
}
} else {
// Even-Odd / Alternate Rule
for (int i = 0; i < intersections.size(); i += 2) {
if (i + 1 >= intersections.size()) break;
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg);
}
}
}
}
@@ -50,6 +50,8 @@ public final class GocaConstants {
public static final int G_GSMS = 0x1B; // Set Marker Size
public static final int G_GSCP = 0x21; // Set Current Position
public static final int G_GSAP = 0x22; // Arc Parameters
public static final int G_GSC = 0x22; // Segment Characteristics
public static final int G_GSVW_DEF = 0x23; // Set Viewing Window Definition
public static final int G_GSECOL = 0x26; // Set Extended Color
public static final int G_GSVW = 0x27; // Set Viewing Window
public static final int G_GSPT = 0x28; // Set Pattern Symbol
@@ -157,6 +159,19 @@ public final class GocaConstants {
public static final int MIX_XOR = 4;
public static final int MIX_UNDER = 5;
// Fill Rules (GBAR 0x68 flags)
public static final int FILL_RULE_EVEN_ODD = 0;
public static final int FILL_RULE_WINDING = 1;
// Image Formats & Compression (GBIMG 0xD1)
public static final int IMG_UNCOMPRESSED = 0;
public static final int IMG_RLE = 1;
public static final int IMG_MMR = 2;
public static final int BPP_1 = 1;
public static final int BPP_2 = 2;
public static final int BPP_4 = 4;
public static final int BPP_8 = 8;
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
public static final int[] GOCA_COLORS = new int[] {
0xFF00FF00, // 0: Default (Green)
@@ -33,6 +33,8 @@ public class GocaDecoder {
private int fillColor = GocaConstants.GOCA_COLORS[0];
private int charDir = GocaConstants.CD_LR;
private double charAngle = 0.0;
private double charShear = 0.0;
private double fractionalLineWidth = 1.0;
private int charWidth = 9;
private int charHeight = 16;
private int charSet = 0;
@@ -42,11 +44,16 @@ public class GocaDecoder {
private int arcParamR = 0;
private int arcParamS = 1;
private boolean segChained = false;
private boolean segDynamic = false;
private boolean segVisible = true;
private ProgramSymbolManager programSymbolManager;
// Area accumulation
private boolean inArea = false;
private boolean areaDrawBoundary = true;
private int areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private boolean areaFill = true;
private final List<Integer> areaPointsX = new ArrayList<>();
private final List<Integer> areaPointsY = new ArrayList<>();
@@ -59,6 +66,8 @@ public class GocaDecoder {
private int imgY = 0;
private int imgWidth = 0;
private int imgHeight = 0;
private int imgBitDepth = GocaConstants.BPP_1;
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
private final List<Byte> imgBuffer = new ArrayList<>();
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
@@ -198,6 +207,89 @@ public class GocaDecoder {
}
}
public double getCharAngle() {
return charAngle;
}
public synchronized void setCharAngle(double angle) {
this.charAngle = angle;
}
public double getCharShear() {
return charShear;
}
public synchronized void setCharShear(double shear) {
this.charShear = shear;
}
public double getFractionalLineWidth() {
return fractionalLineWidth;
}
public synchronized void setFractionalLineWidth(double flw) {
this.fractionalLineWidth = Math.max(0.1, flw);
if (plane != null) {
plane.setFractionalLineWidth(this.fractionalLineWidth);
}
}
public boolean isSegChained() {
return segChained;
}
public boolean isSegDynamic() {
return segDynamic;
}
public boolean isSegVisible() {
return segVisible;
}
/**
* Processes GOCA order 0x23 / 0x27 Viewing Window clipping viewport.
*/
public synchronized void processViewingWindow(byte[] data) {
if (data == null || data.length < 8) return;
int xMin = readCoord(data, 0);
int yMin = readCoord(data, 2);
int xMax = readCoord(data, 4);
int yMax = readCoord(data, 6);
logger.info(String.format("GOCA processViewingWindow: [%d..%d, %d..%d]", xMin, xMax, yMin, yMax));
if (plane != null) {
plane.setViewingWindow(xMin, yMin, xMax, yMax);
}
}
/**
* Processes GOCA order 0x22 Segment Characteristics (chained/non-chained, dynamic, visible).
*/
public synchronized void processSegmentCharacteristics(byte[] data) {
if (data == null || data.length < 1) return;
int flags = data[0] & 0xFF;
this.segChained = (flags & 0x80) != 0;
this.segDynamic = (flags & 0x40) != 0;
this.segVisible = (flags & 0x20) == 0;
logger.info(String.format("GOCA processSegmentCharacteristics: flags=0x%02x (chained=%b, dynamic=%b, visible=%b)",
flags, segChained, segDynamic, segVisible));
}
/**
* Processes GOCA order 0x11 Fractional Line Width calculation.
*/
public synchronized void processFractionalLineWidth(byte[] data) {
if (data == null || data.length < 1) return;
int intPart = data[0] & 0xFF;
int fracPart = (data.length > 1) ? (data[1] & 0xFF) : 0;
double flw = intPart + (fracPart / 256.0);
if (flw <= 0.0) flw = 1.0;
this.fractionalLineWidth = flw;
if (plane != null) {
plane.setFractionalLineWidth(flw);
}
logger.info("GOCA processFractionalLineWidth: flw=" + flw);
}
public synchronized void resetDefaults() {
curX = 0;
curY = 0;
@@ -217,6 +309,10 @@ public class GocaDecoder {
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL;
fractionalLineWidth = 1.0;
if (plane != null) {
plane.setFractionalLineWidth(1.0);
}
markerType = GocaConstants.MK_PLUS;
markerSize = 5;
markerColor = curColor;
@@ -226,14 +322,18 @@ public class GocaDecoder {
fillColor = curColor;
charDir = GocaConstants.CD_LR;
charAngle = 0.0;
charShear = 0.0;
charSet = 0;
charPrecision = GocaConstants.CP_STRING;
inArea = false;
areaDrawBoundary = true;
areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
areaFill = true;
areaPointsX.clear();
areaPointsY.clear();
inImage = false;
imgBitDepth = GocaConstants.BPP_1;
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
imgBuffer.clear();
}
@@ -270,6 +370,10 @@ public class GocaDecoder {
if (idx + 1 >= end) {
return -1;
}
// Fractional Line Width (0x11): 2-byte operand [int][frac] or 1-byte operand [int]
if (order == GocaConstants.G_GSFLW) {
return (idx + 2 < end) ? 3 : 2;
}
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
if (order < 0x20) {
return 2;
@@ -497,7 +601,15 @@ public class GocaDecoder {
idx += orderLen;
break;
}
case GocaConstants.G_GSVW: { // Set Viewing Window (0x27)
case GocaConstants.G_GSVW_DEF:
case GocaConstants.G_GSVW: { // Set Viewing Window (0x23 / 0x27)
if (payloadLen >= 8 && idx + 9 < end) {
byte[] vwData = new byte[8];
System.arraycopy(inputData, idx + 2, vwData, 0, 8);
processViewingWindow(vwData);
} else if (payloadLen == 0 && plane != null) {
plane.clearViewingWindow();
}
idx += orderLen;
break;
}
@@ -558,6 +670,17 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
if (payloadLen >= 4 && idx + 5 < end) {
int sx = readCoord(inputData, idx + 2);
int sy = readCoord(inputData, idx + 4);
if (sx != 0 || sy != 0) {
charShear = Math.toDegrees(Math.atan2(sx, sy));
}
} else if (payloadLen >= 2 && idx + 3 < end) {
int intPart = inputData[idx + 2];
int fracPart = (payloadLen >= 2) ? (inputData[idx + 3] & 0xFF) : 0;
charShear = (intPart + fracPart / 256.0) * 45.0;
}
idx += orderLen;
break;
}
@@ -581,8 +704,16 @@ public class GocaDecoder {
idx += orderLen;
break;
}
case GocaConstants.G_GSFLW: { // Set Fractional Line Width (0x11)
if (orderLen >= 2 && idx + 1 < end) {
byte[] flwData = new byte[orderLen - 1];
System.arraycopy(inputData, idx + 1, flwData, 0, flwData.length);
processFractionalLineWidth(flwData);
}
idx += orderLen;
break;
}
case 0x06:
case 0x11:
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
lineType = inputData[idx + 1] & 0xFF;
idx += orderLen;
@@ -661,8 +792,9 @@ public class GocaDecoder {
case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary));
beginArea(drawBoundary);
int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
beginArea(drawBoundary, fillRule);
idx += orderLen;
break;
}
@@ -770,7 +902,18 @@ public class GocaDecoder {
int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6);
int h = readCoord(inputData, idx + 8);
beginImage(x, y, w, h);
int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
if (payloadLen >= 9) {
int fmt = inputData[idx + 10] & 0xFF;
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
}
if (payloadLen >= 10) {
compression = inputData[idx + 11] & 0xFF;
}
beginImage(x, y, w, h, bitDepth, compression);
}
idx += orderLen;
break;
@@ -887,16 +1030,21 @@ public class GocaDecoder {
}
private void beginArea(boolean drawBoundary) {
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
}
private void beginArea(boolean drawBoundary, int fillRule) {
this.inArea = true;
this.areaDrawBoundary = drawBoundary;
this.areaFillRule = fillRule;
this.areaFill = true;
this.fillColor = this.curColor;
this.areaPointsX.clear();
this.areaPointsY.clear();
this.areaPolygons.clear();
this.currentPolyPts = 0;
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillColor, patternSet, pattern));
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillRule=%d fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillRule, fillColor, patternSet, pattern));
}
private void endArea() {
@@ -928,11 +1076,11 @@ public class GocaDecoder {
polyCounts[i] = areaPolygons.get(i);
}
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d",
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix));
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d fillRule=%d",
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix, areaFillRule));
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet,
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor);
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor, areaFillRule);
inArea = false;
areaPointsX.clear();
areaPointsY.clear();
@@ -971,11 +1119,17 @@ public class GocaDecoder {
}
private void beginImage(int x, int y, int w, int h) {
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
}
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
this.inImage = true;
this.imgX = x;
this.imgY = y;
this.imgWidth = w;
this.imgHeight = h;
this.imgBitDepth = bitDepth;
this.imgCompression = compression;
this.imgBuffer.clear();
}
@@ -991,7 +1145,7 @@ public class GocaDecoder {
int px = plane.mapX(imgX);
int py = plane.mapY(imgY);
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor);
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
inImage = false;
imgBuffer.clear();
}
@@ -1328,7 +1482,7 @@ public class GocaDecoder {
}
String text = new String(chars);
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
curColor, cw, ch, charDir, charAngle);
curColor, cw, ch, charDir, charAngle, charShear);
} else if (charSet != 0 && programSymbolManager != null) {
for (int i = 0; i < textLen; i++) {
int code = data[pos + i] & 0xFF;
@@ -1366,7 +1520,7 @@ public class GocaDecoder {
}
String text = new String(chars);
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
curColor, cw, ch, charDir, charAngle);
curColor, cw, ch, charDir, charAngle, charShear);
}
switch (charDir) {
@@ -1395,11 +1549,15 @@ public class GocaDecoder {
* Draws a transformed character string (matching HODDecoder.drawGCS).
*/
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) {
drawGcs(x, y, text, color, cw, ch, dir, angle, 0.0);
}
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle, double shearAngle) {
if (plane != null && text != null && !text.isEmpty()) {
if (charPrecision == GocaConstants.CP_STROKE) {
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle);
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle, shearAngle);
} else {
plane.drawText(x, y, text, color, cw, ch, dir, angle);
plane.drawText(x, y, text, color, cw, ch, dir, angle, shearAngle);
}
}
}
@@ -1408,12 +1566,16 @@ public class GocaDecoder {
* Draws an EBCDIC byte buffer as a transformed character string.
*/
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) {
drawGcs(ebcdicData, offset, length, color, cw, ch, dir, angle, 0.0);
}
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle, double shearAngle) {
if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return;
char[] chars = new char[length];
for (int i = 0; i < length; i++) {
chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]);
}
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle);
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle, shearAngle);
}
/**
@@ -106,4 +106,11 @@ public class GraphicInputBuilder {
return sf;
}
/**
* Builds the 56-byte Graphic Input Structured Field for pick correlation with aperture.
*/
public static byte[] buildPickCorrelation(int gocaX, int gocaY, int aperture) {
return buildGraphicInput(gocaX, gocaY, 1, true, false, false);
}
}
@@ -148,6 +148,7 @@ public class GraphicsPlane {
this.canvasWidth = w;
this.canvasHeight = h;
this.rgbBuffer = newBuffer;
updateViewingWindowPixels();
}
public synchronized void clear() {
@@ -170,6 +171,59 @@ public class GraphicsPlane {
return rgbBuffer;
}
private int viewingWindowXMin = 0;
private int viewingWindowYMin = 0;
private int viewingWindowXMax = 0;
private int viewingWindowYMax = 0;
private boolean viewingWindowActive = false;
private int clipPixelXMin = 0;
private int clipPixelYMin = 0;
private int clipPixelXMax = 0;
private int clipPixelYMax = 0;
private double fractionalLineWidth = 1.0;
public synchronized void setViewingWindow(int xMin, int yMin, int xMax, int yMax) {
this.viewingWindowXMin = xMin;
this.viewingWindowYMin = yMin;
this.viewingWindowXMax = xMax;
this.viewingWindowYMax = yMax;
this.viewingWindowActive = true;
updateViewingWindowPixels();
}
public synchronized void clearViewingWindow() {
this.viewingWindowActive = false;
}
public synchronized boolean isViewingWindowActive() {
return viewingWindowActive;
}
public synchronized int getViewingWindowXMin() { return viewingWindowXMin; }
public synchronized int getViewingWindowYMin() { return viewingWindowYMin; }
public synchronized int getViewingWindowXMax() { return viewingWindowXMax; }
public synchronized int getViewingWindowYMax() { return viewingWindowYMax; }
public synchronized void setFractionalLineWidth(double flw) {
this.fractionalLineWidth = Math.max(0.1, flw);
}
public synchronized double getFractionalLineWidth() {
return fractionalLineWidth;
}
private void updateViewingWindowPixels() {
if (!viewingWindowActive) return;
int px1 = mapX(viewingWindowXMin);
int px2 = mapX(viewingWindowXMax);
int py1 = mapY(viewingWindowYMin);
int py2 = mapY(viewingWindowYMax);
this.clipPixelXMin = Math.max(0, Math.min(px1, px2));
this.clipPixelXMax = Math.min(canvasWidth - 1, Math.max(px1, px2));
this.clipPixelYMin = Math.max(0, Math.min(py1, py2));
this.clipPixelYMax = Math.min(canvasHeight - 1, Math.max(py1, py2));
}
public int getCanvasWidth() {
return canvasWidth;
}
@@ -313,6 +367,11 @@ public class GraphicsPlane {
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
*/
public synchronized void setPixel(int x, int y, int colorArgb) {
if (viewingWindowActive) {
if (x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax) {
return;
}
}
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
int srcA = (colorArgb >>> 24) & 0xFF;
if (srcA == 0) return;
@@ -447,11 +506,15 @@ public class GraphicsPlane {
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
if (brightness <= 0.0) return;
if (lineWidth == GocaConstants.LW_THICK) {
if (lineWidth == GocaConstants.LW_THICK || fractionalLineWidth >= 1.5) {
int extra = (int) Math.round(Math.max(1, fractionalLineWidth - 0.5));
setPixelCoverage(x, y, colorRgb, 1.0);
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness));
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness));
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7));
for (int dx = -extra; dx <= extra; dx++) {
for (int dy = -extra; dy <= extra; dy++) {
if (dx == 0 && dy == 0) continue;
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
}
}
} else {
// Perceptual gamma correction for crisp contrast on dark backgrounds
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
@@ -641,6 +704,9 @@ public class GraphicsPlane {
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
}
/**
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
*/
/**
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
*/
@@ -648,25 +714,41 @@ public class GraphicsPlane {
int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, patternSet, pattern,
drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, GocaConstants.FILL_RULE_EVEN_ODD);
}
private static class NodeInter implements Comparable<NodeInter> {
final int x;
final int dir;
NodeInter(int x, int dir) {
this.x = x;
this.dir = dir;
}
@Override
public int compareTo(NodeInter o) {
return Integer.compare(this.x, o.x);
}
}
/**
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, int fillRule) {
if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb;
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
// Background Mix (GSBMX / bgMix):
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
// Find polygon vertical bounds across all points
int minY = py[0];
int maxY = py[0];
for (int i = 1; i < numPoints; i++) {
@@ -676,7 +758,7 @@ public class GraphicsPlane {
minY = Math.max(0, minY);
maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>();
List<NodeInter> nodeIntersections = new ArrayList<>();
byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && programSymbolManager != null) {
@@ -691,7 +773,7 @@ public class GraphicsPlane {
}
for (int y = minY; y <= maxY; y++) {
nodeX.clear();
nodeIntersections.clear();
int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) {
@@ -705,7 +787,8 @@ public class GraphicsPlane {
int xj = px[offset + j];
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
nodeX.add(x);
int dir = (yj > yi) ? 1 : -1;
nodeIntersections.add(new NodeInter(x, dir));
}
j = i;
}
@@ -713,35 +796,72 @@ public class GraphicsPlane {
offset += pLen;
}
Collections.sort(nodeX);
Collections.sort(nodeIntersections);
for (int i = 0; i < nodeX.size(); i += 2) {
if (i + 1 >= nodeX.size()) break;
int leftX = Math.max(0, nodeX.get(i));
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
int winding = 0;
for (int i = 0; i < nodeIntersections.size() - 1; i++) {
winding += nodeIntersections.get(i).dir;
if (winding != 0) {
int leftX = Math.max(0, nodeIntersections.get(i).x);
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
}
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
}
}
} else {
for (int i = 0; i < nodeIntersections.size(); i += 2) {
if (i + 1 >= nodeIntersections.size()) break;
int leftX = Math.max(0, nodeIntersections.get(i).x);
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background)
setPixel(x, y, bg);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
}
}
}
@@ -857,19 +977,29 @@ public class GraphicsPlane {
*/
public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) {
drawText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
}
public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
if (text == null || text.isEmpty()) return;
if (textRenderer != null) {
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
hasContent = true;
updateCount++;
} else {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
}
}
public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
}
public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
}
/**
@@ -877,6 +1007,11 @@ public class GraphicsPlane {
*/
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
}
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
if (text == null || text.isEmpty()) return;
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
@@ -884,25 +1019,37 @@ public class GraphicsPlane {
double ch = cellHeight > 0 ? cellHeight : 20.0;
double curX = x;
double curY = y;
if (dir == GocaConstants.CD_TB) {
curY += ch;
} else if (dir == GocaConstants.CD_RL) {
curX -= cw;
double radAngle = Math.toRadians(angle);
double cosA = Math.cos(radAngle);
double sinA = Math.sin(radAngle);
if (angle == 0.0) {
if (dir == GocaConstants.CD_TB) {
curY += ch;
} else if (dir == GocaConstants.CD_RL) {
curX -= cw;
}
}
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
drawVssChar(curX, curY, c, color, cw, ch);
drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle);
switch (dir) {
case GocaConstants.CD_TB: curY += ch; break;
case GocaConstants.CD_RL: curX -= cw; break;
case GocaConstants.CD_BT: curY -= ch; break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
if (angle != 0.0) {
curX += cw * cosA;
curY += cw * sinA;
} else {
switch (dir) {
case GocaConstants.CD_TB: curY += ch; break;
case GocaConstants.CD_RL: curX -= cw; break;
case GocaConstants.CD_BT: curY -= ch; break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
}
}
}
hasContent = true;
@@ -911,10 +1058,19 @@ public class GraphicsPlane {
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
}
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
}
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
}
private void drawVssChar(double x, double y, char c, int color, double cw, double ch, double angle, double shearAngle) {
int code = (int) c;
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
return;
@@ -924,6 +1080,11 @@ public class GraphicsPlane {
return;
}
double radAngle = Math.toRadians(angle);
double cosA = Math.cos(radAngle);
double sinA = Math.sin(radAngle);
double tanShear = Math.tan(Math.toRadians(shearAngle));
int ptr = offset;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
@@ -941,8 +1102,18 @@ public class GraphicsPlane {
for (int p = 0; p < numPoints; p++) {
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
py[p] = y - ((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
double sx = nx - ny * tanShear;
double sy = ny;
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
px[p] = x + rx;
py[p] = y + ry;
ipx[p] = (int) Math.round(px[p]);
ipy[p] = (int) Math.round(py[p]);
}
@@ -970,21 +1141,91 @@ public class GraphicsPlane {
}
/**
* Draws raw image pixel bitmap.
* Draws raw image pixel bitmap with default 1-bit depth and uncompressed format.
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
drawImage(x, y, width, height, imageData, fgColorArgb, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
}
/**
* Draws bitmap image with support for 1-bit, 2-bit, 4-bit, 8-bit depth and RLE decompression.
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
int bitDepth, int compressionMode) {
if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / 8);
if (byteIdx < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) {
setPixel(x + col, y + row, fgColor);
byte[] rawData = imageData;
if (compressionMode == GocaConstants.IMG_RLE) {
rawData = decompressGocaRle(imageData, width, height, bitDepth);
}
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
if (depth == 1) {
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / 8);
if (byteIdx < rawData.length) {
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) {
setPixel(x + col, y + row, fgColor);
}
}
}
}
} else if (depth == 2) {
int pixelsPerByte = 4;
int bytesPerRow = (width + 3) / 4;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / pixelsPerByte);
if (byteIdx < rawData.length) {
int shift = (3 - (col % 4)) * 2;
int val = (rawData[byteIdx] >> shift) & 0x03;
if (val != 0) {
int pixelColor;
switch (val) {
case 1: pixelColor = GocaConstants.GOCA_COLORS[1]; break; // Blue
case 2: pixelColor = GocaConstants.GOCA_COLORS[2]; break; // Red
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
default: pixelColor = fgColor; break;
}
setPixel(x + col, y + row, pixelColor);
}
}
}
}
} else if (depth == 4) {
int pixelsPerByte = 2;
int bytesPerRow = (width + 1) / 2;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + (col / pixelsPerByte);
if (byteIdx < rawData.length) {
int shift = (1 - (col % 2)) * 4;
int val = (rawData[byteIdx] >> shift) & 0x0F;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
}
}
}
}
} else if (depth == 8) {
int bytesPerRow = width;
for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) {
int byteIdx = rowOffset + col;
if (byteIdx < rawData.length) {
int val = rawData[byteIdx] & 0xFF;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
}
}
}
}
@@ -992,4 +1233,35 @@ public class GraphicsPlane {
hasContent = true;
updateCount++;
}
/**
* Decompresses IBM GOCA Run-Length Encoded (RLE) bitmap raster streams.
*/
public static byte[] decompressGocaRle(byte[] rleData, int width, int height, int bitDepth) {
if (rleData == null || rleData.length == 0) return new byte[0];
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
int bytesPerRow = (width * depth + 7) / 8;
int expectedTotalBytes = bytesPerRow * height;
byte[] out = new byte[expectedTotalBytes];
int outIdx = 0;
int inIdx = 0;
while (inIdx < rleData.length && outIdx < expectedTotalBytes) {
int count = rleData[inIdx++] & 0xFF;
if (count == 0) {
if (inIdx < rleData.length) {
int litLen = rleData[inIdx++] & 0xFF;
for (int k = 0; k < litLen && inIdx < rleData.length && outIdx < expectedTotalBytes; k++) {
out[outIdx++] = rleData[inIdx++];
}
}
} else if (inIdx < rleData.length) {
byte val = rleData[inIdx++];
for (int k = 0; k < count && outIdx < expectedTotalBytes; k++) {
out[outIdx++] = val;
}
}
}
return out;
}
}
@@ -212,15 +212,14 @@ public class ProgramSymbolManager {
int remaining = data.length - offset;
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
int bytesPerSymbol;
if (loadFormat == 1) {
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
} else {
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
}
int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (sliceBytes <= 0) sliceBytes = 18;
if (bytesPerSymbol <= 0) {
bytesPerSymbol = 18;
int bytesPerSymbol;
if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
bytesPerSymbol = sliceBytes * 3;
} else {
bytesPerSymbol = sliceBytes;
}
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
@@ -230,10 +229,23 @@ public class ProgramSymbolManager {
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
}
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
// 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
}
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
}
}
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
@@ -5,6 +5,8 @@ import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.telnet.TelnetFSM;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
@@ -22,6 +24,7 @@ public class InputProcessor {
private final ScreenBuffer screen;
private final EbcdicTranslator translator;
private final TelnetFSM fsm;
private ECLOIA oia;
private int lastAid = AID_NO;
private boolean keyboardLocked;
private boolean insertMode;
@@ -32,6 +35,14 @@ public class InputProcessor {
this.fsm = fsm;
}
public void setOIA(ECLOIA oia) {
this.oia = oia;
}
public ECLOIA getOIA() {
return oia;
}
public TelnetFSM getFsm() {
return fsm;
}
@@ -102,7 +113,9 @@ public class InputProcessor {
* Enter a character at the current cursor position.
*/
public void typeCharacter(char ch) {
if (keyboardLocked) return;
if (keyboardLocked) {
return;
}
if (isNvtMode()) {
try {
@@ -118,18 +131,37 @@ public class InputProcessor {
int baddr = screen.getCursorAddress();
baddr = ((baddr % size) + size) % size;
// Check if cursor is at a field attribute or in a protected field
ExtendedAttribute ea = screen.getCell(baddr);
if (ea.isFieldAttribute()) {
// Move to next position
baddr = (baddr + 1) % size;
ea = screen.getCell(baddr);
}
if (screen.isFormatted()) {
// Check if cursor is at a field attribute
ExtendedAttribute ea = screen.getCell(baddr);
if (ea.isFieldAttribute()) {
// Move to next position
baddr = (baddr + 1) % size;
ea = screen.getCell(baddr);
}
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) {
// Protected field can't type here
return;
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) {
// Protected field cannot type here
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_PROTECTED_FIELD);
}
setKeyboardLocked(true);
return;
}
// Numeric-only field check: digits 0-9, minus (-), period (.), space ( ), DUP, FM
if (faIsNumeric(faVal & 0xFF)) {
boolean isValidNumeric = (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == ' '
|| ch == '*' || ch == ';' || ch == (char) FCORDER_DUP || ch == (char) FCORDER_FM;
if (!isValidNumeric) {
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_NUMERIC_ONLY);
}
setKeyboardLocked(true);
return;
}
}
}
// Translate character to EBCDIC
@@ -138,7 +170,6 @@ public class InputProcessor {
if (insertMode) {
// Insert mode: shift characters right from cursor to end of field
// Find end of field
int endAddr = baddr;
int count = 0;
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
@@ -146,9 +177,13 @@ public class InputProcessor {
count++;
if (endAddr == baddr) break; // wrapped around (unformatted)
}
// Check if last position is non-null (field overflow)
if (screen.getCell(endAddr).ec != 0) {
// Field overflow can't insert
// Check if last position is non-null/non-blank (field overflow)
ExtendedAttribute eaEnd = screen.getCell(endAddr);
if (eaEnd.ec != 0 && (eaEnd.ec & 0xFF) != 0x40 && eaEnd.ucs4 != 0 && eaEnd.ucs4 != ' ') {
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
}
setKeyboardLocked(true);
return;
}
// Shift right from endAddr-1 down to baddr
@@ -164,48 +199,291 @@ public class InputProcessor {
}
// Write character preserve existing fg/bg/gr/cs attributes
// so the character inherits the field's color scheme
ea = screen.getCell(baddr);
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) ebc;
ea.ucs4 = ch;
// Set MDT on field attribute
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
ExtendedAttribute faEa = screen.getCell(faAddr);
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
if (screen.isFormatted()) {
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
ExtendedAttribute faEa = screen.getCell(faAddr);
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
}
}
// Advance cursor
int startAdvance = baddr;
baddr = (baddr + 1) % size;
int advCount = 0;
// Skip over field attributes safely
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance && advCount < size) {
baddr = (baddr + 1) % size;
advCount++;
// Advance cursor and handle auto-skip
if (screen.isFormatted()) {
int nextAddr = screen.incrementAddress(baddr);
if (screen.getCell(nextAddr).isFieldAttribute()) {
byte nextFa = screen.getCell(nextAddr).fa;
if (faIsSkip(nextFa & 0xFF)) {
// Auto-skip field (Protected + Numeric): jump to next unprotected field
int skipTarget = screen.findNextUnprotected(nextAddr);
screen.setCursorAddress(skipTarget);
} else {
int adv = nextAddr;
int advCount = 0;
while (screen.getCell(adv).isFieldAttribute() && adv != baddr && advCount < size) {
adv = screen.incrementAddress(adv);
advCount++;
}
screen.setCursorAddress(adv);
}
} else {
screen.setCursorAddress(nextAddr);
}
} else {
screen.setCursorAddress((baddr + 1) % size);
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
/**
* Process character input at current cursor position (HoD source compatibility).
*/
public void processChar(char ch) {
typeCharacter(ch);
}
/**
* Process character input with explicit position and insert mode flag.
*/
public void processChar(char ch, int pos, boolean insert) {
if (pos >= 0) {
screen.setCursorAddress(pos);
}
boolean oldInsert = insertMode;
insertMode = insert;
try {
typeCharacter(ch);
} finally {
insertMode = oldInsert;
}
}
/**
* Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields).
*/
public byte[] buildReadModifiedInboundData() {
return buildReadModifiedInboundData(lastAid != 0 ? lastAid : AID_ENTER, false);
}
/**
* Build inbound 3270 Read Modified data stream with specified AID code.
*/
public byte[] buildReadModifiedInboundData(int aidCode) {
return buildReadModifiedInboundData(aidCode, false);
}
/**
* Build inbound 3270 Read Modified data stream with specified AID code and 'all' fields flag.
*/
public byte[] buildReadModifiedInboundData(int aidCode, boolean all) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3 || aidCode == AID_CLEAR) {
return out.toByteArray();
}
if (aidCode == AID_SELECT) {
// 3270 Selector Pen / Light Pen Immediate Selection (AID 0x7E):
// Per 3270 DS spec: Send AID (0x7E) + Cursor Address (2 bytes) + SBA (0x11) + Designator Addr for each modified field.
// NO character data is transmitted for AID_SELECT.
if (screen.isFormatted()) {
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute cell = screen.getCell(i);
if (cell.isFieldAttribute() && (all || faIsModified(cell.fa & 0xFF))) {
int designatorAddr = (i + 1) % size;
out.write(ORDER_SBA);
byte[] addr = encodeAddress(designatorAddr, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
}
}
}
return out.toByteArray();
}
byte replyMode = screen.getReplyMode();
boolean extendedMode = (replyMode == SF_SRM_XFIELD || replyMode == SF_SRM_CHAR);
if (screen.isFormatted()) {
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute cell = screen.getCell(i);
if (cell.isFieldAttribute() && (all || faIsModified(cell.fa & 0xFF))) {
int fieldStart = (i + 1) % size;
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
int pos = fieldStart;
while (!screen.getCell(pos).isFieldAttribute()) {
ExtendedAttribute charCell = screen.getCell(pos);
int b = charCell.ec & 0xFF;
if (b != 0x00) {
if (extendedMode) {
if (charCell.fg != curFg) {
out.write(ORDER_SA); out.write(XA_FOREGROUND); out.write(charCell.fg & 0xFF);
curFg = charCell.fg;
}
if (charCell.bg != curBg) {
out.write(ORDER_SA); out.write(XA_BACKGROUND); out.write(charCell.bg & 0xFF);
curBg = charCell.bg;
}
if (charCell.gr != curGr) {
out.write(ORDER_SA); out.write(XA_HIGHLIGHTING);
int xah = XAH_NORMAL;
if ((charCell.gr & GR_BLINK) != 0) xah = XAH_BLINK;
else if ((charCell.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
else if ((charCell.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
else if ((charCell.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
out.write(xah);
curGr = charCell.gr;
}
if (charCell.cs != curCs) {
out.write(ORDER_SA); out.write(XA_CHARSET); out.write(charCell.cs & 0xFF);
curCs = charCell.cs;
}
}
out.write(b);
}
pos = (pos + 1) % size;
if (pos == fieldStart) break;
}
}
}
} else {
// Unformatted screen in 3270 mode: send all non-null characters
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
int b = screen.getCell(i).ec & 0xFF;
if (b != 0x00) {
out.write(b);
}
}
}
return out.toByteArray();
}
/**
* Build inbound 3270 Read Buffer data stream.
*/
public byte[] buildReadBufferInboundData() {
return buildReadBufferInboundData(lastAid != 0 ? lastAid : AID_NO);
}
/**
* Build inbound 3270 Read Buffer data stream with specified AID code.
*/
public byte[] buildReadBufferInboundData(int aidCode) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
int size = screen.getRows() * screen.getCols();
byte mode = screen.getReplyMode();
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
int count = 1;
if (ea.fg != 0) count++;
if (ea.bg != 0) count++;
if (ea.gr != 0) count++;
if (ea.cs != 0) count++;
if (ea.ol != 0) count++;
out.write(ORDER_SFE);
out.write(count);
out.write(XA_3270);
out.write(ea.fa & 0xFF);
if (ea.fg != 0) { out.write(XA_FOREGROUND); out.write(ea.fg & 0xFF); }
if (ea.bg != 0) { out.write(XA_BACKGROUND); out.write(ea.bg & 0xFF); }
if (ea.gr != 0) {
out.write(XA_HIGHLIGHTING);
int xah = XAH_NORMAL;
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
out.write(xah);
}
if (ea.cs != 0) { out.write(XA_CHARSET); out.write(ea.cs & 0xFF); }
if (ea.ol != 0) { out.write(XA_OUTLINING); out.write(ea.ol & 0xFF); }
} else {
if (mode == SF_SRM_CHAR) {
if (ea.fg != curFg) {
out.write(ORDER_SA); out.write(XA_FOREGROUND); out.write(ea.fg & 0xFF);
curFg = ea.fg;
}
if (ea.bg != curBg) {
out.write(ORDER_SA); out.write(XA_BACKGROUND); out.write(ea.bg & 0xFF);
curBg = ea.bg;
}
if (ea.gr != curGr) {
out.write(ORDER_SA); out.write(XA_HIGHLIGHTING);
int xah = XAH_NORMAL;
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
out.write(xah);
curGr = ea.gr;
}
if (ea.cs != curCs) {
out.write(ORDER_SA); out.write(XA_CHARSET); out.write(ea.cs & 0xFF);
curCs = ea.cs;
}
}
out.write(ea.ec & 0xFF);
}
}
} else {
// Standard Field mode
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
out.write(ORDER_SF);
out.write(ea.fa & 0xFF);
} else {
out.write(ea.ec & 0xFF);
}
}
}
return out.toByteArray();
}
/**
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
*/
public void sendAid(int aidCode) {
System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
log.fine("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
if (aidCode == AID_SYSREQ) {
sysReq();
return;
if (fsm != null && fsm.isTn3270eNegotiated()) {
fsm.handleSysReq();
return;
}
}
if (keyboardLocked && aidCode != AID_CLEAR) {
System.err.println("Keyboard locked, dropping AID");
return;
}
lastAid = aidCode;
setKeyboardLocked(true);
if (aidCode == AID_CLEAR) {
screen.clear();
@@ -213,22 +491,27 @@ public class InputProcessor {
if (graphicsPlane != null) {
graphicsPlane.clear();
}
// Send just the AID
byte[] data = new byte[] { (byte) aidCode };
sendAidResponse(data);
return;
}
if (keyboardLocked || (oia != null && oia.getInputInhibited() != ECLConstants.INHIBIT_NOT_INHIBITED)) {
log.fine("Keyboard locked or OIA inhibited, dropping AID 0x" + Integer.toHexString(aidCode));
return;
}
lastAid = aidCode;
setKeyboardLocked(true);
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
// SSCP-LU Mode (e.g. VM line mode / CP console before conmode 3270):
// Per RFC 2355: Send raw EBCDIC line data without AID or 3270 cursor address.
// SSCP-LU Mode: send raw EBCDIC line data without AID or 3270 cursor address
int cols = screen.getCols();
int curAddr = screen.getCursorAddress();
int row = curAddr / cols;
int rowStart = row * cols;
int rowEnd = rowStart + cols;
// Find last non-null, non-blank character in the current row
int lastChar = rowStart - 1;
for (int i = rowEnd - 1; i >= rowStart; i--) {
int ec = screen.getCell(i).ec & 0xFF;
@@ -246,7 +529,6 @@ public class InputProcessor {
fsm.sendSscpLuData(sscpData.toByteArray());
// Advance cursor to beginning of next row
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
screen.setCursorAddress(nextRowAddr);
screen.markAllChanged();
@@ -255,111 +537,14 @@ public class InputProcessor {
return;
}
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
// PA keys: send AID + optional PID + cursor address only (no modified data)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
sendAidResponse(out.toByteArray());
return;
}
if (aidCode == AID_SELECT) {
// 3270 Selector Pen / Light Pen Immediate Selection (AID 0x7E):
// Per IBM 3270 Data Stream Architecture (GA23-0059) and IBM Host On-Demand (DS3270.sendAid lines 727-1019):
// The inbound data stream consists of:
// 1. AID byte (0x7E)
// 2. Cursor address (2 bytes)
// 3. For each field with MDT=1:
// - SBA order (0x11)
// - Designator character address (faAddr + 1)
// CRITICAL: NO FIELD CHARACTER DATA IS TRANSMITTED FOR AID_SELECT!
// Sending character data in an AID_SELECT stream violates 3270 protocol and causes the host to reject the selection.
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(AID_SELECT);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
if (screen.isFormatted()) {
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
int designatorAddr = (i + 1) % size;
out.write(ORDER_SBA);
byte[] addr = encodeAddress(designatorAddr, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
}
}
}
sendAidResponse(out.toByteArray());
return;
}
// Enter, PF keys: send AID + optional PID + cursor address + modified field data
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
if (screen.isFormatted()) {
// Send modified fields with SBA
// Per 3270 Data Stream Architecture: suppress NULLs (0x00) from field data
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
int fieldStart = (i + 1) % size;
// Always send SBA and address of first character in field
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
// Send all non-null characters in field (suppressing 0x00)
int pos = fieldStart;
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 0xFF;
if (b != 0x00) {
out.write(b);
}
pos = (pos + 1) % size;
if (pos == fieldStart) break;
}
}
}
} else {
// Unformatted screen in 3270 mode:
// Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line
// or we can just send everything up to the last non-null on the screen.
// IBM spec: "all alphanumeric characters... Nulls are suppressed."
// Actually, the simplest is to send everything, but suppress nulls.
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
int b = screen.getCell(i).ec & 0xFF;
if (b != 0x00) {
out.write(b);
}
}
}
sendAidResponse(out.toByteArray());
byte[] payload = buildReadModifiedInboundData(aidCode, false);
sendAidResponse(payload);
}
protected void sendAidResponse(byte[] data) {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
fsm.sendSscpLuData(data);
} else if (fsm != null) {
} else if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
fsm.send3270Data(data);
}
}
@@ -758,29 +943,244 @@ public class InputProcessor {
screen.updateDisplaySnapshot();
}
/** Attention key (sends Telnet IP). */
public void attn() {
/** Attention key (sends Telnet Interrupt Process + IAC EOR break signal). */
public void processAttn() {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
byte[] ip = new byte[] { (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP };
fsm.sendRecord(ip);
byte[] ipEor = new byte[] {
(byte) TelnetConstants.IAC, (byte) TelnetConstants.IP,
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
};
fsm.sendRecord(ipEor);
}
}
/** SysReq key. */
public void sysReq() {
public void attn() {
processAttn();
}
/** SysReq key (generates TN3270E SYSREQ signal or 3270 Test Request AID). */
public void processSysReq() {
if (fsm != null && fsm.isTn3270eNegotiated()) {
fsm.handleSysReq();
} else {
byte[] payload = buildReadModifiedInboundData(AID_SYSREQ, false);
sendAidResponse(payload);
reset();
}
}
/** Reset (unlock keyboard, cancel insert mode). */
public void sysReq() {
processSysReq();
}
/** Reset (unlock keyboard, cancel insert mode, clear OIA error states). */
public void reset() {
setKeyboardLocked(false);
insertMode = false;
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
}
}
public void processReset() {
reset();
}
/**
* Move cursor backwards to the beginning of the previous/current word within the unprotected field.
*/
public void processWordLeft() {
if (keyboardLocked || screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int cur = screen.getCursorAddress();
if (screen.isFormatted()) {
int faAddr = screen.findFieldAttribute(cur);
if (faAddr < 0) return;
if (faIsProtected(screen.getCell(faAddr).fa & 0xFF)) return;
int fieldStart = screen.incrementAddress(faAddr);
int pos = cur;
if (pos == fieldStart) {
return;
}
// Step 1: Scan backwards to skip any whitespace/nulls immediately preceding cursor
pos = screen.decrementAddress(pos);
while (pos != faAddr && isWhitespaceOrNull(pos)) {
pos = screen.decrementAddress(pos);
}
if (pos == faAddr) {
screen.setCursorAddress(fieldStart);
screen.updateDisplaySnapshot();
return;
}
// Step 2: Scan backwards across non-whitespace characters until whitespace or field start
while (pos != faAddr && !isWhitespaceOrNull(pos)) {
pos = screen.decrementAddress(pos);
}
int wordStart = screen.incrementAddress(pos);
screen.setCursorAddress(wordStart);
} else {
int pos = cur;
if (pos > 0) pos--;
while (pos > 0 && isWhitespaceOrNull(pos)) {
pos--;
}
while (pos > 0 && !isWhitespaceOrNull(pos - 1)) {
pos--;
}
screen.setCursorAddress(pos);
}
screen.updateDisplaySnapshot();
}
/**
* Move cursor forwards to the beginning of the next word within the unprotected field.
*/
public void processWordRight() {
if (keyboardLocked || screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int cur = screen.getCursorAddress();
if (screen.isFormatted()) {
int faAddr = screen.findFieldAttribute(cur);
if (faAddr < 0) return;
if (faIsProtected(screen.getCell(faAddr).fa & 0xFF)) return;
int pos = cur;
// Step 1: Scan forwards across current word (non-whitespace)
while (!screen.getCell(pos).isFieldAttribute() && !isWhitespaceOrNull(pos)) {
pos = screen.incrementAddress(pos);
}
// Step 2: Scan forwards across whitespace
while (!screen.getCell(pos).isFieldAttribute() && isWhitespaceOrNull(pos)) {
pos = screen.incrementAddress(pos);
}
if (screen.getCell(pos).isFieldAttribute()) {
pos = screen.findNextUnprotected(pos);
}
screen.setCursorAddress(pos);
} else {
int pos = cur;
while (pos < size && !isWhitespaceOrNull(pos)) {
pos++;
}
while (pos < size && isWhitespaceOrNull(pos)) {
pos++;
}
if (pos >= size) pos = size - 1;
screen.setCursorAddress(pos);
}
screen.updateDisplaySnapshot();
}
/**
* Move cursor to the position immediately following the last non-blank character in the current field.
*/
public void processFieldEnd() {
if (keyboardLocked || screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int cur = screen.getCursorAddress();
if (screen.isFormatted()) {
int faAddr = screen.findFieldAttribute(cur);
if (faAddr < 0) return;
if (faIsProtected(screen.getCell(faAddr).fa & 0xFF)) return;
int fieldStart = screen.incrementAddress(faAddr);
int endAddr = fieldStart;
int count = 0;
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
endAddr = screen.incrementAddress(endAddr);
count++;
if (endAddr == fieldStart) break;
}
int check = endAddr;
int lastNonBlank = -1;
int scanCount = 0;
while (scanCount <= count) {
ExtendedAttribute ea = screen.getCell(check);
if (ea.ec != 0 && (ea.ec & 0xFF) != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') {
lastNonBlank = check;
break;
}
if (check == fieldStart) break;
check = screen.decrementAddress(check);
scanCount++;
}
if (lastNonBlank < 0) {
screen.setCursorAddress(fieldStart);
} else if (lastNonBlank == endAddr) {
screen.setCursorAddress(endAddr);
} else {
screen.setCursorAddress(screen.incrementAddress(lastNonBlank));
}
} else {
int cols = screen.getCols();
int row = cur / cols;
int rowStart = row * cols;
int rowEnd = rowStart + cols - 1;
int lastNonBlank = -1;
for (int i = rowEnd; i >= rowStart; i--) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.ec != 0 && (ea.ec & 0xFF) != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') {
lastNonBlank = i;
break;
}
}
if (lastNonBlank < 0) {
screen.setCursorAddress(rowStart);
} else if (lastNonBlank == rowEnd) {
screen.setCursorAddress(rowEnd);
} else {
screen.setCursorAddress(lastNonBlank + 1);
}
}
screen.updateDisplaySnapshot();
}
/** Emulate light pen / cursor selection on the current cursor position. */
public boolean processLightPen() {
if (screen == null) return false;
return lightPenSelect(screen.getCursorAddress());
}
/** Emulate light pen / cursor selection on the specified screen address. */
public boolean processLightPen(int address) {
return lightPenSelect(address);
}
// ========== IBM Host On-Demand 1:1 Compatible Method Overloads ==========
public void processEnter() { sendAid(AID_ENTER); }
public void processPF(int pfNum) { if (pfNum >= 1 && pfNum <= 24) sendAid(AID_PF1 + (pfNum - 1)); }
public void processPA(int paNum) { if (paNum >= 1 && paNum <= 3) sendAid(AID_PA1 + (paNum - 1)); }
public void processClear() { sendAid(AID_CLEAR); }
public void processCursorUp() { cursorUp(); }
public void processCursorDown() { cursorDown(); }
public void processCursorLeft() { cursorLeft(); }
public void processCursorRight() { cursorRight(); }
public void processTab() { tab(); }
public void processBackTab() { backTab(); }
public void processHome() { cursorHome(); }
public void processNewline() { newline(); }
public void processDelete() { deleteChar(); }
public void processBackspace() { backspace(); }
public void processEraseEOF() { eraseEof(); }
public void processEraseInput() { eraseInput(); }
public void processDup() { dup(); }
public void processFieldMark() { fieldMark(); }
public void processToggleInsert() { setInsertMode(!isInsertMode()); }
public boolean processCurSel() { return cursorSelect(); }
public boolean processCursorSelect() { return cursorSelect(); }
// ========== File Transfer Support ==========
/**
@@ -1080,9 +1480,27 @@ public class InputProcessor {
case "wordbacktab":
processWordTab(false);
break;
case "wordleft":
processWordLeft();
break;
case "wordright":
processWordRight();
break;
case "fieldend":
case "end":
processFieldEnd();
break;
case "deleteword":
processDeleteWord();
break;
case "cursel":
case "cursorselect":
case "select":
cursorSelect();
break;
case "lightpen":
processLightPen();
break;
default:
if (token.startsWith("pf")) {
try {
@@ -1122,43 +1540,11 @@ public class InputProcessor {
}
/**
* Jump cursor to next or previous word boundary.
* Jump cursor to next or previous word boundary or tab stop.
*/
public void processWordTab(boolean forward) {
if (keyboardLocked || screen == null) return;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int cur = screen.getCursorAddress();
if (forward) {
// Find next whitespace then next non-whitespace
int pos = screen.incrementAddress(cur);
int count = 0;
while (count < size && !isWhitespaceOrNull(pos)) {
pos = screen.incrementAddress(pos);
count++;
}
while (count < size && isWhitespaceOrNull(pos)) {
pos = screen.incrementAddress(pos);
count++;
}
screen.setCursorAddress(pos);
} else {
// Find previous non-whitespace after whitespace
int pos = screen.decrementAddress(cur);
int count = 0;
while (count < size && isWhitespaceOrNull(pos)) {
pos = screen.decrementAddress(pos);
count++;
}
while (count < size && !isWhitespaceOrNull(screen.decrementAddress(pos))) {
pos = screen.decrementAddress(pos);
count++;
}
screen.setCursorAddress(pos);
}
screen.markAllChanged();
screen.updateDisplaySnapshot();
screen.processWordTab(forward);
}
/**
@@ -1166,23 +1552,7 @@ public class InputProcessor {
*/
public void processDeleteWord() {
if (keyboardLocked || screen == null) return;
int cur = screen.getCursorAddress();
if (screen.isFormatted()) {
byte fa = screen.getFieldAttributeAt(cur);
if (faIsProtected(fa & 0xFF)) return;
}
int count = 0;
int size = screen.getRows() * screen.getCols();
while (count < size && !isWhitespaceOrNull(screen.getCursorAddress())) {
deleteChar();
count++;
}
// Also delete trailing spaces
while (count < size && isWhitespaceOrNull(screen.getCursorAddress()) && screen.getCell(screen.getCursorAddress()).ec != 0) {
deleteChar();
count++;
}
screen.processDeleteWord();
}
private boolean isWhitespaceOrNull(int pos) {
@@ -204,44 +204,52 @@ public final class DS3270Constants {
public static final int SF_DESTROY_PART = 0x0d;
public static final int SF_ACTIVATE_PART = 0x0e;
public static final int SF_MODIFY_PART = 0x0f;
public static final int SF_SET_WINDOW = 0x0f; // Set Window (GOCA/Modify Partition)
public static final int SF_3270_GRAPHICS = 0x20; // 3270 Graphics / Object Control
public static final int SF_OBJECT_CONTROL = 0x20; // Object Control
public static final int SF_DOCUMENT_DATA = 0x24; // Document Data (embedded SCS / GOCA)
public static final int SF_DOC_DATA = 0x24; // Alias for Document Data
public static final int SF_OUTBOUND_DS = 0x40;
public static final int SF_TRANSFER_DATA = 0xd0;
// ========== Query Reply codes ==========
public static final int QR_SUMMARY = 0x80; // Summary
public static final int QR_USABLE_AREA = 0x81; // Usable Area
public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
public static final int QR_TEXT_PART = 0x83; // Text Partitions
public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
public static final int QR_CHARSETS = 0x85; // Character Sets
public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
public static final int QR_OUTLINING = 0x8c; // Field Outlining
public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
public static final int QR_DDM = 0x95; // Distributed Data Management
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
public static final int QR_FILE = 0x9f; // File Transfer
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA)
public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev)
public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported)
public static final int QR_SUMMARY = 0x80; // Summary
public static final int QR_USABLE_AREA = 0x81; // Usable Area
public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
public static final int QR_TEXT_PART = 0x83; // Text Partitions
public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
public static final int QR_CHARSETS = 0x85; // Character Sets
public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
public static final int QR_OUTLINING = 0x8c; // Field Outlining
public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
public static final int QR_DDM = 0x95; // Distributed Data Management
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
public static final int QR_FILE = 0x9f; // File Transfer
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA)
public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev)
public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE
public static final int QR_AUX_DEVICE = 0xb2; // Aux Device alias
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
public static final int QR_OEM_FORMAT = 0xb3; // OEM Format alias
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
public static final int QR_GRAPHIC_COLOR = 0xb4; // Graphic Color alias
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported)
// ========== Screen model sizes ==========
public static final int MODEL_2_ROWS = 24;
@@ -18,6 +18,20 @@ public class ExtendedAttribute {
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */
public byte bg;
// Character set constants
public static final byte CS_BASE = 0;
public static final byte CS_APL = 1;
public static final byte CS_LINEDRAW = 2;
public static final byte CS_DBCS = 3;
public static final byte CS_GE = 0x04;
// DBCS state constants
public static final byte DB_NONE = 0;
public static final byte DB_LEFT = 1; // Left / first half of double-byte char
public static final byte DB_RIGHT = 2; // Right / second half of double-byte char
public static final byte DB_SI = 3; // Shift-In control char
public static final byte DB_SO = 4; // Shift-Out control char
/**
* Graphics rendition bits.
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.screen;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLPS;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/**
@@ -39,6 +42,14 @@ public class ScreenBuffer {
private byte defaultGr = 0x00;
private byte defaultCs = 0x00;
private byte defaultIc = 0x00;
// Entry Assist / DOC mode state
private boolean docMode = false;
private boolean wordWrap = false;
private int docStartCol = 0;
private int docEndCol = -1;
private int[] tabStops = null;
private final EbcdicTranslator translator;
private final Object renderLock = new Object();
@@ -216,6 +227,10 @@ public class ScreenBuffer {
this.explicitPartitionActive = (pid != 0);
}
public synchronized PartitionInfo getPartition(int pid) {
return partitions.get(pid);
}
public synchronized void eraseReset(boolean alt) {
partitions.clear();
this.activePartition = 0;
@@ -269,7 +284,7 @@ public class ScreenBuffer {
public void setFieldAttribute(int pos, byte fa) {
ExtendedAttribute ea = buffer[pos];
ea.clear();
ea.fa = fa;
ea.fa = (fa != 0) ? fa : (byte) FA_PRINTABLE;
if (!formatted) {
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
}
@@ -471,39 +486,10 @@ public class ScreenBuffer {
}
private char getAplGraphic(int ec) {
switch (ec) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
// Special math and APL symbols (matching x3270 cg.c / apl.c)
case 0x8C: return '\u2264'; // Less-than or equal '≤'
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
case 0xBE: return '\u2260'; // Not equal '≠'
case 0xAD: return '['; // Left bracket
case 0xBD: return ']'; // Right bracket
case 0x8D: return '{'; // Left brace
case 0x9D: return '}'; // Right brace
case 0xB0: return '\u00B0'; // Degree '°'
case 0xB1: return '\u00B1'; // Plus-minus '±'
case 0xB2: return '\u00B2'; // Superscript 2 '²'
case 0xB3: return '\u00B3'; // Superscript 3 '³'
case 0xAF: return '\u00AF'; // Overbar '¯'
case 0xBA: return '\u03A9'; // Omega 'Ω'
case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬'
default: return translator.ebcdicToUnicode(ec);
if (translator != null) {
return translator.mapAPL(ec);
}
return (char) (ec & 0xFF);
}
/**
@@ -567,4 +553,511 @@ public class ScreenBuffer {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
return buffer[baddr].fa;
}
// ========== Phase 3: Field Management & Navigation ==========
/**
* Construct an ECLFieldList representation of the presentation space.
*/
public synchronized ECLFieldList buildFieldList() {
return new ECLFieldList(new ECLPS(this, null, translator), this);
}
/**
* Find the ECLField at the specified row and column.
*/
public ECLField findFieldAt(int row, int col) {
return buildFieldList().findFieldAt(row, col);
}
/**
* Find the ECLField containing the specified buffer position.
*/
public ECLField findFieldAt(int pos) {
return buildFieldList().findFieldAt(pos);
}
/**
* Find the ECLField containing the specified buffer position.
*/
public ECLField findField(int pos) {
return buildFieldList().findField(pos);
}
/**
* Find the field preceding the field at the given position.
*/
public ECLField findPrevField(int pos) {
return buildFieldList().findPrevField(pos);
}
/**
* Find the field succeeding the field at the given position.
*/
public ECLField findNextField(int pos) {
return buildFieldList().findNextField(pos);
}
/**
* Get the first field in the presentation space.
*/
public ECLField getFirstField() {
return buildFieldList().getFirstField();
}
// ========== Presentation Space Accessors & Convenience Methods ==========
public int getSize() {
return rows * cols;
}
public synchronized char getChar(int row, int col) {
int addr = rowColToAddress(row, col);
if (addr < 0 || addr >= rows * cols) return ' ';
ExtendedAttribute ea = buffer[addr];
if (ea.isFieldAttribute()) return ' ';
if (ea.ucs4 != 0) return (char) ea.ucs4;
if (ea.ec != 0) return translator.ebcdicToUnicode(ea.ec & 0xFF);
return ' ';
}
public synchronized void setChar(int row, int col, char c) {
int addr = rowColToAddress(row, col);
if (addr < 0 || addr >= rows * cols) return;
int ebc = translator.unicodeToEbcdic(c);
buffer[addr].ec = (byte) (ebc >= 0 ? ebc : 0);
buffer[addr].ucs4 = c;
screenChanged = true;
}
public synchronized void writeChar(int pos, byte ebc) {
if (pos < 0 || pos >= rows * cols) return;
buffer[pos].ec = ebc;
buffer[pos].ucs4 = translator.ebcdicToUnicode(ebc & 0xFF);
screenChanged = true;
}
public byte getAttr(int row, int col) {
return getFieldAttributeAt(rowColToAddress(row, col));
}
public ExtendedAttribute getExtAttr(int row, int col) {
return getCell(rowColToAddress(row, col));
}
public synchronized void setExtAttr(int row, int col, ExtendedAttribute ea) {
int addr = rowColToAddress(row, col);
if (addr >= 0 && addr < rows * cols && ea != null) {
buffer[addr].copyFrom(ea);
screenChanged = true;
}
}
public synchronized String getText() {
int size = rows * cols;
char[] buf = new char[size];
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (ea.isFieldAttribute()) {
buf[i] = ' ';
} else if (ea.ucs4 != 0) {
buf[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
} else {
buf[i] = ' ';
}
}
return new String(buf);
}
public synchronized String getString(int pos, int len) {
if (len <= 0) return "";
int size = rows * cols;
if (size <= 0) return "";
char[] buf = new char[len];
for (int i = 0; i < len; i++) {
int addr = (pos + i) % size;
ExtendedAttribute ea = buffer[addr];
if (ea.isFieldAttribute()) {
buf[i] = ' ';
} else if (ea.ucs4 != 0) {
buf[i] = (char) ea.ucs4;
} else if (ea.ec != 0) {
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
} else {
buf[i] = ' ';
}
}
return new String(buf);
}
public synchronized void setText(String text) {
if (text == null) return;
int size = rows * cols;
int len = Math.min(text.length(), size);
for (int i = 0; i < len; i++) {
char ch = text.charAt(i);
int ebc = translator.unicodeToEbcdic(ch);
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0);
buffer[i].ucs4 = ch;
}
screenChanged = true;
updateDisplaySnapshot();
}
public int searchString(String target) {
if (target == null || target.isEmpty()) return -1;
String full = getText();
return full.indexOf(target);
}
public boolean isModified() {
int size = rows * cols;
for (int i = 0; i < size; i++) {
if (buffer[i].isFieldAttribute() && faIsModified(buffer[i].fa & 0xFF)) {
return true;
}
}
return false;
}
public boolean isModified(int pos) {
return faIsModified(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isProtected(int pos) {
return faIsProtected(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isNumeric(int pos) {
return faIsNumeric(getFieldAttributeAt(pos) & 0xFF);
}
public boolean isDisplay(int pos) {
return !faIsZero(getFieldAttributeAt(pos) & 0xFF);
}
// ========== DBCS Character Insertion & Deletion ==========
/**
* Insert a character at the specified buffer address with field boundary and DBCS preservation.
*/
public synchronized boolean insertChar(int pos, char ch) {
int size = rows * cols;
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
if (formatted) {
byte faVal = getFieldAttributeAt(pos);
if (faIsProtected(faVal & 0xFF)) return false;
}
boolean isDbcsChar = translator != null && translator.isDBCS() && translator.unicodeToDbcs(ch) >= 0;
int shiftAmount = isDbcsChar ? 2 : 1;
// Find end of field
int endAddr = pos;
int count = 0;
while (!getCell(incrementAddress(endAddr)).isFieldAttribute() && count < size) {
endAddr = incrementAddress(endAddr);
count++;
if (endAddr == pos) break;
}
// Check overflow
for (int s = 0; s < shiftAmount; s++) {
int checkAddr = endAddr;
for (int k = 0; k < s; k++) checkAddr = decrementAddress(checkAddr);
ExtendedAttribute eaEnd = getCell(checkAddr);
if (eaEnd.ec != 0 && eaEnd.ec != 0x40 && eaEnd.ucs4 != 0 && eaEnd.ucs4 != ' ') {
return false;
}
}
// Shift characters right
for (int s = 0; s < shiftAmount; s++) {
int dst = endAddr;
int shiftCount = 0;
while (dst != pos && shiftCount < size) {
int src = decrementAddress(dst);
getCell(dst).copyFrom(getCell(src));
dst = src;
shiftCount++;
}
getCell(pos).clear();
}
if (isDbcsChar) {
int dbcs = translator.unicodeToDbcs(ch);
int b1 = (dbcs >> 8) & 0xFF;
int b2 = dbcs & 0xFF;
int nextPos = incrementAddress(pos);
ExtendedAttribute ea1 = getCell(pos);
ea1.ec = (byte) b1;
ea1.ucs4 = ch;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ExtendedAttribute ea2 = getCell(nextPos);
ea2.ec = (byte) b2;
ea2.ucs4 = ch;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
setCursorAddress(incrementAddress(nextPos));
} else {
int ebc = translator.unicodeToEbcdic(ch);
ExtendedAttribute ea = getCell(pos);
ea.ec = (byte) (ebc >= 0 ? ebc : 0);
ea.ucs4 = ch;
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_NONE;
setCursorAddress(incrementAddress(pos));
}
// Set MDT
if (formatted) {
int faAddr = findFieldAttribute(pos);
if (faAddr >= 0) {
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
}
}
screenChanged = true;
updateDisplaySnapshot();
return true;
}
public synchronized boolean insertChar(char ch) {
return insertChar(cursorAddress, ch);
}
/**
* Delete a character at the specified buffer address, pulling trailing field text and preserving DBCS glyphs.
*/
public synchronized boolean deleteChar(int pos) {
int size = rows * cols;
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
if (getCell(pos).isFieldAttribute()) return false;
if (formatted) {
byte faVal = getFieldAttributeAt(pos);
if (faIsProtected(faVal & 0xFF)) return false;
}
ExtendedAttribute curCell = getCell(pos);
boolean isDbcs = curCell.db == ExtendedAttribute.DB_LEFT || curCell.db == ExtendedAttribute.DB_RIGHT
|| curCell.cs == ExtendedAttribute.CS_DBCS;
int deleteAmount = isDbcs ? 2 : 1;
if (curCell.db == ExtendedAttribute.DB_RIGHT) {
pos = decrementAddress(pos);
}
for (int d = 0; d < deleteAmount; d++) {
int shiftAddr = pos;
int count = 0;
while (count < size) {
int next = incrementAddress(shiftAddr);
if (getCell(next).isFieldAttribute()) {
getCell(shiftAddr).clear();
break;
}
getCell(shiftAddr).copyFrom(getCell(next));
shiftAddr = next;
count++;
}
}
cleanAdjacentSISO(pos);
if (formatted) {
int faAddr = findFieldAttribute(pos);
if (faAddr >= 0) {
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
}
}
screenChanged = true;
updateDisplaySnapshot();
return true;
}
public synchronized boolean deleteChar() {
return deleteChar(cursorAddress);
}
private void cleanAdjacentSISO(int nearPos) {
int size = rows * cols;
int start = Math.max(0, nearPos - 5);
int end = Math.min(size, nearPos + 10);
for (int i = start; i < end - 1; i++) {
ExtendedAttribute ea1 = getCell(i);
ExtendedAttribute ea2 = getCell(i + 1);
if (!ea1.isFieldAttribute() && !ea2.isFieldAttribute()) {
if ((ea1.ec & 0xFF) == 0x0E && (ea2.ec & 0xFF) == 0x0F) {
ea1.clear();
ea2.clear();
}
}
}
}
// ========== Entry Assist & DOC Mode Operations ==========
public boolean isEntryAssistDOCmode() { return docMode; }
public void setEntryAssistDOCmode(boolean bl) { this.docMode = bl; }
public boolean isEntryAssistWordWrap() { return wordWrap; }
public void setEntryAssistWordWrap(boolean bl) { this.wordWrap = bl; }
public int getEntryAssistStartColumn() { return docStartCol; }
public void setEntryAssistStartColumn(int n) { this.docStartCol = Math.max(0, n); }
public int getEntryAssistEndColumn() { return docEndCol >= 0 ? docEndCol : (cols - 1); }
public void setEntryAssistEndColumn(int n) { this.docEndCol = n; }
public int[] getEntryAssistTabStops() { return tabStops; }
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
public synchronized void processWordTab(boolean forward) {
int size = rows * cols;
if (size <= 0) return;
int cur = cursorAddress;
int curRow = cur / cols;
int curCol = cur % cols;
if (tabStops != null && tabStops.length > 0) {
if (forward) {
for (int stop : tabStops) {
if (stop > curCol && stop < cols) {
setCursorPosition(curRow, stop);
return;
}
}
int nextRow = (curRow + 1) % rows;
setCursorPosition(nextRow, tabStops[0]);
return;
} else {
for (int i = tabStops.length - 1; i >= 0; i--) {
int stop = tabStops[i];
if (stop < curCol && stop >= 0) {
setCursorPosition(curRow, stop);
return;
}
}
int prevRow = (curRow - 1 + rows) % rows;
setCursorPosition(prevRow, tabStops[tabStops.length - 1]);
return;
}
}
// Standard Word Tab: Jump to next / prev word boundary
if (forward) {
int addr = cur;
int count = 0;
while (count < size && getChar(addr / cols, addr % cols) != ' ' && !getCell(addr).isFieldAttribute()) {
addr = incrementAddress(addr);
count++;
}
while (count < size && (getChar(addr / cols, addr % cols) == ' ' || getCell(addr).isFieldAttribute())) {
addr = incrementAddress(addr);
count++;
}
setCursorAddress(addr);
} else {
int addr = decrementAddress(cur);
int count = 0;
while (count < size && (getChar(addr / cols, addr % cols) == ' ' || getCell(addr).isFieldAttribute())) {
addr = decrementAddress(addr);
count++;
}
while (count < size && getChar(decrementAddress(addr) / cols, decrementAddress(addr) % cols) != ' '
&& !getCell(decrementAddress(addr)).isFieldAttribute()) {
addr = decrementAddress(addr);
count++;
}
setCursorAddress(addr);
}
updateDisplaySnapshot();
}
public synchronized void processDeleteWord() {
int size = rows * cols;
if (size <= 0) return;
int cur = cursorAddress;
if (formatted) {
byte fa = getFieldAttributeAt(cur);
if (faIsProtected(fa & 0xFF)) return;
}
int endWord = cur;
int count = 0;
while (count < size && !getCell(endWord).isFieldAttribute()) {
char ch = getChar(endWord / cols, endWord % cols);
endWord = incrementAddress(endWord);
count++;
if (ch == ' ') break;
}
for (int i = 0; i < count; i++) {
deleteChar(cur);
}
screenChanged = true;
updateDisplaySnapshot();
}
// ========== DBCS Shift-Out / Shift-In Display Transformation ==========
public synchronized void processSOSI() {
int size = rows * cols;
if (size <= 0) return;
boolean insideDBCS = false;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (ea.isFieldAttribute()) {
insideDBCS = false;
continue;
}
int ec = ea.ec & 0xFF;
if (ec == 0x0E) { // Shift-Out
insideDBCS = true;
ea.db = ExtendedAttribute.DB_SO;
ea.ucs4 = ' ';
} else if (ec == 0x0F) { // Shift-In
insideDBCS = false;
ea.db = ExtendedAttribute.DB_SI;
ea.ucs4 = ' ';
} else if (insideDBCS) {
int nextIdx = (i + 1) % size;
ExtendedAttribute nextEa = buffer[nextIdx];
if (!nextEa.isFieldAttribute() && (nextEa.ec & 0xFF) != 0x0F) {
int b1 = ec;
int b2 = nextEa.ec & 0xFF;
ea.cs = ExtendedAttribute.CS_DBCS;
ea.db = ExtendedAttribute.DB_LEFT;
nextEa.cs = ExtendedAttribute.CS_DBCS;
nextEa.db = ExtendedAttribute.DB_RIGHT;
if (translator != null && translator.isDBCS() && translator.getCodePage() != null) {
char uni = translator.getCodePage().dbcsToUnicode(b1, b2);
ea.ucs4 = uni;
nextEa.ucs4 = uni;
}
i++;
}
} else {
ea.db = ExtendedAttribute.DB_NONE;
if (ea.cs == ExtendedAttribute.CS_DBCS) {
ea.cs = ExtendedAttribute.CS_BASE;
}
}
}
screenChanged = true;
updateDisplaySnapshot();
}
}
@@ -0,0 +1,361 @@
package haus.nightmare.lib3270j.datastream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.graphics.GocaConstants;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import haus.nightmare.lib3270j.printer.PrintSCS3270;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Comprehensive test suite verifying all Phase 2 functional updates:
* 3270 Data Stream parsing, Graphic Escape & APL mapping, Partitioning,
* Structured Fields (0x0F, 0x20, 0x24, 0x40), and Query Reply builders.
*/
public class DataStreamProcessorPhase2FullTest {
private EbcdicTranslator translator;
private ScreenBuffer screen;
private DataStreamProcessor processor;
private ByteArrayOutputStream output;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
processor = new DataStreamProcessor(screen, translator);
output = new ByteArrayOutputStream();
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
}
@Test
public void testGraphicEscapeOrderWithAPLTranslation() {
// Write record: CMD_EW (0x05), WCC (0xC3), SBA to 0 (0x11, 0x40, 0x40), ORDER_GE (0x08), 0xA2 (s -> )
// followed by ORDER_GE, 0x85 (e -> ), ORDER_GE, 0xCB (Cross -> ), ORDER_GE, 0xBA (Omega -> Ω)
byte[] record = new byte[] {
(byte) CMD_EW, (byte) 0xC3,
(byte) ORDER_SBA, 0x40, 0x40,
(byte) ORDER_GE, (byte) 0xA2, // Horizontal Line
(byte) ORDER_GE, (byte) 0x85, // Vertical Line
(byte) ORDER_GE, (byte) 0xCB, // Cross
(byte) ORDER_GE, (byte) 0xBA // Greek Omega
};
processor.processRecord(record);
ExtendedAttribute cell0 = screen.getCell(0);
assertEquals(CS_GE, cell0.cs);
assertEquals((byte) 0xA2, cell0.ec);
assertEquals('─', cell0.ucs4);
ExtendedAttribute cell1 = screen.getCell(1);
assertEquals(CS_GE, cell1.cs);
assertEquals((byte) 0x85, cell1.ec);
assertEquals('│', cell1.ucs4);
ExtendedAttribute cell2 = screen.getCell(2);
assertEquals(CS_GE, cell2.cs);
assertEquals((byte) 0xCB, cell2.ec);
assertEquals('┼', cell2.ucs4);
ExtendedAttribute cell3 = screen.getCell(3);
assertEquals(CS_GE, cell3.cs);
assertEquals((byte) 0xBA, cell3.ec);
assertEquals('Ω', cell3.ucs4);
}
@Test
public void testRepeatToAddressWithGraphicEscape() {
// Repeat to address 5 with ORDER_GE prefix + 0xA2 ('─')
byte[] record = new byte[] {
(byte) CMD_EW, (byte) 0xC3,
(byte) ORDER_SBA, 0x40, 0x40, // Addr 0
(byte) ORDER_RA, 0x40, (byte) 0x45, // Addr 5
(byte) ORDER_GE, (byte) 0xA2
};
processor.processRecord(record);
for (int i = 0; i < 5; i++) {
ExtendedAttribute cell = screen.getCell(i);
assertEquals(CS_GE, cell.cs, "Cell " + i + " should have CS_GE");
assertEquals('─', cell.ucs4, "Cell " + i + " should have '─'");
}
}
@Test
public void testOutbound3270DSNonZeroPartition() {
// First create partition 2 (32 rows x 80 cols)
byte[] createPart = new byte[] {
(byte) CMD_WSF,
0x00, 0x08,
(byte) SF_CREATE_PART, 0x02,
0x00, 80,
0x00, 32
};
processor.processRecord(createPart);
assertEquals(2, screen.getActivePartition());
// Now activate partition 0
screen.setActivePartition(0);
assertEquals(0, screen.getActivePartition());
// Send Outbound 3270DS targeting Partition 2 containing Write command to write 'H' at pos 0
byte[] outboundDs = new byte[] {
(byte) CMD_WSF,
0x00, 0x0A, // Field length = 10
(byte) SF_OUTBOUND_DS, 0x02, // Target PID = 2
(byte) CMD_W, 0x00, // Write command with null WCC
(byte) ORDER_SBA, 0x40, 0x40, // Pos 0
(byte) 0xC8 // EBCDIC 'H'
};
processor.processRecord(outboundDs);
// Verify active partition was switched to PID 2
assertEquals(2, screen.getActivePartition());
assertEquals('H', screen.getCell(0).ucs4);
}
@Test
public void testStructuredField0x0FSetWindow() {
// SF 0x0F with explicit viewport window (xmin=10, ymin=20, xmax=500, ymax=300)
byte[] setWindowSf = new byte[] {
(byte) CMD_WSF,
0x00, 0x0B, // Length = 11
(byte) SF_SET_WINDOW,
0x00, 10, // xMin = 10
0x00, 20, // yMin = 20
0x01, (byte) 0xF4, // xMax = 500
0x01, 0x2C // yMax = 300
};
processor.processRecord(setWindowSf);
// Test direct overload processSFSetWindow
processor.processSFSetWindow(setWindowSf);
}
@Test
public void testStructuredField0x0FDataUnitActivatesGraphicCursor() {
assertFalse(processor.getGocaDecoder().isGraphicsCursorActive());
byte[] objDataSf = new byte[] {
(byte) CMD_WSF,
0x00, 0x04,
(byte) SF_SET_WINDOW,
(byte) GocaConstants.SF_OBJDATA_SUB // 0x0F
};
processor.processRecord(objDataSf);
assertTrue(processor.getGocaDecoder().isGraphicsCursorActive());
}
@Test
public void testStructuredField0x20ObjectControl() {
// SF 0x20 (3270 Graphics / Object Control) with GOCA begin/end area orders
byte[] objControl = new byte[] {
(byte) CMD_WSF,
0x00, 0x07,
(byte) SF_OBJECT_CONTROL,
(byte) GocaConstants.G_GBAR, 0x00, // Begin area
(byte) GocaConstants.G_GEAR, 0x00 // End area
};
processor.processRecord(objControl);
processor.processSFObjectControl(objControl);
}
@Test
public void testStructuredField0x24DocumentDataWithEmbeddedSCS() {
AtomicReference<byte[]> scsCaptured = new AtomicReference<>();
PrintSCS3270 mockScs = new PrintSCS3270(null, null, translator) {
@Override
public void processHostData(byte[] data, int offset, int length) {
byte[] b = new byte[length];
System.arraycopy(data, offset, b, 0, length);
scsCaptured.set(b);
}
};
processor.setEmbeddedScsProcessor(mockScs);
// SF 0x24 (Document Data) with 4 bytes of SCS printer data
byte[] docData = new byte[] {
(byte) CMD_WSF,
0x00, 0x07,
(byte) SF_DOCUMENT_DATA,
(byte) 0x15, (byte) 0xC8, (byte) 0xC9, (byte) 0x15 // NL, 'H', 'I', NL
};
processor.processRecord(docData);
assertNotNull(scsCaptured.get());
assertEquals(4, scsCaptured.get().length);
assertEquals(0x15, scsCaptured.get()[0]);
assertEquals((byte) 0xC8, scsCaptured.get()[1]);
// Test direct method
processor.processSFDocumentData(docData);
}
@Test
public void testQueryReplyBuilderGraphicColor109Bytes() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
byte[] grColor = qrBuilder.buildGraphicColor();
assertNotNull(grColor);
assertEquals(105, grColor.length); // 105 bytes payload + 4 bytes header = 109 bytes in complete SF
// Check header bytes in payload: 0x00, 0x04, 0x00, 0xFF, 0xFF, 0x00, 0x10, 0x00, 0x10
assertEquals(0x00, grColor[0]);
assertEquals(0x04, grColor[1]);
assertEquals(0x00, grColor[2]);
assertEquals((byte) 0xFF, grColor[3]);
assertEquals((byte) 0xFF, grColor[4]);
assertEquals(0x00, grColor[5]);
assertEquals(0x10, grColor[6]);
assertEquals(0x00, grColor[7]);
assertEquals(0x10, grColor[8]);
// Check aliases
assertArrayEquals(grColor, qrBuilder.buildGrColor());
assertArrayEquals(grColor, qrBuilder.buildGColor());
}
@Test
public void testQueryReplyBuilderAuxDeviceAndLineType() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
byte[] lineType = qrBuilder.buildAuxDevice();
assertNotNull(lineType);
assertEquals(20, lineType.length); // 20 bytes payload + 4 bytes header = 24 bytes
assertArrayEquals(lineType, qrBuilder.buildLineType());
assertArrayEquals(lineType, qrBuilder.buildAuxDev());
}
@Test
public void testQueryReplyBuilderOemFormatAndPort() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
byte[] port = qrBuilder.buildOemFormat();
assertNotNull(port);
assertTrue(port.length > 0);
assertArrayEquals(port, qrBuilder.buildPort());
}
@Test
public void testQueryReplyBuilderUsableAreaAspectRatiosAndNoArgOverloads() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
// 1. Vector Graphics enabled (3179G standard: SDH = 16, flags = 0x03)
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
byte[] uaVector = qrBuilder.buildUsableArea();
assertNotNull(uaVector);
assertEquals(19, uaVector.length);
assertEquals(0x03, uaVector[0] & 0xFF); // Flags: 12/14 bit + Graphics
assertEquals(9, uaVector[15] & 0xFF); // AW = 9
assertEquals(16, uaVector[16] & 0xFF); // AH = 16
// 2. Text mode (GraphicsMode.NONE: SDH = 12, flags = 0x01)
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
byte[] uaText = qrBuilder.buildUsableArea();
assertNotNull(uaText);
assertEquals(0x01, uaText[0] & 0xFF); // Flags: 12/14 bit only
assertEquals(9, uaText[15] & 0xFF); // AW = 9
assertEquals(12, uaText[16] & 0xFF); // AH = 12
// 3. No-arg overloads check
assertNotNull(qrBuilder.buildSummary());
assertNotNull(qrBuilder.buildAlphaPartitions());
assertNotNull(qrBuilder.buildCharsets());
assertNotNull(qrBuilder.buildColor());
assertNotNull(qrBuilder.buildHighlighting());
assertNotNull(qrBuilder.buildReplyModes());
assertNotNull(qrBuilder.buildDdm());
assertNotNull(qrBuilder.buildImplicitPartition());
assertNotNull(qrBuilder.buildSegment());
assertNotNull(qrBuilder.buildProcedure());
assertNotNull(qrBuilder.buildGraphics());
assertNotNull(qrBuilder.buildGImage());
}
@Test
public void testQueryReplyBuilderColorTable18Bytes() {
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
byte[] colorTable = qrBuilder.buildColor();
assertNotNull(colorTable);
assertEquals(18, colorTable.length, "Color table payload must be exactly 18 bytes matching HoD DS3270.java:1850");
assertEquals(0x00, colorTable[0]);
assertEquals(0x08, colorTable[1]);
assertEquals(0x00, colorTable[2]);
assertEquals((byte) 0xF4, colorTable[3]); // Default green
}
@Test
public void testDataStreamProcessorConvenienceOverloads() {
// Test processEraseAllUnprotected
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.getCell(1).ucs4 = 'X';
screen.getCell(1).ec = (byte) 0xE7;
processor.processEraseAllUnprotected();
assertEquals(0, screen.getCell(1).ucs4);
// Test processReadModified and processReadModifiedAll
output.reset();
processor.processReadModified();
assertTrue(output.size() >= 3);
output.reset();
processor.processReadModifiedAll();
assertTrue(output.size() >= 3);
// Test processReadBuffer
output.reset();
processor.processReadBuffer();
assertTrue(output.size() >= 3);
// Test processEraseWrite and processEraseWriteAlternate
byte[] ewData = new byte[] { (byte) CMD_EW, 0x00, (byte) ORDER_SBA, 0x40, 0x40, (byte) 0xC1 };
processor.processEraseWrite(ewData);
assertEquals('A', screen.getCell(0).ucs4);
byte[] ewaData = new byte[] { (byte) CMD_EWA, 0x00, (byte) ORDER_SBA, 0x40, 0x40, (byte) 0xC2 };
processor.processEraseWriteAlternate(ewaData);
assertEquals('B', screen.getCell(0).ucs4);
// Test processSetReplyMode, processCreatePartition, processEraseReset byte[] overloads
byte[] srm = new byte[] { (byte) CMD_WSF, 0x00, 0x05, (byte) SF_SET_REPLY_MODE, 0x00, (byte) SF_SRM_CHAR };
processor.processSetReplyMode(srm);
assertEquals((byte) SF_SRM_CHAR, screen.getReplyMode());
byte[] cp = new byte[] { (byte) CMD_WSF, 0x00, 0x08, (byte) SF_CREATE_PART, 0x01, 0x00, 80, 0x00, 24 };
processor.processCreatePartition(cp);
assertEquals(1, screen.getActivePartition());
byte[] er = new byte[] { (byte) CMD_WSF, 0x00, 0x04, (byte) SF_ERASE_RESET, (byte) SF_ER_DEFAULT };
processor.processEraseReset(er);
assertEquals(0, screen.getActivePartition());
// Test query partition overloads
output.reset();
processor.processSFReadPartitionQuery(new byte[] { (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SF_RP_QUERY });
assertTrue(output.size() > 0);
output.reset();
processor.processSFReadPartitionQueryList(new byte[] {
(byte) CMD_WSF, 0x00, 0x07, (byte) SF_READ_PART, 0x00, (byte) SF_RP_QLIST, (byte) SF_RPQ_LIST, (byte) QR_COLOR
});
assertTrue(output.size() > 0);
}
}
@@ -0,0 +1,128 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLOIAPhase3Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLOIA oia;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, input, null);
}
@Test
public void testAlphanumericTypeDetection() {
screen.erase(false);
// Unformatted screen
assertEquals(TYPE_ALPHANUMERIC, oia.getAlphanumericType());
assertEquals("A", oia.getAlphanumericTypeString());
// Numeric field
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_NUMERIC));
screen.setCursorAddress(1);
assertEquals(TYPE_NUMERIC, oia.getAlphanumericType());
assertEquals("N", oia.getAlphanumericTypeString());
// Alphanumeric unprotected field
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
screen.setCursorAddress(11);
assertEquals(TYPE_ALPHANUMERIC, oia.getAlphanumericType());
assertEquals("A", oia.getAlphanumericTypeString());
// DBCS cell
ExtendedAttribute ea = screen.getCell(11);
ea.cs = ExtendedAttribute.CS_DBCS;
assertEquals(TYPE_DBCS, oia.getAlphanumericType());
assertEquals("D", oia.getAlphanumericTypeString());
}
@Test
public void testOIAStatusFlagsAndStrings() {
screen.erase(false);
assertEquals("READY", oia.getStatusString());
assertFalse(oia.isXSystem());
assertFalse(oia.isXProt());
assertFalse(oia.isXNum());
assertFalse(oia.isXComm());
assertFalse(oia.isXOverflow());
assertFalse(oia.isXOperatorDue());
// Keyboard lock -> X-SYSTEM
input.setKeyboardLocked(true);
assertTrue(oia.isXSystem());
assertTrue(oia.isXWait());
assertEquals("X-SYSTEM", oia.getStatusString());
input.setKeyboardLocked(false);
// Protected field inhibit
oia.setInputInhibited(INHIBIT_PROTECTED_FIELD);
assertTrue(oia.isXProt());
assertEquals("X-PROT", oia.getStatusString());
// Numeric only inhibit
oia.setInputInhibited(INHIBIT_NUMERIC_ONLY);
assertTrue(oia.isXNum());
assertEquals("X-NUM", oia.getStatusString());
// Overflow inhibit
oia.setInputInhibited(INHIBIT_OVERFLOW);
assertTrue(oia.isXOverflow());
assertEquals("X-OVERFLOW", oia.getStatusString());
// Comm check inhibit
oia.setInputInhibited(INHIBIT_COMM_CHECK);
assertTrue(oia.isXComm());
assertEquals("X-COMM", oia.getStatusString());
// Operator due inhibit
oia.setInputInhibited(INHIBIT_OPERATOR_DUE);
assertTrue(oia.isXOperatorDue());
assertEquals("X-OP", oia.getStatusString());
// Insert mode
oia.setInputInhibited(INHIBIT_NOT_INHIBITED);
input.setInsertMode(true);
assertTrue(oia.isXInsert());
assertEquals("X-INSERT", oia.getStatusString());
}
@Test
public void testECLFieldOperations() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setChar(0, 1, 'X');
screen.setChar(0, 2, 'Y');
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
ECLPS ps = new ECLPS(screen, input, translator);
ECLField f = ps.getFieldList().getFirstField();
assertNotNull(f);
assertEquals("XY ", f.getText());
assertFalse(f.isModified());
f.setModified(true);
assertTrue(f.isModified());
f.erase();
assertEquals(" ", f.getText());
assertFalse(f.isModified());
}
}
@@ -0,0 +1,124 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLPSPhase3Test implements ECLConstants {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testSearchPSOverloadsAndCaseOptions() {
screen.erase(false);
screen.setText("Host On-Demand ECL Automation");
// 1-based SearchPS
assertEquals(6, ps.SearchPS("On-Demand"));
assertEquals(6, ps.SearchPS("on-demand", 1, 1, SEARCH_FORWARD, true));
assertEquals(0, ps.SearchPS("on-demand", 1, 1, SEARCH_FORWARD, false));
assertEquals(0, ps.SearchPS("NONEXISTENT"));
// SearchPSExt
int pos = ps.SearchPSExt("ECL", 1, 50, SEARCH_FORWARD, false, false);
assertEquals(16, pos);
}
@Test
public void testSearchStringBackwardsAndWrap() {
screen.erase(false);
screen.setChar(0, 10, 'A');
screen.setChar(0, 11, 'B');
screen.setChar(0, 12, 'C');
screen.setChar(1, 20, 'A');
screen.setChar(1, 21, 'B');
screen.setChar(1, 22, 'C');
// Backward search starting from row 1 col 0 (addr 80) should find "ABC" at row 0 col 10 (addr 10)
int found = ps.searchString("ABC", 1, 0, SEARCH_BACKWARD, false);
assertEquals(10, found);
// Backward search starting from row 0 col 5 (addr 5) wraps around and finds "ABC" at row 1 col 20 (addr 100)
int wrapped = ps.searchString("ABC", 0, 5, SEARCH_BACKWARD, false);
assertEquals(100, wrapped);
}
@Test
public void testCopyStringRectangularExtraction() {
screen.erase(false);
// Row 0: "0123456789"
for (int c = 0; c < 10; c++) {
screen.setChar(0, c, (char) ('0' + c));
}
// Row 1: "ABCDEFGHIJ"
for (int c = 0; c < 10; c++) {
screen.setChar(1, c, (char) ('A' + c));
}
// Copy columns 2..5 of rows 0..1
String block = ps.copyString(0, 2, 1, 5);
assertEquals("2345\nCDEF", block);
}
@Test
public void testPasteStringRectangular() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
int pasted = ps.pasteString("HELLO\nWORLD", 0, 1);
assertEquals(10, pasted);
assertEquals("HELLO", ps.getString(1, 5));
assertEquals("WORLD", ps.getString(81, 5));
}
@Test
public void testSendCharactersWithDelay() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
ps.sendCharacters("ABC[tab]DEF", 2);
assertEquals('A', screen.getChar(0, 1));
assertEquals('B', screen.getChar(0, 2));
assertEquals('C', screen.getChar(0, 3));
assertEquals('D', screen.getChar(0, 11));
assertEquals('E', screen.getChar(0, 12));
assertEquals('F', screen.getChar(0, 13));
}
@Test
public void testWaitForScreenAndCursor() {
screen.erase(false);
assertFalse(ps.waitForScreen("READY", 50));
assertFalse(ps.waitForCursor(5, 10, 50));
screen.setText("READY");
assertTrue(ps.waitForScreen("READY", 50));
assertTrue(ps.waitForScreen("READY", 0, 0, 50));
assertFalse(ps.waitForScreen("READY", 1, 0, 50));
ps.setCursorPos(5, 10);
assertTrue(ps.waitForCursor(5, 10, 50));
}
}
@@ -288,6 +288,199 @@ public class GocaDecoderPhase5Test {
assertEquals(56, sf2.length);
assertEquals(0x00, sf2[0]);
assertEquals(0x34, sf2[1]);
byte[] sf3 = GraphicInputBuilder.buildPickCorrelation(120, 240, 5);
assertEquals(56, sf3.length);
assertEquals(120, ((sf3[24] & 0xFF) << 8) | (sf3[25] & 0xFF));
assertEquals(240, ((sf3[26] & 0xFF) << 8) | (sf3[27] & 0xFF));
}
@Test
public void testViewingWindowClipping() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Define viewing window covering center of presentation space: [-100..100, -100..100]
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSVW_DEF);
out.write(0x08);
out.write((byte) 0xFF); out.write((byte) 0x9C); // xMin = -100
out.write((byte) 0xFF); out.write((byte) 0x9C); // yMin = -100
out.write(0x00); out.write(100); // xMax = 100
out.write(0x00); out.write(100); // yMax = 100
// Draw a line inside presentation space but outside the viewing window: (-300, -150) -> (-200, -150)
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red
out.write(GocaConstants.G_GLINE); out.write(0x08);
out.write((byte) 0xFE); out.write((byte) 0xD4); out.write((byte) 0xFF); out.write((byte) 0x6A); // (-300, -150)
out.write((byte) 0xFF); out.write((byte) 0x38); out.write((byte) 0xFF); out.write((byte) 0x6A); // (-200, -150)
byte[] stream = out.toByteArray();
decoder.decodeGoca(stream, 0, stream.length);
// Pixels outside viewing window should have been clipped out
int pxClipped = plane.mapX(-300);
int pyClipped = plane.mapY(-150);
int pixelOutside = plane.getRgbBuffer()[pyClipped * plane.getCanvasWidth() + pxClipped];
assertEquals(0, pixelOutside, "Pixel outside viewing window must be 0 (clipped)");
// Now draw a line crossing the center (0, 0) -> (50, 50)
ByteArrayOutputStream insideStream = new ByteArrayOutputStream();
insideStream.write(GocaConstants.G_GLINE); insideStream.write(0x08);
insideStream.write(0x00); insideStream.write(0); insideStream.write(0x00); insideStream.write(0);
insideStream.write(0x00); insideStream.write(50); insideStream.write(0x00); insideStream.write(50);
byte[] inBytes = insideStream.toByteArray();
decoder.decodeGoca(inBytes, 0, inBytes.length);
int pxInside = plane.mapX(0);
int pyInside = plane.mapY(0);
int pixelInside = plane.getRgbBuffer()[pyInside * plane.getCanvasWidth() + pxInside];
assertNotEquals(0, pixelInside, "Pixel inside viewing window must be drawn");
}
@Test
public void testFractionalLineWidth() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Fractional Line Width: 2.5 (0x02 0x80)
out.write(GocaConstants.G_GSFLW);
out.write(0x02);
out.write(0x80);
byte[] stream = out.toByteArray();
decoder.decodeGoca(stream, 0, stream.length);
assertEquals(2.5, decoder.getFractionalLineWidth(), 0.01);
assertEquals(2.5, plane.getFractionalLineWidth(), 0.01);
}
@Test
public void testSegmentCharacteristics() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Flags = 0xC0: Chained (0x80), Dynamic (0x40), Visible (0x00)
decoder.processSegmentCharacteristics(new byte[]{(byte) 0xC0});
assertTrue(decoder.isSegChained());
assertTrue(decoder.isSegDynamic());
assertTrue(decoder.isSegVisible());
// Flags = 0x20: Invisible
decoder.processSegmentCharacteristics(new byte[]{(byte) 0x20});
assertFalse(decoder.isSegChained());
assertFalse(decoder.isSegDynamic());
assertFalse(decoder.isSegVisible());
}
@Test
public void testCharacterAngleAndShear() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Character Angle: 45 degrees vector (ax=10, ay=10)
out.write(GocaConstants.G_GSCA);
out.write(0x04);
out.write(0x00); out.write(10);
out.write(0x00); out.write(10);
// Set Character Shear: vector (sx=10, sy=0)
out.write(GocaConstants.G_GSCR);
out.write(0x04);
out.write(0x00); out.write(10);
out.write(0x00); out.write(0);
byte[] stream = out.toByteArray();
decoder.decodeGoca(stream, 0, stream.length);
assertEquals(45.0, decoder.getCharAngle(), 0.01);
assertEquals(90.0, decoder.getCharShear(), 0.01);
}
@Test
public void testWindingRuleAreaFill() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
FillArea fillArea = new FillArea();
fillArea.setFillRule(GocaConstants.FILL_RULE_WINDING);
assertEquals(GocaConstants.FILL_RULE_WINDING, fillArea.getFillRule());
// Fill self-intersecting bow-tie polygon
int[] px = new int[]{50, 150, 50, 150};
int[] py = new int[]{50, 150, 150, 50};
fillArea.fill(plane, px, py, 4, 0xFF00FF00, GocaConstants.PT_SOLID, false, 0, 0, 0, 2, 0);
assertTrue(plane.hasContent());
}
@Test
public void testMultiBitAndCompressedImages() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
// Test 2-bit per pixel image
byte[] twoBppData = new byte[]{(byte) 0b00011011}; // 4 pixels: 0, 1 (Blue), 2 (Red), 3 (Green)
plane.drawImage(10, 10, 4, 1, twoBppData, 0xFFFFFFFF, GocaConstants.BPP_2, GocaConstants.IMG_UNCOMPRESSED);
assertTrue(plane.hasContent());
// Test 4-bit per pixel image
byte[] fourBppData = new byte[]{(byte) 0x12, (byte) 0x34}; // 4 pixels: 1, 2, 3, 4
plane.clear();
plane.drawImage(10, 10, 4, 1, fourBppData, 0xFFFFFFFF, GocaConstants.BPP_4, GocaConstants.IMG_UNCOMPRESSED);
assertTrue(plane.hasContent());
// Test RLE decompressed image
byte[] rleData = new byte[]{
0x04, (byte) 0xFF // 4 repeats of 0xFF
};
byte[] decompressed = GraphicsPlane.decompressGocaRle(rleData, 32, 1, GocaConstants.BPP_1);
assertEquals(4, decompressed.length);
assertEquals((byte) 0xFF, decompressed[0]);
assertEquals((byte) 0xFF, decompressed[3]);
}
@Test
public void testMultiColorProgrammedSymbolsOverlay() {
ProgramSymbolManager psm = new ProgramSymbolManager(9, 16);
// Load Red plane (colorPlane = 1) into Slot 4 (LCID 0x41, Start 0x40, RWS 0x04)
byte[] loadRed = new byte[4 + 6 + 18];
loadRed[0] = (byte) 0x81; // Extended header present (0x80) | Format 1
loadRed[1] = 0x41; // LCID
loadRed[2] = 0x40; // Start codepoint
loadRed[3] = 0x04; // RWS Slot 4 (Triple Plane)
loadRed[4] = 0x06; // Ext header length = 6
loadRed[5] = 0x00;
loadRed[6] = 9; // Cell width = 9
loadRed[7] = 16; // Cell height = 16
loadRed[8] = 0x00;
loadRed[9] = 0x01; // Plane = Red (1)
for (int i = 0; i < 18; i++) loadRed[10 + i] = (byte) 0xFF;
psm.loadps(loadRed);
// Load Green plane (colorPlane = 2) into the same slot without clearing (clearAll = false)
byte[] loadGreen = new byte[4 + 6 + 18];
loadGreen[0] = (byte) 0x81;
loadGreen[1] = 0x41;
loadGreen[2] = 0x40;
loadGreen[3] = 0x04;
loadGreen[4] = 0x06;
loadGreen[5] = 0x00;
loadGreen[6] = 9;
loadGreen[7] = 16;
loadGreen[8] = 0x00;
loadGreen[9] = 0x02; // Plane = Green (2)
for (int i = 0; i < 18; i++) loadGreen[10 + i] = (byte) 0xFF;
psm.loadps(loadGreen);
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x41, 0x40);
assertNotNull(slot);
byte[] pixels = slot.getPixelData();
assertNotNull(pixels);
// Pixels should be composite of Red (1) | Green (2) = Yellow (3)
assertEquals(3, pixels[0] & 0xFF);
}
private static byte outCoord(int val) {
@@ -0,0 +1,323 @@
package haus.nightmare.lib3270j.input;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLConstants;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.protocol.DS3270Constants;
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 static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class InputProcessorPhase4Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor inputProcessor;
private ECLOIA oia;
@BeforeEach
public void setUp() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
inputProcessor = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, inputProcessor, null);
inputProcessor.setOIA(oia);
}
@Test
public void testNumericOnlyValidation() {
// Setup numeric field at pos 0 (FA), data at 1..9
screen.setFieldAttribute(0, (byte) FA_NUMERIC);
screen.setFieldAttribute(10, (byte) FA_PROTECT); // terminate field
screen.setCursorAddress(1);
// Typing valid digits and punctuation
inputProcessor.typeCharacter('1');
assertEquals(2, screen.getCursorAddress());
assertEquals('1', (char) screen.getCell(1).ucs4);
assertFalse(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
inputProcessor.typeCharacter('-');
assertEquals(3, screen.getCursorAddress());
assertEquals('-', (char) screen.getCell(2).ucs4);
inputProcessor.typeCharacter('.');
assertEquals(4, screen.getCursorAddress());
assertEquals('.', (char) screen.getCell(3).ucs4);
inputProcessor.typeCharacter(' ');
assertEquals(5, screen.getCursorAddress());
// Typing invalid non-numeric character 'A' in numeric field
inputProcessor.typeCharacter('A');
assertTrue(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_NUMERIC_ONLY, oia.getInputInhibited());
assertEquals("X-NUM", oia.getStatusString());
// Buffer position 5 should NOT have changed
assertEquals(5, screen.getCursorAddress());
assertEquals(0, screen.getCell(5).ec);
// Reset clears error and unlocks keyboard
inputProcessor.reset();
assertFalse(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
}
@Test
public void testProtectedFieldInhibition() {
// Setup protected field at pos 0, length 10
screen.setFieldAttribute(0, (byte) FA_PROTECT);
screen.setCursorAddress(1);
// Attempting to type into protected field
inputProcessor.typeCharacter('Z');
assertTrue(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_PROTECTED_FIELD, oia.getInputInhibited());
assertEquals("X-PROT", oia.getStatusString());
assertEquals(0, screen.getCell(1).ec);
// Reset unlocks
inputProcessor.reset();
assertFalse(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
}
@Test
public void testAutoSkipFieldBoundary() {
// Field 1: Unprotected at pos 0 (chars at 1, 2)
screen.setFieldAttribute(0, (byte) 0x00);
// Field 2: Auto-skip at pos 3 (FA_PROTECT | FA_NUMERIC) (chars at 4, 5)
screen.setFieldAttribute(3, (byte) (FA_PROTECT | FA_NUMERIC));
// Field 3: Unprotected at pos 6 (chars at 7, 8)
screen.setFieldAttribute(6, (byte) 0x00);
// Delimiter at pos 9
screen.setFieldAttribute(9, (byte) FA_PROTECT);
// Cursor at pos 1, type 'A' -> moves to pos 2
screen.setCursorAddress(1);
inputProcessor.typeCharacter('A');
assertEquals(2, screen.getCursorAddress());
// Type 'B' at pos 2 (the last char in Field 1) -> auto-skips over Field 2 (pos 3..5) to pos 7!
inputProcessor.typeCharacter('B');
assertEquals(7, screen.getCursorAddress());
}
@Test
public void testInsertModeFieldOverflowInhibition() {
// Unprotected field at pos 0, size 3 (positions 1, 2, 3)
screen.setFieldAttribute(0, (byte) 0x00);
screen.setFieldAttribute(4, (byte) FA_PROTECT);
screen.setCursorAddress(1);
inputProcessor.typeCharacter('A');
inputProcessor.typeCharacter('B');
inputProcessor.typeCharacter('C');
// Field is full: pos 1='A', pos 2='B', pos 3='C'
assertEquals('A', (char) screen.getCell(1).ucs4);
assertEquals('B', (char) screen.getCell(2).ucs4);
assertEquals('C', (char) screen.getCell(3).ucs4);
// Enable insert mode and attempt to type at pos 1
inputProcessor.setInsertMode(true);
screen.setCursorAddress(1);
inputProcessor.typeCharacter('X');
assertTrue(inputProcessor.isKeyboardLocked());
assertEquals(ECLConstants.INHIBIT_OVERFLOW, oia.getInputInhibited());
assertEquals("X-OVERFLOW", oia.getStatusString());
assertEquals('A', (char) screen.getCell(1).ucs4); // unchanged
}
@Test
public void testReadModifiedInboundDataFraming() {
// Create field at pos 0, type "TEST"
screen.setFieldAttribute(0, (byte) 0x00);
screen.setFieldAttribute(10, (byte) FA_PROTECT);
screen.setCursorAddress(1);
inputProcessor.typeCharacter('T');
inputProcessor.typeCharacter('E');
inputProcessor.typeCharacter('S');
inputProcessor.typeCharacter('T');
byte[] inbound = inputProcessor.buildReadModifiedInboundData(AID_ENTER, false);
assertNotNull(inbound);
assertTrue(inbound.length >= 7);
assertEquals((byte) AID_ENTER, inbound[0]); // AID
// Next 2 bytes: cursor address
// Next byte: SBA (0x11)
assertEquals((byte) ORDER_SBA, inbound[3]);
// Followed by field address (pos 1), then EBCDIC bytes for "TEST"
assertEquals((byte) translator.unicodeToEbcdic('T'), inbound[6]);
assertEquals((byte) translator.unicodeToEbcdic('E'), inbound[7]);
assertEquals((byte) translator.unicodeToEbcdic('S'), inbound[8]);
assertEquals((byte) translator.unicodeToEbcdic('T'), inbound[9]);
}
@Test
public void testAidSelectDoesNotTransmitFieldCharacters() {
// Create selectable field with designator ' ' at pos 1
screen.setFieldAttribute(0, (byte) (FA_INT_HIGH_SEL | FA_NUMERIC)); // selectable
screen.getCell(0).fa |= FA_MODIFY;
screen.getCell(1).ec = 0x40; // space designator
screen.getCell(2).ec = (byte) translator.unicodeToEbcdic('X');
screen.setFieldAttribute(10, (byte) FA_PROTECT);
screen.setCursorAddress(1);
byte[] inbound = inputProcessor.buildReadModifiedInboundData(AID_SELECT, false);
assertNotNull(inbound);
assertEquals((byte) AID_SELECT, inbound[0]);
// Must contain SBA and designator address, but NO field text bytes
assertEquals(6, inbound.length); // AID (1) + Cursor (2) + SBA (1) + Addr (2) = 6 bytes
assertEquals((byte) ORDER_SBA, inbound[3]);
}
@Test
public void testPAKeysAndClearInboundData() {
screen.setCursorAddress(80);
byte[] pa1 = inputProcessor.buildReadModifiedInboundData(AID_PA1, false);
assertEquals(3, pa1.length); // AID + 2 cursor bytes
assertEquals((byte) AID_PA1, pa1[0]);
byte[] clear = inputProcessor.buildReadModifiedInboundData(AID_CLEAR, false);
assertEquals(3, clear.length);
assertEquals((byte) AID_CLEAR, clear[0]);
}
@Test
public void testReadBufferInboundDataStandardAndExtended() {
screen.setFieldAttribute(0, (byte) 0x00);
screen.getCell(1).ec = (byte) translator.unicodeToEbcdic('A');
screen.setFieldAttribute(10, (byte) FA_PROTECT);
// Standard Field Mode
screen.setReplyMode((byte) SF_SRM_FIELD);
byte[] standardBuf = inputProcessor.buildReadBufferInboundData(AID_ENTER);
assertNotNull(standardBuf);
assertEquals((byte) AID_ENTER, standardBuf[0]);
assertEquals((byte) ORDER_SF, standardBuf[3]);
// Extended Field Mode
screen.setReplyMode((byte) SF_SRM_XFIELD);
byte[] extendedBuf = inputProcessor.buildReadBufferInboundData(AID_ENTER);
assertNotNull(extendedBuf);
assertEquals((byte) AID_ENTER, extendedBuf[0]);
assertEquals((byte) ORDER_SFE, extendedBuf[3]);
}
@Test
public void testWordLeftWordRightAndFieldEndNavigation() {
// Field: "HELLO WORLD "
screen.setFieldAttribute(0, (byte) 0x00);
screen.setFieldAttribute(25, (byte) FA_PROTECT);
String text = "HELLO WORLD ";
for (int i = 0; i < text.length(); i++) {
screen.getCell(1 + i).ec = (byte) translator.unicodeToEbcdic(text.charAt(i));
screen.getCell(1 + i).ucs4 = text.charAt(i);
}
// Test Field End
screen.setCursorAddress(1);
inputProcessor.processFieldEnd();
// Immediately following 'D' in WORLD (pos 1 + 13 = 14)
assertEquals(14, screen.getCursorAddress());
// Test Word Left from pos 14 -> jumps to start of "WORLD" (pos 9)
inputProcessor.processWordLeft();
assertEquals(9, screen.getCursorAddress());
// Test Word Left from pos 9 -> jumps to start of "HELLO" (pos 1)
inputProcessor.processWordLeft();
assertEquals(1, screen.getCursorAddress());
// Test Word Right from pos 1 -> jumps to start of "WORLD" (pos 9)
inputProcessor.processWordRight();
assertEquals(9, screen.getCursorAddress());
}
@Test
public void testAll22CompatibleMethodsAndOverloads() {
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_2, false);
Telnet3270Client client = new Telnet3270Client(config);
// 1. processChar
client.processChar('A');
client.processChar('B', 5, false);
// 2. processEnter
client.processEnter();
// 3. processPF
client.processPF(3);
// 4. processPA
client.processPA(1);
// 5. processClear
client.processClear();
// 6-9. Cursor navigation
client.processCursorUp();
client.processCursorDown();
client.processCursorLeft();
client.processCursorRight();
// 10-13. Field navigation
client.processTab();
client.processBackTab();
client.processHome();
client.processNewline();
// 14-16. Editing
client.processDelete();
client.processBackspace();
client.processEraseEOF();
client.processEraseInput();
// 17-19. Special orders & toggles
client.processDup();
client.processFieldMark();
client.processToggleInsert();
// 20-22. Control / Selection
client.processReset();
client.processWordLeft();
client.processWordRight();
client.processFieldEnd();
client.processAttn();
client.processSysReq();
client.processCurSel();
client.processLightPen();
client.processLightPen(10);
assertNotNull(client.getInputProcessor().buildReadModifiedInboundData());
assertNotNull(client.getInputProcessor().buildReadBufferInboundData());
}
@Test
public void testSendKeysMnemonicTokens() {
screen.setFieldAttribute(0, (byte) 0x00);
screen.setFieldAttribute(15, (byte) 0x00);
screen.setFieldAttribute(30, (byte) FA_PROTECT);
screen.setCursorAddress(1);
inputProcessor.sendKeys("123[tab]456[wordleft][fieldend][reset]");
assertEquals('1', (char) screen.getCell(1).ucs4);
assertEquals('2', (char) screen.getCell(2).ucs4);
assertEquals('3', (char) screen.getCell(3).ucs4);
assertEquals('4', (char) screen.getCell(16).ucs4);
assertEquals('5', (char) screen.getCell(17).ucs4);
assertEquals('6', (char) screen.getCell(18).ucs4);
assertFalse(inputProcessor.isKeyboardLocked());
}
}
@@ -0,0 +1,278 @@
package haus.nightmare.lib3270j.screen;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.AbstractDBCSCodePage;
import haus.nightmare.lib3270j.charset.CodePageRegistry;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ScreenBufferPhase3Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
}
@Test
public void testWrappedFieldListFirstFaNotAtZero() {
screen.erase(false);
// Place FA at pos 100 (unprotected)
screen.setFieldAttribute(100, (byte) FA_PRINTABLE);
// Place FA at pos 150 (protected) - wraps around 1919 back to 99
screen.setFieldAttribute(150, (byte) (FA_PRINTABLE | FA_PROTECT));
ECLFieldList list = screen.buildFieldList();
assertEquals(2, list.getFieldCount());
ECLField f1 = list.getFirstField();
assertNotNull(f1);
assertEquals(100, f1.getStart());
assertEquals(101, f1.getDataStart());
assertEquals(149, f1.getEnd());
assertEquals(49, f1.getLength());
assertFalse(f1.isWrapped());
assertFalse(f1.isProtected());
ECLField f2 = list.getNextField(f1);
assertNotNull(f2);
assertEquals(150, f2.getStart());
assertEquals(151, f2.getDataStart());
assertEquals(99, f2.getEnd());
assertEquals(1920 - 150 - 1 + 100, f2.getLength());
assertTrue(f2.isWrapped());
assertTrue(f2.isProtected());
// Test field containment on wrapped field
assertTrue(f2.contains(150));
assertTrue(f2.contains(151));
assertTrue(f2.contains(1919));
assertTrue(f2.contains(0));
assertTrue(f2.contains(99));
assertFalse(f2.contains(100));
assertFalse(f2.contains(120));
// Test field lookup at position
assertEquals(150, screen.findFieldAt(50).getStart());
assertEquals(100, screen.findFieldAt(120).getStart());
// Test navigation
assertEquals(150, screen.findPrevField(120).getStart());
assertEquals(150, screen.findNextField(120).getStart());
}
@Test
public void testSingleFieldScreenWrap() {
screen.erase(false);
screen.setFieldAttribute(50, (byte) FA_PRINTABLE);
ECLFieldList list = screen.buildFieldList();
assertEquals(1, list.getFieldCount());
ECLField f = list.getFirstField();
assertNotNull(f);
assertEquals(50, f.getStart());
assertEquals(51, f.getDataStart());
assertEquals(49, f.getEnd());
assertEquals(1919, f.getLength());
assertTrue(f.isWrapped());
assertTrue(f.contains(0));
assertTrue(f.contains(50));
assertTrue(f.contains(1919));
}
@Test
public void testInsertCharSBCSAndOverflow() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(6, (byte) (FA_PRINTABLE | FA_PROTECT)); // field of 5 chars (pos 1..5)
// Write 'A', 'B', 'C', 'D' at pos 1..4 (pos 5 is null)
screen.setChar(0, 1, 'A');
screen.setChar(0, 2, 'B');
screen.setChar(0, 3, 'C');
screen.setChar(0, 4, 'D');
// Insert 'Z' at pos 1
boolean inserted = screen.insertChar(1, 'Z');
assertTrue(inserted);
assertEquals('Z', screen.getChar(0, 1));
assertEquals('A', screen.getChar(0, 2));
assertEquals('B', screen.getChar(0, 3));
assertEquals('C', screen.getChar(0, 4));
assertEquals('D', screen.getChar(0, 5));
// Field is now full (pos 5 is 'D'). Inserting another char should overflow and return false
boolean overflow = screen.insertChar(1, 'X');
assertFalse(overflow);
}
@Test
public void testInsertAndShiftDBCSChar() {
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
dbcsScreen.erase(false);
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
dbcsScreen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
// Insert DBCS char at pos 1
boolean ok = dbcsScreen.insertChar(1, '\u6771');
assertTrue(ok);
assertEquals(0x43, dbcsScreen.getCell(1).ec & 0xFF);
assertEquals(0x41, dbcsScreen.getCell(2).ec & 0xFF);
assertEquals(ExtendedAttribute.CS_DBCS, dbcsScreen.getCell(1).cs);
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
assertEquals(3, dbcsScreen.getCursorAddress());
}
@Test
public void testDeleteCharDBCSAndCleanAdjacentSISO() {
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
cp.registerDbcsPair(0x4341, '\u6771');
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
dbcsScreen.erase(false);
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
dbcsScreen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
// Insert DBCS character at pos 1
dbcsScreen.insertChar(1, '\u6771');
// Delete DBCS character at pos 1
boolean deleted = dbcsScreen.deleteChar(1);
assertTrue(deleted);
assertEquals(0, dbcsScreen.getCell(1).ec);
assertEquals(0, dbcsScreen.getCell(2).ec);
// Test orphaned SO/SI cleanup on delete
dbcsScreen.setCell(3, 0x0E); // SO
dbcsScreen.setCell(4, 0x43); // DBCS byte 1
dbcsScreen.setCell(5, 0x41); // DBCS byte 2
dbcsScreen.setCell(6, 0x0F); // SI
dbcsScreen.getCell(4).cs = ExtendedAttribute.CS_DBCS;
dbcsScreen.getCell(4).db = ExtendedAttribute.DB_LEFT;
dbcsScreen.getCell(5).cs = ExtendedAttribute.CS_DBCS;
dbcsScreen.getCell(5).db = ExtendedAttribute.DB_RIGHT;
// Deleting the DBCS character at pos 4 pulls SI (pos 6) adjacent to SO (pos 3), triggering SISO cleanup
dbcsScreen.deleteChar(4);
assertEquals(0, dbcsScreen.getCell(3).ec);
assertEquals(0, dbcsScreen.getCell(4).ec);
assertEquals(0, dbcsScreen.getCell(5).ec);
assertEquals(0, dbcsScreen.getCell(6).ec);
}
@Test
public void testEntryAssistDOCModeAndTabStops() {
screen.erase(false);
assertFalse(screen.isEntryAssistDOCmode());
assertFalse(screen.isEntryAssistWordWrap());
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
screen.setEntryAssistStartColumn(5);
screen.setEntryAssistEndColumn(75);
screen.setEntryAssistTabStops(new int[]{ 10, 20, 30, 40 });
assertTrue(screen.isEntryAssistDOCmode());
assertTrue(screen.isEntryAssistWordWrap());
assertEquals(5, screen.getEntryAssistStartColumn());
assertEquals(75, screen.getEntryAssistEndColumn());
assertArrayEquals(new int[]{ 10, 20, 30, 40 }, screen.getEntryAssistTabStops());
// Test word tab forward with tab stops
screen.setCursorPosition(0, 0);
screen.processWordTab(true);
assertEquals(10, screen.getCursorCol());
screen.processWordTab(true);
assertEquals(20, screen.getCursorCol());
// Test word tab backward
screen.processWordTab(false);
assertEquals(10, screen.getCursorCol());
}
@Test
public void testProcessDeleteWord() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
// Write "HELLO WORLD" starting at pos 1
String text = "HELLO WORLD";
for (int i = 0; i < text.length(); i++) {
screen.setChar(0, 1 + i, text.charAt(i));
}
screen.setCursorAddress(1);
screen.processDeleteWord();
// "HELLO " is deleted, "WORLD" is shifted left to pos 1
assertEquals('W', screen.getChar(0, 1));
assertEquals('O', screen.getChar(0, 2));
assertEquals('R', screen.getChar(0, 3));
assertEquals('L', screen.getChar(0, 4));
assertEquals('D', screen.getChar(0, 5));
}
@Test
public void testProcessSOSIDisplayTransformation() {
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
cp.registerDbcsPair(0x4341, '\u6771');
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
dbcsScreen.erase(false);
// Put SO (0x0E), 0x43, 0x41, SI (0x0F) at pos 0..3
dbcsScreen.setCell(0, 0x0E);
dbcsScreen.setCell(1, 0x43);
dbcsScreen.setCell(2, 0x41);
dbcsScreen.setCell(3, 0x0F);
dbcsScreen.processSOSI();
assertEquals(ExtendedAttribute.DB_SO, dbcsScreen.getCell(0).db);
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
assertEquals(ExtendedAttribute.DB_SI, dbcsScreen.getCell(3).db);
assertEquals('\u6771', dbcsScreen.getCell(1).ucs4);
assertEquals('\u6771', dbcsScreen.getCell(2).ucs4);
}
@Test
public void testAccessorsAndConvenienceMethods() {
screen.erase(false);
assertEquals(24 * 80, screen.getSize());
screen.writeChar(10, (byte) 0xC1); // 'A' in CP037
assertEquals('A', screen.getChar(0, 10));
screen.setChar(1, 5, 'Z');
assertEquals('Z', screen.getChar(1, 5));
ExtendedAttribute ea = new ExtendedAttribute();
ea.fg = 2; // RED
screen.setExtAttr(1, 5, ea);
assertEquals(2, screen.getExtAttr(1, 5).fg);
screen.setText("TESTING 123");
assertEquals("TESTING 123", screen.getString(0, 11));
assertEquals(0, screen.searchString("TESTING"));
assertEquals(-1, screen.searchString("NOTFOUND"));
}
}