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