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.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator); this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm); 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); this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection // Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
@@ -197,11 +198,17 @@ public class Telnet3270Client {
fsm.sendNVTString(s); fsm.sendNVTString(s);
} }
// ========== Convenience input methods ========== // ========== Convenience input methods & HoD compatibility ==========
/** Type a character at the cursor position. */ /** Type a character at the cursor position. */
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); } 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. */ /** Type a string at the cursor position. */
public void typeString(String s) { public void typeString(String s) {
for (char ch : s.toCharArray()) { for (char ch : s.toCharArray()) {
@@ -218,6 +225,7 @@ public class Telnet3270Client {
public void sendEnter() { public void sendEnter() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER); inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
} }
public void processEnter() { inputProcessor.processEnter(); }
/** Send a PF key (1-24). */ /** Send a PF key (1-24). */
public void sendPF(int number) { public void sendPF(int number) {
@@ -239,6 +247,7 @@ public class Telnet3270Client {
} }
inputProcessor.sendAid(aid); inputProcessor.sendAid(aid);
} }
public void processPF(int number) { inputProcessor.processPF(number); }
/** Send a PA key (1-3). */ /** Send a PA key (1-3). */
public void sendPA(int number) { public void sendPA(int number) {
@@ -251,44 +260,104 @@ public class Telnet3270Client {
} }
inputProcessor.sendAid(aid); inputProcessor.sendAid(aid);
} }
public void processPA(int number) { inputProcessor.processPA(number); }
/** Send Clear key. */ /** Send Clear key. */
public void sendClear() { public void sendClear() {
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR); inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
} }
public void processClear() { inputProcessor.processClear(); }
/** Move cursor up. */ /** Move cursor up. */
public void cursorUp() { inputProcessor.cursorUp(); } public void cursorUp() { inputProcessor.cursorUp(); }
public void processCursorUp() { inputProcessor.processCursorUp(); }
/** Move cursor down. */ /** Move cursor down. */
public void cursorDown() { inputProcessor.cursorDown(); } public void cursorDown() { inputProcessor.cursorDown(); }
public void processCursorDown() { inputProcessor.processCursorDown(); }
/** Move cursor left. */ /** Move cursor left. */
public void cursorLeft() { inputProcessor.cursorLeft(); } public void cursorLeft() { inputProcessor.cursorLeft(); }
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
/** Move cursor right. */ /** Move cursor right. */
public void cursorRight() { inputProcessor.cursorRight(); } public void cursorRight() { inputProcessor.cursorRight(); }
public void processCursorRight() { inputProcessor.processCursorRight(); }
/** Move cursor to home position. */ /** 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. */ /** 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. */ /** 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. */ /** 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. */ /** Erase all unprotected fields. */
public void eraseInput() { inputProcessor.eraseInput(); } public void eraseInput() { inputProcessor.eraseInput(); }
public void processEraseInput() { inputProcessor.processEraseInput(); }
/** Insert Duplicate order. */ /** Insert Duplicate order. */
public void dup() { inputProcessor.dup(); } public void dup() { inputProcessor.dup(); }
public void processDup() { inputProcessor.processDup(); }
/** Insert Field Mark order. */ /** 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. */ /** Attention key. */
public void attn() { inputProcessor.attn(); } public void attn() { inputProcessor.attn(); }
public void processAttn() { inputProcessor.processAttn(); }
/** SysReq key. */ /** SysReq key. */
public void sysReq() { inputProcessor.sysReq(); } public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */ public void processSysReq() { inputProcessor.processSysReq(); }
public void reset() { inputProcessor.reset(); }
/** 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. */ /** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); } public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
/** Trigger Light Pen selection at the specified screen address. */ 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 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() { public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return dsProcessor.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) { public char mapAPL(int ebcdicCodePoint) {
switch (ec & 0xFF) { switch (ebcdicCodePoint & 0xFF) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL) // Box-drawing line and corner characters (standard IBM 3270 GE / APL)
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─' case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│' case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
@@ -198,7 +199,14 @@ public class EbcdicTranslator {
case 0xBF: return '\u00B5'; // Micro 'µ' case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬' 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 // Handle GE (graphic escape) prefix
byte fillCs = currentCs; byte fillCs = currentCs;
char fillUcs4 = 0;
if (pos + 4 < end && fillChar == ORDER_GE) { if (pos + 4 < end && fillChar == ORDER_GE) {
fillChar = data[pos + 4] & 0xFF; fillChar = data[pos + 4] & 0xFF;
fillCs = CS_GE; fillCs = CS_GE;
fillUcs4 = (translator != null) ? translator.mapAPL(fillChar) : (char) fillChar;
pos += 5; pos += 5;
} else { } else {
fillUcs4 = (translator != null) ? translator.ebcdicToUnicode(fillChar) : (char) fillChar;
pos += 4; pos += 4;
} }
@@ -529,6 +532,7 @@ public class DataStreamProcessor {
ExtendedAttribute ea = screen.getCell(baddr); ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) fillChar; ea.ec = (byte) fillChar;
ea.ucs4 = fillUcs4;
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
@@ -581,6 +585,7 @@ public class DataStreamProcessor {
ea.fa = 0; // Destroy previous field attribute if any ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) geChar; ea.ec = (byte) geChar;
ea.cs = CS_GE; ea.cs = CS_GE;
ea.ucs4 = (translator != null) ? translator.mapAPL(geChar) : (char) geChar;
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
@@ -686,7 +691,7 @@ public class DataStreamProcessor {
// ========== Read Buffer ========== // ========== Read Buffer ==========
private void processReadBuffer() { public void processReadBuffer() {
outputPos = 0; outputPos = 0;
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
@@ -786,11 +791,21 @@ public class DataStreamProcessor {
// ========== Read Modified ========== // ========== Read Modified ==========
private void processReadModified(boolean all) { public void processReadModified(boolean all) {
if (ftDft != null && ftDft.readModified()) { if (ftDft != null && ftDft.readModified()) {
return; 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; outputPos = 0;
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0) int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
@@ -849,10 +864,11 @@ public class DataStreamProcessor {
sendOutput(); sendOutput();
} }
// ========== Write Structured Field ========== public void processWriteStructuredField(byte[] data, int offset, int length) {
if (data == null || length <= 0)
private void processWriteStructuredField(byte[] data, int offset, int length) { return;
int pos = offset + 1; // Skip WSF command byte int firstByte = data[offset] & 0xFF;
int pos = (firstByte == CMD_WSF || firstByte == SNA_CMD_WSF) ? offset + 1 : offset;
int end = offset + length; int end = offset + length;
while (pos < end) { while (pos < end) {
@@ -887,10 +903,19 @@ public class DataStreamProcessor {
case SF_ACTIVATE_PART: case SF_ACTIVATE_PART:
processActivatePartition(data, pos, fieldLen); processActivatePartition(data, pos, fieldLen);
break; 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); processOutbound3270DS(data, pos, fieldLen);
break; break;
case SF_TRANSFER_DATA: case SF_TRANSFER_DATA: // 0xD0
if (ftDft != null) { if (ftDft != null) {
ftDft.processStructuredField(data, pos, fieldLen); ftDft.processStructuredField(data, pos, fieldLen);
} else { } else {
@@ -904,95 +929,7 @@ public class DataStreamProcessor {
programSymbolManager.loadps(psData); programSymbolManager.loadps(psData);
} }
break; 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_OBJDATA: // 0x85: Graphics Object Data / GOCA
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
if (fieldLen > 3) { if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3); gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
@@ -1280,11 +1217,275 @@ public class DataStreamProcessor {
return pid >= 0 && pid <= 255; 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) { public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
if (fieldLen > 5) { if (fieldLen >= 4) {
int pid = data[offset + 3] & 0xFF; int pid = data[offset + 3] & 0xFF;
screen.setActivePartition(pid); 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); out.write(data, 0, data.length);
} }
private byte[] buildSummary() { public byte[] buildSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE; int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
@@ -307,7 +307,13 @@ public class QueryReplyBuilder {
return out.toByteArray(); 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); ByteArrayOutputStream out = new ByteArrayOutputStream(19);
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java) out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
out.write(0x00); // no special character features 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 >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low out.write(maxRows & 0xFF); // usable height low
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING) 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 >> 24) & 0xFF);
out.write((Xr_HOD >> 16) & 0xFF); out.write((Xr_HOD >> 16) & 0xFF);
out.write((Xr_HOD >> 8) & 0xFF); out.write((Xr_HOD >> 8) & 0xFF);
out.write(Xr_HOD & 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 >> 24) & 0xFF);
out.write((Yr_HOD >> 16) & 0xFF); out.write((Yr_HOD >> 16) & 0xFF);
out.write((Yr_HOD >> 8) & 0xFF); out.write((Yr_HOD >> 8) & 0xFF);
@@ -359,8 +365,14 @@ public class QueryReplyBuilder {
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
} }
private byte[] buildAlphaPartitions(int maxRows) { public byte[] buildAlphaPartitions() {
int bufSize = screen.getMaxCols() * screen.getMaxRows(); 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); ByteArrayOutputStream out = new ByteArrayOutputStream(4);
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition) out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
out.write((bufSize >> 8) & 0xFF); // total partition storage high out.write((bufSize >> 8) & 0xFF); // total partition storage high
@@ -369,7 +381,7 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
private byte[] buildCharsets() { public byte[] buildCharsets() {
int charW = getCharWidth(); int charW = getCharWidth();
int charH = getCharHeight(); int charH = getCharHeight();
@@ -429,7 +441,7 @@ public class QueryReplyBuilder {
return out.toByteArray(); 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) // Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
return new byte[] { return new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4, 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) // Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
return new byte[] { return new byte[] {
0x04, 0x00, (byte) 0xF0, 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 }; 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); ByteArrayOutputStream out = new ByteArrayOutputStream(8);
out.write(0x00); // reserved out.write(0x00); // reserved
out.write(0x00); // reserved out.write(0x00); // reserved
@@ -470,7 +486,13 @@ public class QueryReplyBuilder {
return out.toByteArray(); 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); ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters // 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 }; 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) { public byte[] buildSegment(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000") // HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
return new byte[]{ return new byte[]{
@@ -524,10 +552,20 @@ public class QueryReplyBuilder {
}; };
} }
public byte[] buildGraphics() {
return buildSegment();
}
public byte[] buildGraphics(int maxCols, int maxRows) { public byte[] buildGraphics(int maxCols, int maxRows) {
return buildSegment(maxCols, 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) { 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") // 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[]{ return new byte[]{
@@ -540,6 +578,10 @@ public class QueryReplyBuilder {
}; };
} }
public byte[] buildGImage() {
return buildProcedure();
}
public byte[] buildGImage(int maxCols, int maxRows) { public byte[] buildGImage(int maxCols, int maxRows) {
return buildProcedure(maxCols, maxRows); return buildProcedure(maxCols, maxRows);
} }
@@ -581,6 +623,16 @@ public class QueryReplyBuilder {
appendPort(out); appendPort(out);
} }
public byte[] buildPort() {
ByteArrayOutputStream out = new ByteArrayOutputStream(70);
appendPort(out);
return out.toByteArray();
}
public byte[] buildOemFormat() {
return buildPort();
}
public byte[] buildGrColor() { public byte[] buildGrColor() {
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total) // HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
ByteArrayOutputStream out = new ByteArrayOutputStream(110); ByteArrayOutputStream out = new ByteArrayOutputStream(110);
@@ -602,6 +654,10 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
public byte[] buildGraphicColor() {
return buildGrColor();
}
public byte[] buildGColor() { public byte[] buildGColor() {
return buildGrColor(); return buildGrColor();
} }
@@ -49,4 +49,23 @@ public interface ECLConstants {
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow) int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check) int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due) 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; package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
@@ -56,24 +58,34 @@ public class ECLField {
return cols > 0 ? endPos % cols : 0; 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() { public boolean isModified() {
return faIsModified(attribute & 0xFF); return faIsModified(getLiveAttribute() & 0xFF);
} }
public boolean isProtected() { public boolean isProtected() {
return faIsProtected(attribute & 0xFF); return faIsProtected(getLiveAttribute() & 0xFF);
} }
public boolean isNumeric() { public boolean isNumeric() {
return faIsNumeric(attribute & 0xFF); return faIsNumeric(getLiveAttribute() & 0xFF);
} }
public boolean isHighIntensity() { public boolean isHighIntensity() {
return faIsHigh(attribute & 0xFF); return faIsHigh(getLiveAttribute() & 0xFF);
} }
public boolean isHidden() { public boolean isHidden() {
return faIsZero(attribute & 0xFF); return faIsZero(getLiveAttribute() & 0xFF);
} }
public boolean isDisplay() { public boolean isDisplay() {
@@ -81,11 +93,11 @@ public class ECLField {
} }
public boolean isPenSelectable() { public boolean isPenSelectable() {
return faIsSelectable(attribute & 0xFF); return faIsSelectable(getLiveAttribute() & 0xFF);
} }
public short getAttribute() { 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 @Override
public String toString() { public String toString() {
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]", 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); 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. * Find field containing the given text string.
*/ */
@@ -147,6 +193,13 @@ public class ECLFieldList {
return f; 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; return null;
} }
} }
@@ -5,6 +5,7 @@ import java.util.List;
import haus.nightmare.lib3270j.input.InputProcessor; import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.telnet.TelnetFSM; import haus.nightmare.lib3270j.telnet.TelnetFSM;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; 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(); 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. * Get the current Input Inhibited code.
* Returns one of INHIBIT_* constants from ECLConstants. * Returns one of INHIBIT_* constants from ECLConstants.
@@ -81,6 +168,9 @@ public class ECLOIA implements ECLConstants {
if (isCommError()) { if (isCommError()) {
return INHIBIT_COMM_CHECK; return INHIBIT_COMM_CHECK;
} }
if (inhibitOverride != INHIBIT_NOT_INHIBITED) {
return inhibitOverride;
}
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) { if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
return INHIBIT_SYSTEM_LOCK; return INHIBIT_SYSTEM_LOCK;
} }
@@ -152,8 +152,8 @@ public class ECLPS implements ECLConstants {
} }
/** /**
* Search for a string in the presentation space. * Search for a string in the presentation space (0-based indexing).
* Returns 1-based or 0-based position, or -1 if not found. * Returns 0-based position, or -1 if not found.
*/ */
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) { public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
if (target == null || target.isEmpty() || screen == null) return -1; if (target == null || target.isEmpty() || screen == null) return -1;
@@ -174,7 +174,6 @@ public class ECLPS implements ECLConstants {
int targetLen = target.length(); int targetLen = target.length();
if (dir == SEARCH_FORWARD) { if (dir == SEARCH_FORWARD) {
// Forward search with wrapping
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
int pos = (startPos + i) % size; int pos = (startPos + i) % size;
if (matchesAt(screenText, target, pos, size)) { if (matchesAt(screenText, target, pos, size)) {
@@ -182,7 +181,6 @@ public class ECLPS implements ECLConstants {
} }
} }
} else { } else {
// Backward search with wrapping
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
int pos = (startPos - i + size) % size; int pos = (startPos - i + size) % size;
if (matchesAt(screenText, target, pos, size)) { if (matchesAt(screenText, target, pos, size)) {
@@ -193,6 +191,78 @@ public class ECLPS implements ECLConstants {
return -1; 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) { private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length(); int len = target.length();
for (int j = 0; j < len; j++) { for (int j = 0; j < len; j++) {
@@ -204,6 +274,62 @@ public class ECLPS implements ECLConstants {
return true; 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. * Paste text with line wrapping across unprotected fields.
*/ */
@@ -224,15 +350,15 @@ public class ECLPS implements ECLConstants {
int curPos = screen.getCursorAddress(); int curPos = screen.getCursorAddress();
int curCol = curPos % cols; int curCol = curPos % cols;
if (endCol > 0 && curCol >= endCol) { if (endCol > 0 && curCol >= endCol) {
// Advance to next row
int nextRow = (curPos / cols + 1) % rows; int nextRow = (curPos / cols + 1) % rows;
setCursorPos(nextRow * cols); setCursorPos(nextRow * cols);
} }
inputProcessor.typeCharacter(line.charAt(i)); if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(i));
}
charsPasted++; charsPasted++;
} }
if (l < lines.length - 1) { if (l < lines.length - 1 && inputProcessor != null) {
// Newline key between lines
inputProcessor.newline(); inputProcessor.newline();
} }
} }
@@ -247,4 +373,103 @@ public class ECLPS implements ECLConstants {
inputProcessor.sendKeys(keys); 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 static class Edge {
public final double x1, y1; public final double x1, y1;
public final double x2, y2; 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) { public Edge(double x1, double y1, double x2, double y2) {
this.x1 = x1; this.x1 = x1;
this.y1 = y1; this.y1 = y1;
this.x2 = x2; this.x2 = x2;
this.y2 = y2; 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<Edge> edges = new ArrayList<>();
private final List<double[]> subpathsX = new ArrayList<>(); private final List<double[]> subpathsX = new ArrayList<>();
private final List<double[]> subpathsY = new ArrayList<>(); private final List<double[]> subpathsY = new ArrayList<>();
public FillArea() {} 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. * Adds a single directed edge to the edge table.
*/ */
@@ -104,12 +119,47 @@ public class FillArea {
subpathsY.clear(); 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. * Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
*/ */
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern, public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, ProgramSymbolManager psm) { 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 (plane == null) return;
if (edges.isEmpty() && subpathsX.isEmpty()) return; if (edges.isEmpty() && subpathsX.isEmpty()) return;
@@ -140,7 +190,7 @@ public class FillArea {
int iMinY = Math.max(0, (int) Math.floor(minY)); int iMinY = Math.max(0, (int) Math.floor(minY));
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY)); int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
List<Double> nodeX = new ArrayList<>(); List<NodeIntersection> intersections = new ArrayList<>();
byte[] patRows = null; byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null; ProgramSymbolSet.SymbolSlot psSlot = null;
@@ -156,45 +206,84 @@ public class FillArea {
} }
for (int y = iMinY; y <= iMaxY; y++) { for (int y = iMinY; y <= iMaxY; y++) {
nodeX.clear(); intersections.clear();
double scanY = y + 0.5; double scanY = y + 0.5;
for (Edge e : edges) { for (Edge e : edges) {
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) { 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); 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 (fillRule == GocaConstants.FILL_RULE_WINDING) {
if (i + 1 >= nodeX.size()) break; // Non-Zero Winding Rule: evaluate winding count
int leftX = Math.max(0, (int) Math.round(nodeX.get(i))); int winding = 0;
int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1))); 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++) { for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) { if (psSlot != null) {
int psW = psSlot.getWidth(); int psW = psSlot.getWidth();
int psH = psSlot.getHeight(); int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0; int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0; int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData(); byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
plane.setPixel(x, y, fill); plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (bgMix == GocaConstants.MIX_OVER) {
plane.setPixel(x, y, bg); 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) { } else {
int b = patRows[y & 7] & 0xFF; // Even-Odd / Alternate Rule
if (((b >> (7 - (x & 7))) & 1) != 0) { 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); plane.setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (patRows != null) {
plane.setPixel(x, y, bg); 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_GSMS = 0x1B; // Set Marker Size
public static final int G_GSCP = 0x21; // Set Current Position public static final int G_GSCP = 0x21; // Set Current Position
public static final int G_GSAP = 0x22; // Arc Parameters 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_GSECOL = 0x26; // Set Extended Color
public static final int G_GSVW = 0x27; // Set Viewing Window public static final int G_GSVW = 0x27; // Set Viewing Window
public static final int G_GSPT = 0x28; // Set Pattern Symbol 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_XOR = 4;
public static final int MIX_UNDER = 5; 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) // Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
public static final int[] GOCA_COLORS = new int[] { public static final int[] GOCA_COLORS = new int[] {
0xFF00FF00, // 0: Default (Green) 0xFF00FF00, // 0: Default (Green)
@@ -33,6 +33,8 @@ public class GocaDecoder {
private int fillColor = GocaConstants.GOCA_COLORS[0]; private int fillColor = GocaConstants.GOCA_COLORS[0];
private int charDir = GocaConstants.CD_LR; private int charDir = GocaConstants.CD_LR;
private double charAngle = 0.0; private double charAngle = 0.0;
private double charShear = 0.0;
private double fractionalLineWidth = 1.0;
private int charWidth = 9; private int charWidth = 9;
private int charHeight = 16; private int charHeight = 16;
private int charSet = 0; private int charSet = 0;
@@ -42,11 +44,16 @@ public class GocaDecoder {
private int arcParamR = 0; private int arcParamR = 0;
private int arcParamS = 1; private int arcParamS = 1;
private boolean segChained = false;
private boolean segDynamic = false;
private boolean segVisible = true;
private ProgramSymbolManager programSymbolManager; private ProgramSymbolManager programSymbolManager;
// Area accumulation // Area accumulation
private boolean inArea = false; private boolean inArea = false;
private boolean areaDrawBoundary = true; private boolean areaDrawBoundary = true;
private int areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private boolean areaFill = true; private boolean areaFill = true;
private final List<Integer> areaPointsX = new ArrayList<>(); private final List<Integer> areaPointsX = new ArrayList<>();
private final List<Integer> areaPointsY = new ArrayList<>(); private final List<Integer> areaPointsY = new ArrayList<>();
@@ -59,6 +66,8 @@ public class GocaDecoder {
private int imgY = 0; private int imgY = 0;
private int imgWidth = 0; private int imgWidth = 0;
private int imgHeight = 0; private int imgHeight = 0;
private int imgBitDepth = GocaConstants.BPP_1;
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
private final List<Byte> imgBuffer = new ArrayList<>(); private final List<Byte> imgBuffer = new ArrayList<>();
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A) // 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() { public synchronized void resetDefaults() {
curX = 0; curX = 0;
curY = 0; curY = 0;
@@ -217,6 +309,10 @@ public class GocaDecoder {
bgColor = GocaConstants.GOCA_COLORS[8]; // Black bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID; lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL; lineWidth = GocaConstants.LW_NORMAL;
fractionalLineWidth = 1.0;
if (plane != null) {
plane.setFractionalLineWidth(1.0);
}
markerType = GocaConstants.MK_PLUS; markerType = GocaConstants.MK_PLUS;
markerSize = 5; markerSize = 5;
markerColor = curColor; markerColor = curColor;
@@ -226,14 +322,18 @@ public class GocaDecoder {
fillColor = curColor; fillColor = curColor;
charDir = GocaConstants.CD_LR; charDir = GocaConstants.CD_LR;
charAngle = 0.0; charAngle = 0.0;
charShear = 0.0;
charSet = 0; charSet = 0;
charPrecision = GocaConstants.CP_STRING; charPrecision = GocaConstants.CP_STRING;
inArea = false; inArea = false;
areaDrawBoundary = true; areaDrawBoundary = true;
areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
areaFill = true; areaFill = true;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
inImage = false; inImage = false;
imgBitDepth = GocaConstants.BPP_1;
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
imgBuffer.clear(); imgBuffer.clear();
} }
@@ -270,6 +370,10 @@ public class GocaDecoder {
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; 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.) // All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
if (order < 0x20) { if (order < 0x20) {
return 2; return 2;
@@ -497,7 +601,15 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; 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; idx += orderLen;
break; break;
} }
@@ -558,6 +670,17 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GSCR: { // Set Character Shear (0x35) 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; idx += orderLen;
break; break;
} }
@@ -581,8 +704,16 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; 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 0x06:
case 0x11:
case GocaConstants.G_GSLT: { // Set Line Type (0x18) case GocaConstants.G_GSLT: { // Set Line Type (0x18)
lineType = inputData[idx + 1] & 0xFF; lineType = inputData[idx + 1] & 0xFF;
idx += orderLen; idx += orderLen;
@@ -661,8 +792,9 @@ public class GocaDecoder {
case GocaConstants.G_GBAR: { // Begin Area (0x68) case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0; boolean drawBoundary = (flags & 0x80) != 0;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary)); int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
beginArea(drawBoundary); logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
beginArea(drawBoundary, fillRule);
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -770,7 +902,18 @@ public class GocaDecoder {
int y = readCoord(inputData, idx + 4); int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6); int w = readCoord(inputData, idx + 6);
int h = readCoord(inputData, idx + 8); 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; idx += orderLen;
break; break;
@@ -887,16 +1030,21 @@ public class GocaDecoder {
} }
private void beginArea(boolean drawBoundary) { private void beginArea(boolean drawBoundary) {
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
}
private void beginArea(boolean drawBoundary, int fillRule) {
this.inArea = true; this.inArea = true;
this.areaDrawBoundary = drawBoundary; this.areaDrawBoundary = drawBoundary;
this.areaFillRule = fillRule;
this.areaFill = true; this.areaFill = true;
this.fillColor = this.curColor; this.fillColor = this.curColor;
this.areaPointsX.clear(); this.areaPointsX.clear();
this.areaPointsY.clear(); this.areaPointsY.clear();
this.areaPolygons.clear(); this.areaPolygons.clear();
this.currentPolyPts = 0; this.currentPolyPts = 0;
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d", logger.info(String.format("GOCA beginArea: drawBoundary=%b fillRule=%d fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillColor, patternSet, pattern)); drawBoundary, fillRule, fillColor, patternSet, pattern));
} }
private void endArea() { private void endArea() {
@@ -928,11 +1076,11 @@ public class GocaDecoder {
polyCounts[i] = areaPolygons.get(i); 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", 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)); n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix, areaFillRule));
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet, 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; inArea = false;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
@@ -971,11 +1119,17 @@ public class GocaDecoder {
} }
private void beginImage(int x, int y, int w, int h) { 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.inImage = true;
this.imgX = x; this.imgX = x;
this.imgY = y; this.imgY = y;
this.imgWidth = w; this.imgWidth = w;
this.imgHeight = h; this.imgHeight = h;
this.imgBitDepth = bitDepth;
this.imgCompression = compression;
this.imgBuffer.clear(); this.imgBuffer.clear();
} }
@@ -991,7 +1145,7 @@ public class GocaDecoder {
int px = plane.mapX(imgX); int px = plane.mapX(imgX);
int py = plane.mapY(imgY); 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; inImage = false;
imgBuffer.clear(); imgBuffer.clear();
} }
@@ -1328,7 +1482,7 @@ public class GocaDecoder {
} }
String text = new String(chars); String text = new String(chars);
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text, 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) { } else if (charSet != 0 && programSymbolManager != null) {
for (int i = 0; i < textLen; i++) { for (int i = 0; i < textLen; i++) {
int code = data[pos + i] & 0xFF; int code = data[pos + i] & 0xFF;
@@ -1366,7 +1520,7 @@ public class GocaDecoder {
} }
String text = new String(chars); String text = new String(chars);
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text, plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
curColor, cw, ch, charDir, charAngle); curColor, cw, ch, charDir, charAngle, charShear);
} }
switch (charDir) { switch (charDir) {
@@ -1395,11 +1549,15 @@ public class GocaDecoder {
* Draws a transformed character string (matching HODDecoder.drawGCS). * 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) { 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 (plane != null && text != null && !text.isEmpty()) {
if (charPrecision == GocaConstants.CP_STROKE) { 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 { } 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. * 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) { 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; if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return;
char[] chars = new char[length]; char[] chars = new char[length];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + 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; 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.canvasWidth = w;
this.canvasHeight = h; this.canvasHeight = h;
this.rgbBuffer = newBuffer; this.rgbBuffer = newBuffer;
updateViewingWindowPixels();
} }
public synchronized void clear() { public synchronized void clear() {
@@ -170,6 +171,59 @@ public class GraphicsPlane {
return rgbBuffer; 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() { public int getCanvasWidth() {
return canvasWidth; return canvasWidth;
} }
@@ -313,6 +367,11 @@ public class GraphicsPlane {
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending. * Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
*/ */
public synchronized void setPixel(int x, int y, int colorArgb) { 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) { if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
int srcA = (colorArgb >>> 24) & 0xFF; int srcA = (colorArgb >>> 24) & 0xFF;
if (srcA == 0) return; if (srcA == 0) return;
@@ -447,11 +506,15 @@ public class GraphicsPlane {
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) { private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
if (brightness <= 0.0) return; 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, y, colorRgb, 1.0);
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness)); for (int dx = -extra; dx <= extra; dx++) {
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness)); for (int dy = -extra; dy <= extra; dy++) {
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7)); if (dx == 0 && dy == 0) continue;
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
}
}
} else { } else {
// Perceptual gamma correction for crisp contrast on dark backgrounds // Perceptual gamma correction for crisp contrast on dark backgrounds
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15); 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); 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. * 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, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) { 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; if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0]; int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb; 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) && boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0); (bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
// Find polygon vertical bounds across all points
int minY = py[0]; int minY = py[0];
int maxY = py[0]; int maxY = py[0];
for (int i = 1; i < numPoints; i++) { for (int i = 1; i < numPoints; i++) {
@@ -676,7 +758,7 @@ public class GraphicsPlane {
minY = Math.max(0, minY); minY = Math.max(0, minY);
maxY = Math.min(canvasHeight - 1, maxY); maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>(); List<NodeInter> nodeIntersections = new ArrayList<>();
byte[] patRows = null; byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null; ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && programSymbolManager != null) { if (patternSet >= 0x40 && programSymbolManager != null) {
@@ -691,7 +773,7 @@ public class GraphicsPlane {
} }
for (int y = minY; y <= maxY; y++) { for (int y = minY; y <= maxY; y++) {
nodeX.clear(); nodeIntersections.clear();
int offset = 0; int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1; int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) { for (int p = 0; p < polyCount; p++) {
@@ -705,7 +787,8 @@ public class GraphicsPlane {
int xj = px[offset + j]; int xj = px[offset + j];
if ((yi < y && yj >= y) || (yj < y && yi >= y)) { if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi)); 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; j = i;
} }
@@ -713,35 +796,72 @@ public class GraphicsPlane {
offset += pLen; offset += pLen;
} }
Collections.sort(nodeX); Collections.sort(nodeIntersections);
for (int i = 0; i < nodeX.size(); i += 2) { if (fillRule == GocaConstants.FILL_RULE_WINDING) {
if (i + 1 >= nodeX.size()) break; int winding = 0;
int leftX = Math.max(0, nodeX.get(i)); for (int i = 0; i < nodeIntersections.size() - 1; i++) {
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); 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++) { for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) { if (psSlot != null) {
int psW = psSlot.getWidth(); int psW = psSlot.getWidth();
int psH = psSlot.getHeight(); int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0; int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0; int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData(); byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { } else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg); 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 { } else {
int b = patRows[y & 7] & 0xFF; for (int i = 0; i < nodeIntersections.size(); i += 2) {
if (((b >> (7 - (x & 7))) & 1) != 0) { 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); setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background) } else {
setPixel(x, y, bg); 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, public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) { 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 (text == null || text.isEmpty()) return;
if (textRenderer != null) { if (textRenderer != null) {
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle); textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
hasContent = true; hasContent = true;
updateCount++; updateCount++;
} else { } 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, public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) { 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, public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) { 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; if (text == null || text.isEmpty()) return;
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0]; int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
@@ -884,25 +1019,37 @@ public class GraphicsPlane {
double ch = cellHeight > 0 ? cellHeight : 20.0; double ch = cellHeight > 0 ? cellHeight : 20.0;
double curX = x; double curX = x;
double curY = y; double curY = y;
if (dir == GocaConstants.CD_TB) {
curY += ch; double radAngle = Math.toRadians(angle);
} else if (dir == GocaConstants.CD_RL) { double cosA = Math.cos(radAngle);
curX -= cw; 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++) { for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i); char c = text.charAt(i);
drawVssChar(curX, curY, c, color, cw, ch); drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle);
switch (dir) { if (angle != 0.0) {
case GocaConstants.CD_TB: curY += ch; break; curX += cw * cosA;
case GocaConstants.CD_RL: curX -= cw; break; curY += cw * sinA;
case GocaConstants.CD_BT: curY -= ch; break; } else {
case GocaConstants.CD_LR: switch (dir) {
case GocaConstants.CD_DEFAULT: case GocaConstants.CD_TB: curY += ch; break;
default: case GocaConstants.CD_RL: curX -= cw; break;
curX += cw; case GocaConstants.CD_BT: curY -= ch; break;
break; case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
}
} }
} }
hasContent = true; hasContent = true;
@@ -911,10 +1058,19 @@ public class GraphicsPlane {
public synchronized void drawVectorText(int x, int y, String text, int colorArgb, public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) { 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) { 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; int code = (int) c;
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) { if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
return; return;
@@ -924,6 +1080,11 @@ public class GraphicsPlane {
return; 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; int ptr = offset;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) { while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF; int order = VectorSymbolData.vss_data[ptr] & 0xFF;
@@ -941,8 +1102,18 @@ public class GraphicsPlane {
for (int p = 0; p < numPoints; p++) { 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 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); 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]); ipx[p] = (int) Math.round(px[p]);
ipy[p] = (int) Math.round(py[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) { 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; if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0]; int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) { byte[] rawData = imageData;
int rowOffset = row * bytesPerRow; if (compressionMode == GocaConstants.IMG_RLE) {
for (int col = 0; col < width; col++) { rawData = decompressGocaRle(imageData, width, height, bitDepth);
int byteIdx = rowOffset + (col / 8); }
if (byteIdx < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0; int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
if (bit) {
setPixel(x + col, y + row, fgColor); 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; hasContent = true;
updateCount++; 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 remaining = data.length - offset;
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint; int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
int bytesPerSymbol; int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (loadFormat == 1) { if (sliceBytes <= 0) sliceBytes = 18;
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
} else {
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
}
if (bytesPerSymbol <= 0) { int bytesPerSymbol;
bytesPerSymbol = 18; if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
bytesPerSymbol = sliceBytes * 3;
} else {
bytesPerSymbol = sliceBytes;
} }
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) { 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)); System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
} }
if (loadFormat == 1) { if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane); // 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 { } 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)); 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.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.telnet.TelnetFSM; import haus.nightmare.lib3270j.telnet.TelnetFSM;
import haus.nightmare.lib3270j.protocol.TelnetConstants; 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 static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -22,6 +24,7 @@ public class InputProcessor {
private final ScreenBuffer screen; private final ScreenBuffer screen;
private final EbcdicTranslator translator; private final EbcdicTranslator translator;
private final TelnetFSM fsm; private final TelnetFSM fsm;
private ECLOIA oia;
private int lastAid = AID_NO; private int lastAid = AID_NO;
private boolean keyboardLocked; private boolean keyboardLocked;
private boolean insertMode; private boolean insertMode;
@@ -32,6 +35,14 @@ public class InputProcessor {
this.fsm = fsm; this.fsm = fsm;
} }
public void setOIA(ECLOIA oia) {
this.oia = oia;
}
public ECLOIA getOIA() {
return oia;
}
public TelnetFSM getFsm() { public TelnetFSM getFsm() {
return fsm; return fsm;
} }
@@ -102,7 +113,9 @@ public class InputProcessor {
* Enter a character at the current cursor position. * Enter a character at the current cursor position.
*/ */
public void typeCharacter(char ch) { public void typeCharacter(char ch) {
if (keyboardLocked) return; if (keyboardLocked) {
return;
}
if (isNvtMode()) { if (isNvtMode()) {
try { try {
@@ -118,18 +131,37 @@ public class InputProcessor {
int baddr = screen.getCursorAddress(); int baddr = screen.getCursorAddress();
baddr = ((baddr % size) + size) % size; baddr = ((baddr % size) + size) % size;
// Check if cursor is at a field attribute or in a protected field if (screen.isFormatted()) {
ExtendedAttribute ea = screen.getCell(baddr); // Check if cursor is at a field attribute
if (ea.isFieldAttribute()) { ExtendedAttribute ea = screen.getCell(baddr);
// Move to next position if (ea.isFieldAttribute()) {
baddr = (baddr + 1) % size; // Move to next position
ea = screen.getCell(baddr); baddr = (baddr + 1) % size;
} ea = screen.getCell(baddr);
}
byte faVal = screen.getFieldAttributeAt(baddr); byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) { if (faIsProtected(faVal & 0xFF)) {
// Protected field can't type here // Protected field cannot type here
return; 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 // Translate character to EBCDIC
@@ -138,7 +170,6 @@ public class InputProcessor {
if (insertMode) { if (insertMode) {
// Insert mode: shift characters right from cursor to end of field // Insert mode: shift characters right from cursor to end of field
// Find end of field
int endAddr = baddr; int endAddr = baddr;
int count = 0; int count = 0;
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) { while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
@@ -146,9 +177,13 @@ public class InputProcessor {
count++; count++;
if (endAddr == baddr) break; // wrapped around (unformatted) if (endAddr == baddr) break; // wrapped around (unformatted)
} }
// Check if last position is non-null (field overflow) // Check if last position is non-null/non-blank (field overflow)
if (screen.getCell(endAddr).ec != 0) { ExtendedAttribute eaEnd = screen.getCell(endAddr);
// Field overflow can't insert if (eaEnd.ec != 0 && (eaEnd.ec & 0xFF) != 0x40 && eaEnd.ucs4 != 0 && eaEnd.ucs4 != ' ') {
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
}
setKeyboardLocked(true);
return; return;
} }
// Shift right from endAddr-1 down to baddr // Shift right from endAddr-1 down to baddr
@@ -164,48 +199,291 @@ public class InputProcessor {
} }
// Write character preserve existing fg/bg/gr/cs attributes // Write character preserve existing fg/bg/gr/cs attributes
// so the character inherits the field's color scheme ExtendedAttribute ea = screen.getCell(baddr);
ea = screen.getCell(baddr);
ea.ec = (byte) ebc; ea.ec = (byte) ebc;
ea.ucs4 = ch; ea.ucs4 = ch;
// Set MDT on field attribute // Set MDT on field attribute
int faAddr = screen.findFieldAttribute(baddr); if (screen.isFormatted()) {
if (faAddr >= 0) { int faAddr = screen.findFieldAttribute(baddr);
ExtendedAttribute faEa = screen.getCell(faAddr); if (faAddr >= 0) {
faEa.fa = (byte) (faEa.fa | FA_MODIFY); ExtendedAttribute faEa = screen.getCell(faAddr);
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
}
} }
// Advance cursor // Advance cursor and handle auto-skip
int startAdvance = baddr; if (screen.isFormatted()) {
baddr = (baddr + 1) % size; int nextAddr = screen.incrementAddress(baddr);
int advCount = 0; if (screen.getCell(nextAddr).isFieldAttribute()) {
// Skip over field attributes safely byte nextFa = screen.getCell(nextAddr).fa;
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance && advCount < size) { if (faIsSkip(nextFa & 0xFF)) {
baddr = (baddr + 1) % size; // Auto-skip field (Protected + Numeric): jump to next unprotected field
advCount++; 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.markAllChanged();
screen.updateDisplaySnapshot(); 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). * Send an AID key (Enter, PF1-24, PA1-3, Clear).
*/ */
public void sendAid(int aidCode) { 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) { if (aidCode == AID_SYSREQ) {
sysReq(); if (fsm != null && fsm.isTn3270eNegotiated()) {
return; fsm.handleSysReq();
return;
}
} }
if (keyboardLocked && aidCode != AID_CLEAR) {
System.err.println("Keyboard locked, dropping AID");
return;
}
lastAid = aidCode;
setKeyboardLocked(true);
if (aidCode == AID_CLEAR) { if (aidCode == AID_CLEAR) {
screen.clear(); screen.clear();
@@ -213,22 +491,27 @@ public class InputProcessor {
if (graphicsPlane != null) { if (graphicsPlane != null) {
graphicsPlane.clear(); graphicsPlane.clear();
} }
// Send just the AID
byte[] data = new byte[] { (byte) aidCode }; byte[] data = new byte[] { (byte) aidCode };
sendAidResponse(data); sendAidResponse(data);
return; 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()) { if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
// SSCP-LU Mode (e.g. VM line mode / CP console before conmode 3270): // SSCP-LU Mode: send raw EBCDIC line data without AID or 3270 cursor address
// Per RFC 2355: Send raw EBCDIC line data without AID or 3270 cursor address.
int cols = screen.getCols(); int cols = screen.getCols();
int curAddr = screen.getCursorAddress(); int curAddr = screen.getCursorAddress();
int row = curAddr / cols; int row = curAddr / cols;
int rowStart = row * cols; int rowStart = row * cols;
int rowEnd = rowStart + cols; int rowEnd = rowStart + cols;
// Find last non-null, non-blank character in the current row
int lastChar = rowStart - 1; int lastChar = rowStart - 1;
for (int i = rowEnd - 1; i >= rowStart; i--) { for (int i = rowEnd - 1; i >= rowStart; i--) {
int ec = screen.getCell(i).ec & 0xFF; int ec = screen.getCell(i).ec & 0xFF;
@@ -246,7 +529,6 @@ public class InputProcessor {
fsm.sendSscpLuData(sscpData.toByteArray()); fsm.sendSscpLuData(sscpData.toByteArray());
// Advance cursor to beginning of next row
int nextRowAddr = ((row + 1) % screen.getRows()) * cols; int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
screen.setCursorAddress(nextRowAddr); screen.setCursorAddress(nextRowAddr);
screen.markAllChanged(); screen.markAllChanged();
@@ -255,111 +537,14 @@ public class InputProcessor {
return; return;
} }
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) { byte[] payload = buildReadModifiedInboundData(aidCode, false);
// PA keys: send AID + optional PID + cursor address only (no modified data) sendAidResponse(payload);
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());
} }
protected void sendAidResponse(byte[] data) { protected void sendAidResponse(byte[] data) {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) { if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
fsm.sendSscpLuData(data); fsm.sendSscpLuData(data);
} else if (fsm != null) { } else if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
fsm.send3270Data(data); fsm.send3270Data(data);
} }
} }
@@ -758,29 +943,244 @@ public class InputProcessor {
screen.updateDisplaySnapshot(); screen.updateDisplaySnapshot();
} }
/** Attention key (sends Telnet IP). */ /** Attention key (sends Telnet Interrupt Process + IAC EOR break signal). */
public void attn() { public void processAttn() {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) { if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
byte[] ip = new byte[] { (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP }; byte[] ipEor = new byte[] {
fsm.sendRecord(ip); (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP,
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
};
fsm.sendRecord(ipEor);
} }
} }
/** SysReq key. */ public void attn() {
public void sysReq() { processAttn();
}
/** SysReq key (generates TN3270E SYSREQ signal or 3270 Test Request AID). */
public void processSysReq() {
if (fsm != null && fsm.isTn3270eNegotiated()) { if (fsm != null && fsm.isTn3270eNegotiated()) {
fsm.handleSysReq(); fsm.handleSysReq();
} else { } else {
byte[] payload = buildReadModifiedInboundData(AID_SYSREQ, false);
sendAidResponse(payload);
reset(); reset();
} }
} }
/** Reset (unlock keyboard, cancel insert mode). */ public void sysReq() {
processSysReq();
}
/** Reset (unlock keyboard, cancel insert mode, clear OIA error states). */
public void reset() { public void reset() {
setKeyboardLocked(false); setKeyboardLocked(false);
insertMode = 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 ========== // ========== File Transfer Support ==========
/** /**
@@ -1080,9 +1480,27 @@ public class InputProcessor {
case "wordbacktab": case "wordbacktab":
processWordTab(false); processWordTab(false);
break; break;
case "wordleft":
processWordLeft();
break;
case "wordright":
processWordRight();
break;
case "fieldend":
case "end":
processFieldEnd();
break;
case "deleteword": case "deleteword":
processDeleteWord(); processDeleteWord();
break; break;
case "cursel":
case "cursorselect":
case "select":
cursorSelect();
break;
case "lightpen":
processLightPen();
break;
default: default:
if (token.startsWith("pf")) { if (token.startsWith("pf")) {
try { 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) { public void processWordTab(boolean forward) {
if (keyboardLocked || screen == null) return; if (keyboardLocked || screen == null) return;
int size = screen.getRows() * screen.getCols(); screen.processWordTab(forward);
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();
} }
/** /**
@@ -1166,23 +1552,7 @@ public class InputProcessor {
*/ */
public void processDeleteWord() { public void processDeleteWord() {
if (keyboardLocked || screen == null) return; if (keyboardLocked || screen == null) return;
int cur = screen.getCursorAddress(); screen.processDeleteWord();
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++;
}
} }
private boolean isWhitespaceOrNull(int pos) { 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_DESTROY_PART = 0x0d;
public static final int SF_ACTIVATE_PART = 0x0e; public static final int SF_ACTIVATE_PART = 0x0e;
public static final int SF_MODIFY_PART = 0x0f; 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_OUTBOUND_DS = 0x40;
public static final int SF_TRANSFER_DATA = 0xd0; public static final int SF_TRANSFER_DATA = 0xd0;
// ========== Query Reply codes ========== // ========== Query Reply codes ==========
public static final int QR_SUMMARY = 0x80; // Summary public static final int QR_SUMMARY = 0x80; // Summary
public static final int QR_USABLE_AREA = 0x81; // Usable Area 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_IMAGE = 0x82; // Image (non-GOCA)
public static final int QR_TEXT_PART = 0x83; // Text Partitions 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_ALPHA_PART = 0x84; // Alphanumeric Partitions
public static final int QR_CHARSETS = 0x85; // Character Sets 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_COLOR = 0x86; // Color Table / Alphanumeric Color
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
public static final int QR_REPLY_MODES = 0x88; // Reply Modes public static final int QR_REPLY_MODES = 0x88; // Reply Modes
public static final int QR_OUTLINING = 0x8c; // Field Outlining 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_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia 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_DDM = 0x95; // Distributed Data Management
public static final int QR_AUXDA = 0x99; // Auxiliary Devices public static final int QR_AUXDA = 0x99; // Auxiliary Devices
public static final int QR_FILE = 0x9f; // File Transfer public static final int QR_FILE = 0x9f; // File Transfer
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer) 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_RPQNAMES = 0xa1; // RPQ Names (legacy)
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes 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_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency 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_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_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_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA) 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_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev) 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_DEV = 0xb2; // Legacy alias for QR_LINETYPE
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format) public static final int QR_AUX_DEVICE = 0xb2; // Aux Device alias
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR public static final int QR_OEM_FORMAT = 0xb3; // OEM Format alias
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported) 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 ========== // ========== Screen model sizes ==========
public static final int MODEL_2_ROWS = 24; 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). */ /** Background color (0x00 for default, or 0xf0-0xff for explicit). */
public byte bg; 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. * Graphics rendition bits.
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08 * 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.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator; 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.*; import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
@@ -39,6 +42,14 @@ public class ScreenBuffer {
private byte defaultGr = 0x00; private byte defaultGr = 0x00;
private byte defaultCs = 0x00; private byte defaultCs = 0x00;
private byte defaultIc = 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 EbcdicTranslator translator;
private final Object renderLock = new Object(); private final Object renderLock = new Object();
@@ -216,6 +227,10 @@ public class ScreenBuffer {
this.explicitPartitionActive = (pid != 0); this.explicitPartitionActive = (pid != 0);
} }
public synchronized PartitionInfo getPartition(int pid) {
return partitions.get(pid);
}
public synchronized void eraseReset(boolean alt) { public synchronized void eraseReset(boolean alt) {
partitions.clear(); partitions.clear();
this.activePartition = 0; this.activePartition = 0;
@@ -269,7 +284,7 @@ public class ScreenBuffer {
public void setFieldAttribute(int pos, byte fa) { public void setFieldAttribute(int pos, byte fa) {
ExtendedAttribute ea = buffer[pos]; ExtendedAttribute ea = buffer[pos];
ea.clear(); ea.clear();
ea.fa = fa; ea.fa = (fa != 0) ? fa : (byte) FA_PRINTABLE;
if (!formatted) { if (!formatted) {
System.err.println("SCREEN BECAME FORMATTED at pos " + pos); System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
} }
@@ -471,39 +486,10 @@ public class ScreenBuffer {
} }
private char getAplGraphic(int ec) { private char getAplGraphic(int ec) {
switch (ec) { if (translator != null) {
// Box-drawing line and corner characters (standard IBM 3270 GE / APL) return translator.mapAPL(ec);
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);
} }
return (char) (ec & 0xFF);
} }
/** /**
@@ -567,4 +553,511 @@ public class ScreenBuffer {
if (baddr < 0 || baddr >= maxRows * maxCols) return 0; if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
return buffer[baddr].fa; 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(56, sf2.length);
assertEquals(0x00, sf2[0]); assertEquals(0x00, sf2[0]);
assertEquals(0x34, sf2[1]); 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) { 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"));
}
}