This commit is contained in:
@@ -54,6 +54,7 @@ public class Telnet3270Client {
|
|||||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||||
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
|
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
|
||||||
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
|
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
|
||||||
|
this.inputProcessor.setOIA(oia);
|
||||||
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
|
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
|
||||||
|
|
||||||
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||||
@@ -197,11 +198,17 @@ public class Telnet3270Client {
|
|||||||
fsm.sendNVTString(s);
|
fsm.sendNVTString(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Convenience input methods ==========
|
// ========== Convenience input methods & HoD compatibility ==========
|
||||||
|
|
||||||
/** Type a character at the cursor position. */
|
/** Type a character at the cursor position. */
|
||||||
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
|
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
|
||||||
|
|
||||||
|
/** Process a character at the current cursor position. */
|
||||||
|
public void processChar(char ch) { inputProcessor.processChar(ch); }
|
||||||
|
|
||||||
|
/** Process a character with explicit position and insert mode flag. */
|
||||||
|
public void processChar(char ch, int pos, boolean insert) { inputProcessor.processChar(ch, pos, insert); }
|
||||||
|
|
||||||
/** Type a string at the cursor position. */
|
/** Type a string at the cursor position. */
|
||||||
public void typeString(String s) {
|
public void typeString(String s) {
|
||||||
for (char ch : s.toCharArray()) {
|
for (char ch : s.toCharArray()) {
|
||||||
@@ -218,6 +225,7 @@ public class Telnet3270Client {
|
|||||||
public void sendEnter() {
|
public void sendEnter() {
|
||||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||||
}
|
}
|
||||||
|
public void processEnter() { inputProcessor.processEnter(); }
|
||||||
|
|
||||||
/** Send a PF key (1-24). */
|
/** Send a PF key (1-24). */
|
||||||
public void sendPF(int number) {
|
public void sendPF(int number) {
|
||||||
@@ -239,6 +247,7 @@ public class Telnet3270Client {
|
|||||||
}
|
}
|
||||||
inputProcessor.sendAid(aid);
|
inputProcessor.sendAid(aid);
|
||||||
}
|
}
|
||||||
|
public void processPF(int number) { inputProcessor.processPF(number); }
|
||||||
|
|
||||||
/** Send a PA key (1-3). */
|
/** Send a PA key (1-3). */
|
||||||
public void sendPA(int number) {
|
public void sendPA(int number) {
|
||||||
@@ -251,44 +260,104 @@ public class Telnet3270Client {
|
|||||||
}
|
}
|
||||||
inputProcessor.sendAid(aid);
|
inputProcessor.sendAid(aid);
|
||||||
}
|
}
|
||||||
|
public void processPA(int number) { inputProcessor.processPA(number); }
|
||||||
|
|
||||||
/** Send Clear key. */
|
/** Send Clear key. */
|
||||||
public void sendClear() {
|
public void sendClear() {
|
||||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||||
}
|
}
|
||||||
|
public void processClear() { inputProcessor.processClear(); }
|
||||||
|
|
||||||
/** Move cursor up. */
|
/** Move cursor up. */
|
||||||
public void cursorUp() { inputProcessor.cursorUp(); }
|
public void cursorUp() { inputProcessor.cursorUp(); }
|
||||||
|
public void processCursorUp() { inputProcessor.processCursorUp(); }
|
||||||
|
|
||||||
/** Move cursor down. */
|
/** Move cursor down. */
|
||||||
public void cursorDown() { inputProcessor.cursorDown(); }
|
public void cursorDown() { inputProcessor.cursorDown(); }
|
||||||
|
public void processCursorDown() { inputProcessor.processCursorDown(); }
|
||||||
|
|
||||||
/** Move cursor left. */
|
/** Move cursor left. */
|
||||||
public void cursorLeft() { inputProcessor.cursorLeft(); }
|
public void cursorLeft() { inputProcessor.cursorLeft(); }
|
||||||
|
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
|
||||||
|
|
||||||
/** Move cursor right. */
|
/** Move cursor right. */
|
||||||
public void cursorRight() { inputProcessor.cursorRight(); }
|
public void cursorRight() { inputProcessor.cursorRight(); }
|
||||||
|
public void processCursorRight() { inputProcessor.processCursorRight(); }
|
||||||
|
|
||||||
/** Move cursor to home position. */
|
/** Move cursor to home position. */
|
||||||
public void cursorHome() { inputProcessor.cursorHome(); }
|
public void cursorHome() { inputProcessor.cursorHome(); }
|
||||||
|
public void processHome() { inputProcessor.processHome(); }
|
||||||
|
|
||||||
/** Tab to next unprotected field. */
|
/** Tab to next unprotected field. */
|
||||||
public void tab() { inputProcessor.tab(); }
|
public void tab() { inputProcessor.tab(); }
|
||||||
|
public void processTab() { inputProcessor.processTab(); }
|
||||||
|
|
||||||
/** Back-tab to previous unprotected field. */
|
/** Back-tab to previous unprotected field. */
|
||||||
public void backTab() { inputProcessor.backTab(); }
|
public void backTab() { inputProcessor.backTab(); }
|
||||||
|
public void processBackTab() { inputProcessor.processBackTab(); }
|
||||||
|
|
||||||
/** Move cursor to next line. */
|
/** Move cursor to next line. */
|
||||||
public void newline() { inputProcessor.newline(); }
|
public void newline() { inputProcessor.newline(); }
|
||||||
|
public void processNewline() { inputProcessor.processNewline(); }
|
||||||
|
|
||||||
|
/** Delete character under cursor. */
|
||||||
|
public void deleteChar() { inputProcessor.deleteChar(); }
|
||||||
|
public void processDelete() { inputProcessor.processDelete(); }
|
||||||
|
|
||||||
|
/** Backspace character before cursor. */
|
||||||
|
public void backspace() { inputProcessor.backspace(); }
|
||||||
|
public void processBackspace() { inputProcessor.processBackspace(); }
|
||||||
|
|
||||||
|
/** Erase to end of field. */
|
||||||
|
public void eraseEof() { inputProcessor.eraseEof(); }
|
||||||
|
public void processEraseEOF() { inputProcessor.processEraseEOF(); }
|
||||||
|
|
||||||
/** Erase all unprotected fields. */
|
/** Erase all unprotected fields. */
|
||||||
public void eraseInput() { inputProcessor.eraseInput(); }
|
public void eraseInput() { inputProcessor.eraseInput(); }
|
||||||
|
public void processEraseInput() { inputProcessor.processEraseInput(); }
|
||||||
|
|
||||||
/** Insert Duplicate order. */
|
/** Insert Duplicate order. */
|
||||||
public void dup() { inputProcessor.dup(); }
|
public void dup() { inputProcessor.dup(); }
|
||||||
|
public void processDup() { inputProcessor.processDup(); }
|
||||||
|
|
||||||
/** Insert Field Mark order. */
|
/** Insert Field Mark order. */
|
||||||
public void fieldMark() { inputProcessor.fieldMark(); }
|
public void fieldMark() { inputProcessor.fieldMark(); }
|
||||||
|
public void processFieldMark() { inputProcessor.processFieldMark(); }
|
||||||
|
|
||||||
|
/** Toggle Insert Mode. */
|
||||||
|
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
|
||||||
|
public void processToggleInsert() { inputProcessor.processToggleInsert(); }
|
||||||
|
|
||||||
|
/** Move word left. */
|
||||||
|
public void processWordLeft() { inputProcessor.processWordLeft(); }
|
||||||
|
|
||||||
|
/** Move word right. */
|
||||||
|
public void processWordRight() { inputProcessor.processWordRight(); }
|
||||||
|
|
||||||
|
/** Move to field end. */
|
||||||
|
public void processFieldEnd() { inputProcessor.processFieldEnd(); }
|
||||||
|
|
||||||
/** Attention key. */
|
/** Attention key. */
|
||||||
public void attn() { inputProcessor.attn(); }
|
public void attn() { inputProcessor.attn(); }
|
||||||
|
public void processAttn() { inputProcessor.processAttn(); }
|
||||||
|
|
||||||
/** SysReq key. */
|
/** SysReq key. */
|
||||||
public void sysReq() { inputProcessor.sysReq(); }
|
public void sysReq() { inputProcessor.sysReq(); }
|
||||||
/** Reset (unlock keyboard). */
|
public void processSysReq() { inputProcessor.processSysReq(); }
|
||||||
public void reset() { inputProcessor.reset(); }
|
|
||||||
|
/** Reset (unlock keyboard, reset OIA). */
|
||||||
|
public void reset() { inputProcessor.reset(); }
|
||||||
|
public void processReset() { inputProcessor.processReset(); }
|
||||||
|
|
||||||
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
|
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
|
||||||
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
|
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
|
||||||
/** Trigger Light Pen selection at the specified screen address. */
|
public boolean processCurSel() { return inputProcessor.processCurSel(); }
|
||||||
|
public boolean processCursorSelect() { return inputProcessor.processCursorSelect(); }
|
||||||
|
|
||||||
|
/** Trigger Light Pen selection at the specified screen address or cursor position. */
|
||||||
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
|
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
|
||||||
|
public boolean processLightPen() { return inputProcessor.processLightPen(); }
|
||||||
|
public boolean processLightPen(int addr) { return inputProcessor.processLightPen(addr); }
|
||||||
|
|
||||||
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||||
return dsProcessor.getProgramSymbolManager();
|
return dsProcessor.getProgramSymbolManager();
|
||||||
|
|||||||
@@ -164,10 +164,11 @@ public class EbcdicTranslator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
|
* Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
|
||||||
|
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification.
|
||||||
*/
|
*/
|
||||||
public char getAplGraphic(int ec) {
|
public char mapAPL(int ebcdicCodePoint) {
|
||||||
switch (ec & 0xFF) {
|
switch (ebcdicCodePoint & 0xFF) {
|
||||||
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
|
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
|
||||||
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
|
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
|
||||||
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
|
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
|
||||||
@@ -198,7 +199,14 @@ public class EbcdicTranslator {
|
|||||||
case 0xBF: return '\u00B5'; // Micro 'µ'
|
case 0xBF: return '\u00B5'; // Micro 'µ'
|
||||||
case 0x5F: return '\u00AC'; // Not sign '¬'
|
case 0x5F: return '\u00AC'; // Not sign '¬'
|
||||||
|
|
||||||
default: return ebcdicToUnicode(ec & 0xFF);
|
default: return ebcdicToUnicode(ebcdicCodePoint & 0xFF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
|
||||||
|
*/
|
||||||
|
public char getAplGraphic(int ec) {
|
||||||
|
return mapAPL(ec);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+299
-98
@@ -516,11 +516,14 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
// Handle GE (graphic escape) prefix
|
// Handle GE (graphic escape) prefix
|
||||||
byte fillCs = currentCs;
|
byte fillCs = currentCs;
|
||||||
|
char fillUcs4 = 0;
|
||||||
if (pos + 4 < end && fillChar == ORDER_GE) {
|
if (pos + 4 < end && fillChar == ORDER_GE) {
|
||||||
fillChar = data[pos + 4] & 0xFF;
|
fillChar = data[pos + 4] & 0xFF;
|
||||||
fillCs = CS_GE;
|
fillCs = CS_GE;
|
||||||
|
fillUcs4 = (translator != null) ? translator.mapAPL(fillChar) : (char) fillChar;
|
||||||
pos += 5;
|
pos += 5;
|
||||||
} else {
|
} else {
|
||||||
|
fillUcs4 = (translator != null) ? translator.ebcdicToUnicode(fillChar) : (char) fillChar;
|
||||||
pos += 4;
|
pos += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,6 +532,7 @@ public class DataStreamProcessor {
|
|||||||
ExtendedAttribute ea = screen.getCell(baddr);
|
ExtendedAttribute ea = screen.getCell(baddr);
|
||||||
ea.fa = 0; // Destroy previous field attribute if any
|
ea.fa = 0; // Destroy previous field attribute if any
|
||||||
ea.ec = (byte) fillChar;
|
ea.ec = (byte) fillChar;
|
||||||
|
ea.ucs4 = fillUcs4;
|
||||||
ea.fg = currentFg;
|
ea.fg = currentFg;
|
||||||
ea.bg = currentBg;
|
ea.bg = currentBg;
|
||||||
ea.gr = (byte) currentGr;
|
ea.gr = (byte) currentGr;
|
||||||
@@ -581,6 +585,7 @@ public class DataStreamProcessor {
|
|||||||
ea.fa = 0; // Destroy previous field attribute if any
|
ea.fa = 0; // Destroy previous field attribute if any
|
||||||
ea.ec = (byte) geChar;
|
ea.ec = (byte) geChar;
|
||||||
ea.cs = CS_GE;
|
ea.cs = CS_GE;
|
||||||
|
ea.ucs4 = (translator != null) ? translator.mapAPL(geChar) : (char) geChar;
|
||||||
ea.fg = currentFg;
|
ea.fg = currentFg;
|
||||||
ea.bg = currentBg;
|
ea.bg = currentBg;
|
||||||
ea.gr = (byte) currentGr;
|
ea.gr = (byte) currentGr;
|
||||||
@@ -686,7 +691,7 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
// ========== Read Buffer ==========
|
// ========== Read Buffer ==========
|
||||||
|
|
||||||
private void processReadBuffer() {
|
public void processReadBuffer() {
|
||||||
outputPos = 0;
|
outputPos = 0;
|
||||||
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
int size = screen.getRows() * screen.getCols();
|
||||||
@@ -786,11 +791,21 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
// ========== Read Modified ==========
|
// ========== Read Modified ==========
|
||||||
|
|
||||||
private void processReadModified(boolean all) {
|
public void processReadModified(boolean all) {
|
||||||
if (ftDft != null && ftDft.readModified()) {
|
if (ftDft != null && ftDft.readModified()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inputProcessor != null) {
|
||||||
|
int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO;
|
||||||
|
inputProcessor.setLastAid(AID_NO);
|
||||||
|
byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all);
|
||||||
|
if (outputSender != null) {
|
||||||
|
outputSender.send3270Data(data);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
outputPos = 0;
|
outputPos = 0;
|
||||||
|
|
||||||
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
||||||
@@ -849,10 +864,11 @@ public class DataStreamProcessor {
|
|||||||
sendOutput();
|
sendOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Write Structured Field ==========
|
public void processWriteStructuredField(byte[] data, int offset, int length) {
|
||||||
|
if (data == null || length <= 0)
|
||||||
private void processWriteStructuredField(byte[] data, int offset, int length) {
|
return;
|
||||||
int pos = offset + 1; // Skip WSF command byte
|
int firstByte = data[offset] & 0xFF;
|
||||||
|
int pos = (firstByte == CMD_WSF || firstByte == SNA_CMD_WSF) ? offset + 1 : offset;
|
||||||
int end = offset + length;
|
int end = offset + length;
|
||||||
|
|
||||||
while (pos < end) {
|
while (pos < end) {
|
||||||
@@ -887,10 +903,19 @@ public class DataStreamProcessor {
|
|||||||
case SF_ACTIVATE_PART:
|
case SF_ACTIVATE_PART:
|
||||||
processActivatePartition(data, pos, fieldLen);
|
processActivatePartition(data, pos, fieldLen);
|
||||||
break;
|
break;
|
||||||
case SF_OUTBOUND_DS:
|
case SF_SET_WINDOW: // 0x0F: Set Window / Modify Partition / 3270 Graphics Viewport
|
||||||
|
processSFSetWindow(data, pos, fieldLen);
|
||||||
|
break;
|
||||||
|
case SF_3270_GRAPHICS: // 0x20: 3270 Graphics / Object Control
|
||||||
|
processSFObjectControl(data, pos, fieldLen);
|
||||||
|
break;
|
||||||
|
case SF_DOCUMENT_DATA: // 0x24: Document Data / embedded SCS / Object Control
|
||||||
|
processSFDocumentData(data, pos, fieldLen);
|
||||||
|
break;
|
||||||
|
case SF_OUTBOUND_DS: // 0x40
|
||||||
processOutbound3270DS(data, pos, fieldLen);
|
processOutbound3270DS(data, pos, fieldLen);
|
||||||
break;
|
break;
|
||||||
case SF_TRANSFER_DATA:
|
case SF_TRANSFER_DATA: // 0xD0
|
||||||
if (ftDft != null) {
|
if (ftDft != null) {
|
||||||
ftDft.processStructuredField(data, pos, fieldLen);
|
ftDft.processStructuredField(data, pos, fieldLen);
|
||||||
} else {
|
} else {
|
||||||
@@ -904,95 +929,7 @@ public class DataStreamProcessor {
|
|||||||
programSymbolManager.loadps(psData);
|
programSymbolManager.loadps(psData);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
|
||||||
if (fieldLen >= 4) {
|
|
||||||
int sfSubId = data[pos + 3] & 0xFF;
|
|
||||||
switch (sfSubId) {
|
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
|
||||||
if (fieldLen > 4) {
|
|
||||||
byte[] psData = new byte[fieldLen - 4];
|
|
||||||
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
|
||||||
programSymbolManager.loadps(psData);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
|
||||||
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
|
|
||||||
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
|
|
||||||
gocaDecoder.setGraphicsCursorActive(true);
|
|
||||||
notifyScreenUpdated();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
|
|
||||||
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
|
||||||
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
|
||||||
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
|
|
||||||
|
|
||||||
StringBuilder hexDump = new StringBuilder();
|
|
||||||
for (int i = 0; i < Math.min(32, fieldLen); i++) {
|
|
||||||
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
|
|
||||||
}
|
|
||||||
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
|
|
||||||
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
|
|
||||||
|
|
||||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
|
||||||
int targetW = screen.getCols() * 9;
|
|
||||||
int targetH = screen.getRows() * 16;
|
|
||||||
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
|
||||||
graphicsPlane.resize(targetW, targetH);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags == 0x80) { // SPAN_FIRST
|
|
||||||
gocaAccumulator.reset();
|
|
||||||
currentGocaSubtype = sfSubId;
|
|
||||||
if (orderLen > 0) {
|
|
||||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
|
||||||
}
|
|
||||||
} else if (flags == 0x00) { // SPAN_MIDDLE
|
|
||||||
if (orderLen > 0) {
|
|
||||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
|
||||||
}
|
|
||||||
} else if (flags == 0x40) { // SPAN_LAST
|
|
||||||
if (orderLen > 0) {
|
|
||||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
|
||||||
}
|
|
||||||
byte[] fullStream = gocaAccumulator.toByteArray();
|
|
||||||
gocaAccumulator.reset();
|
|
||||||
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
|
|
||||||
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
|
||||||
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
|
||||||
} else {
|
|
||||||
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
|
||||||
}
|
|
||||||
notifyScreenUpdated();
|
|
||||||
} else { // SPAN_ONLY (0xC0)
|
|
||||||
gocaAccumulator.reset();
|
|
||||||
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
|
|
||||||
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
|
||||||
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
|
||||||
} else {
|
|
||||||
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
|
||||||
}
|
|
||||||
notifyScreenUpdated();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
|
|
||||||
if (fieldLen > 3) {
|
|
||||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
|
||||||
gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3);
|
|
||||||
notifyScreenUpdated();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
|
|
||||||
if (fieldLen > 3) {
|
if (fieldLen > 3) {
|
||||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||||
@@ -1280,11 +1217,275 @@ public class DataStreamProcessor {
|
|||||||
return pid >= 0 && pid <= 255;
|
return pid >= 0 && pid <= 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Missing Structured Field Handlers (Phase 2) ==========
|
||||||
|
|
||||||
|
public void processSFSetWindow(byte[] data, int offset, int fieldLen) {
|
||||||
|
if (fieldLen >= 4) {
|
||||||
|
int sfSubId = data[offset + 3] & 0xFF;
|
||||||
|
switch (sfSubId) {
|
||||||
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
||||||
|
if (fieldLen > 4) {
|
||||||
|
byte[] psData = new byte[fieldLen - 4];
|
||||||
|
System.arraycopy(data, offset + 4, psData, 0, psData.length);
|
||||||
|
programSymbolManager.loadps(psData);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
||||||
|
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
|
||||||
|
gocaDecoder.setGraphicsCursorActive(true);
|
||||||
|
notifyScreenUpdated();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
||||||
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
|
||||||
|
int flags = (fieldLen >= 6) ? (data[offset + 5] & 0xC0) : 0xC0;
|
||||||
|
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
|
||||||
|
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
|
||||||
|
|
||||||
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
|
int targetW = screen.getCols() * 9;
|
||||||
|
int targetH = screen.getRows() * 16;
|
||||||
|
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||||
|
graphicsPlane.resize(targetW, targetH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags == 0x80) { // SPAN_FIRST
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
currentGocaSubtype = sfSubId;
|
||||||
|
if (orderLen > 0) {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
}
|
||||||
|
} else if (flags == 0x00) { // SPAN_MIDDLE
|
||||||
|
if (orderLen > 0) {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
}
|
||||||
|
} else if (flags == 0x40) { // SPAN_LAST
|
||||||
|
if (orderLen > 0) {
|
||||||
|
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||||
|
}
|
||||||
|
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
|
||||||
|
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||||
|
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||||
|
} else {
|
||||||
|
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
||||||
|
}
|
||||||
|
notifyScreenUpdated();
|
||||||
|
} else { // SPAN_ONLY (0xC0)
|
||||||
|
gocaAccumulator.reset();
|
||||||
|
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
|
||||||
|
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||||
|
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||||
|
} else {
|
||||||
|
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
||||||
|
}
|
||||||
|
notifyScreenUpdated();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if (fieldLen >= 11) {
|
||||||
|
int xMin = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
|
||||||
|
int yMin = ((data[offset + 5] & 0xFF) << 8) | (data[offset + 6] & 0xFF);
|
||||||
|
int xMax = ((data[offset + 7] & 0xFF) << 8) | (data[offset + 8] & 0xFF);
|
||||||
|
int yMax = ((data[offset + 9] & 0xFF) << 8) | (data[offset + 10] & 0xFF);
|
||||||
|
graphicsPlane.setViewingWindow(xMin, yMin, xMax, yMax);
|
||||||
|
}
|
||||||
|
log.fine("SF 0x0F Set Window processed (len=" + fieldLen + ")");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSFSetWindow(byte[] sf) {
|
||||||
|
if (sf != null && sf.length >= 3) {
|
||||||
|
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processSFSetWindow(sf, off, sf.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSFObjectControl(byte[] data, int offset, int fieldLen) {
|
||||||
|
if (fieldLen > 3) {
|
||||||
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
|
gocaDecoder.decodeStream(data, offset + 3, fieldLen - 3);
|
||||||
|
notifyScreenUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSFObjectControl(byte[] sf) {
|
||||||
|
if (sf != null && sf.length >= 3) {
|
||||||
|
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processSFObjectControl(sf, off, sf.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSFDocumentData(byte[] data, int offset, int fieldLen) {
|
||||||
|
if (fieldLen > 3) {
|
||||||
|
if (embeddedScsProcessor != null) {
|
||||||
|
embeddedScsProcessor.processHostData(data, offset + 3, fieldLen - 3);
|
||||||
|
}
|
||||||
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
|
gocaDecoder.processProcedureOrders(data, offset + 3, fieldLen - 3);
|
||||||
|
notifyScreenUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSFDocumentData(byte[] sf) {
|
||||||
|
if (sf != null && sf.length >= 3) {
|
||||||
|
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processSFDocumentData(sf, off, sf.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
|
public void processOutbound3270DS(byte[] data, int offset, int fieldLen) {
|
||||||
if (fieldLen > 5) {
|
if (fieldLen >= 4) {
|
||||||
int pid = data[offset + 3] & 0xFF;
|
int pid = data[offset + 3] & 0xFF;
|
||||||
screen.setActivePartition(pid);
|
screen.setActivePartition(pid);
|
||||||
processRecord(data, offset + 4, fieldLen - 4, false);
|
if (fieldLen > 4) {
|
||||||
|
processRecord(data, offset + 4, fieldLen - 4, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processOutbound3270DS(byte[] sf) {
|
||||||
|
if (sf != null && sf.length >= 4) {
|
||||||
|
int off = ((sf[0] & 0xFF) == CMD_WSF || (sf[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processOutbound3270DS(sf, off, sf.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Convenience Overloads ==========
|
||||||
|
|
||||||
|
public void processRecord(byte[] data) {
|
||||||
|
if (data != null && data.length > 0) {
|
||||||
|
processRecord(data, 0, data.length, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processWrite(byte[] data) {
|
||||||
|
if (data != null && data.length > 0) {
|
||||||
|
processRecord(data, 0, data.length, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processEraseWrite(byte[] data) {
|
||||||
|
if (data != null && data.length > 0) {
|
||||||
|
int oldRows = screen.getRows();
|
||||||
|
int oldCols = screen.getCols();
|
||||||
|
screen.erase(false);
|
||||||
|
graphicsPlane.clear();
|
||||||
|
if (gocaDecoder != null) {
|
||||||
|
gocaDecoder.setGraphicsCursorActive(false);
|
||||||
|
}
|
||||||
|
processWrite(data, 0, data.length, true);
|
||||||
|
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||||
|
notifyScreenSizeChanged();
|
||||||
|
}
|
||||||
|
screen.translateToUnicode();
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processEraseWriteAlternate(byte[] data) {
|
||||||
|
if (data != null && data.length > 0) {
|
||||||
|
int oldRows = screen.getRows();
|
||||||
|
int oldCols = screen.getCols();
|
||||||
|
screen.erase(true);
|
||||||
|
graphicsPlane.clear();
|
||||||
|
if (gocaDecoder != null) {
|
||||||
|
gocaDecoder.setGraphicsCursorActive(false);
|
||||||
|
}
|
||||||
|
processWrite(data, 0, data.length, true);
|
||||||
|
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||||
|
notifyScreenSizeChanged();
|
||||||
|
}
|
||||||
|
screen.translateToUnicode();
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processEraseAllUnprotected() {
|
||||||
|
synchronized (screen.getRenderLock()) {
|
||||||
|
screen.eraseAllUnprotected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processReadModified() {
|
||||||
|
processReadModified(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processReadModifiedAll() {
|
||||||
|
processReadModified(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processWriteStructuredField(byte[] data) {
|
||||||
|
if (data != null && data.length > 0) {
|
||||||
|
processWriteStructuredField(data, 0, data.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSFReadPartition(byte[] data) {
|
||||||
|
if (data != null && data.length >= 3) {
|
||||||
|
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processSFReadPartition(data, off, data.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSFReadPartitionQuery(byte[] data) {
|
||||||
|
sendAllQueryReplies();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSFReadPartitionQueryList(byte[] data) {
|
||||||
|
if (data != null && data.length >= 6) {
|
||||||
|
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
int qlStart = off + 6;
|
||||||
|
if (data.length > qlStart) {
|
||||||
|
byte[] codes = new byte[data.length - qlStart];
|
||||||
|
System.arraycopy(data, qlStart, codes, 0, codes.length);
|
||||||
|
sendRequestedQueryReplies(codes);
|
||||||
|
} else {
|
||||||
|
sendAllQueryReplies();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendAllQueryReplies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processSetReplyMode(byte[] data) {
|
||||||
|
if (data != null && data.length >= 3) {
|
||||||
|
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processSetReplyMode(data, off, data.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processCreatePartition(byte[] data) {
|
||||||
|
if (data != null && data.length >= 3) {
|
||||||
|
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processCreatePartition(data, off, data.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processDestroyPartition(byte[] data) {
|
||||||
|
if (data != null && data.length >= 3) {
|
||||||
|
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processDestroyPartition(data, off, data.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processActivatePartition(byte[] data) {
|
||||||
|
if (data != null && data.length >= 3) {
|
||||||
|
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processActivatePartition(data, off, data.length - off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processEraseReset(byte[] data) {
|
||||||
|
if (data != null && data.length >= 3) {
|
||||||
|
int off = ((data[0] & 0xFF) == CMD_WSF || (data[0] & 0xFF) == SNA_CMD_WSF) ? 1 : 0;
|
||||||
|
processEraseReset(data, off, data.length - off);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ public class QueryReplyBuilder {
|
|||||||
out.write(data, 0, data.length);
|
out.write(data, 0, data.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildSummary() {
|
public byte[] buildSummary() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
||||||
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||||
@@ -307,7 +307,13 @@ public class QueryReplyBuilder {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
public byte[] buildUsableArea() {
|
||||||
|
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||||
|
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||||
|
return buildUsableArea(maxCols, maxRows, maxCols * maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
|
||||||
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
|
out.write(graphicsMode.isVectorGraphicsEnabled() ? 0x03 : 0x01); // 12/14-bit addressing + graphics flag (matching HOD DS3270.java)
|
||||||
out.write(0x00); // no special character features
|
out.write(0x00); // no special character features
|
||||||
@@ -316,12 +322,12 @@ public class QueryReplyBuilder {
|
|||||||
out.write((maxRows >> 8) & 0xFF); // usable height high
|
out.write((maxRows >> 8) & 0xFF); // usable height high
|
||||||
out.write(maxRows & 0xFF); // usable height low
|
out.write(maxRows & 0xFF); // usable height low
|
||||||
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
|
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
|
||||||
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
|
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
|
||||||
out.write((Xr_HOD >> 24) & 0xFF);
|
out.write((Xr_HOD >> 24) & 0xFF);
|
||||||
out.write((Xr_HOD >> 16) & 0xFF);
|
out.write((Xr_HOD >> 16) & 0xFF);
|
||||||
out.write((Xr_HOD >> 8) & 0xFF);
|
out.write((Xr_HOD >> 8) & 0xFF);
|
||||||
out.write(Xr_HOD & 0xFF);
|
out.write(Xr_HOD & 0xFF);
|
||||||
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING
|
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
|
||||||
out.write((Yr_HOD >> 24) & 0xFF);
|
out.write((Yr_HOD >> 24) & 0xFF);
|
||||||
out.write((Yr_HOD >> 16) & 0xFF);
|
out.write((Yr_HOD >> 16) & 0xFF);
|
||||||
out.write((Yr_HOD >> 8) & 0xFF);
|
out.write((Yr_HOD >> 8) & 0xFF);
|
||||||
@@ -359,8 +365,14 @@ public class QueryReplyBuilder {
|
|||||||
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildAlphaPartitions(int maxRows) {
|
public byte[] buildAlphaPartitions() {
|
||||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||||
|
return buildAlphaPartitions(maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildAlphaPartitions(int maxRows) {
|
||||||
|
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||||
|
int bufSize = maxCols * maxRows;
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||||
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
|
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
|
||||||
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
||||||
@@ -369,7 +381,7 @@ public class QueryReplyBuilder {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildCharsets() {
|
public byte[] buildCharsets() {
|
||||||
int charW = getCharWidth();
|
int charW = getCharWidth();
|
||||||
int charH = getCharHeight();
|
int charH = getCharHeight();
|
||||||
|
|
||||||
@@ -429,7 +441,7 @@ public class QueryReplyBuilder {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildColor() {
|
public byte[] buildColor() {
|
||||||
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
|
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
|
||||||
return new byte[] {
|
return new byte[] {
|
||||||
0x00, 0x08, 0x00, (byte) 0xF4,
|
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||||
@@ -443,7 +455,7 @@ public class QueryReplyBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildHighlighting() {
|
public byte[] buildHighlighting() {
|
||||||
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
|
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
|
||||||
return new byte[] {
|
return new byte[] {
|
||||||
0x04, 0x00, (byte) 0xF0,
|
0x04, 0x00, (byte) 0xF0,
|
||||||
@@ -453,11 +465,15 @@ public class QueryReplyBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildReplyModes() {
|
public byte[] buildReplyModes() {
|
||||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildDdm(int bufferSize) {
|
public byte[] buildDdm() {
|
||||||
|
return buildDdm(4096);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildDdm(int bufferSize) {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
|
||||||
out.write(0x00); // reserved
|
out.write(0x00); // reserved
|
||||||
out.write(0x00); // reserved
|
out.write(0x00); // reserved
|
||||||
@@ -470,7 +486,13 @@ public class QueryReplyBuilder {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
public byte[] buildImplicitPartition() {
|
||||||
|
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||||
|
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||||
|
return buildImplicitPartition(maxCols, maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||||
// Implicit partition sizes, 2 self-defining parameters
|
// Implicit partition sizes, 2 self-defining parameters
|
||||||
|
|
||||||
@@ -514,6 +536,12 @@ public class QueryReplyBuilder {
|
|||||||
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] buildSegment() {
|
||||||
|
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||||
|
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||||
|
return buildSegment(maxCols, maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] buildSegment(int maxCols, int maxRows) {
|
public byte[] buildSegment(int maxCols, int maxRows) {
|
||||||
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
|
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
@@ -524,10 +552,20 @@ public class QueryReplyBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] buildGraphics() {
|
||||||
|
return buildSegment();
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] buildGraphics(int maxCols, int maxRows) {
|
public byte[] buildGraphics(int maxCols, int maxRows) {
|
||||||
return buildSegment(maxCols, maxRows);
|
return buildSegment(maxCols, maxRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] buildProcedure() {
|
||||||
|
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||||
|
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||||
|
return buildProcedure(maxCols, maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] buildProcedure(int maxCols, int maxRows) {
|
public byte[] buildProcedure(int maxCols, int maxRows) {
|
||||||
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
|
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
@@ -540,6 +578,10 @@ public class QueryReplyBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] buildGImage() {
|
||||||
|
return buildProcedure();
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] buildGImage(int maxCols, int maxRows) {
|
public byte[] buildGImage(int maxCols, int maxRows) {
|
||||||
return buildProcedure(maxCols, maxRows);
|
return buildProcedure(maxCols, maxRows);
|
||||||
}
|
}
|
||||||
@@ -581,6 +623,16 @@ public class QueryReplyBuilder {
|
|||||||
appendPort(out);
|
appendPort(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] buildPort() {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(70);
|
||||||
|
appendPort(out);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] buildOemFormat() {
|
||||||
|
return buildPort();
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] buildGrColor() {
|
public byte[] buildGrColor() {
|
||||||
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
|
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
||||||
@@ -602,6 +654,10 @@ public class QueryReplyBuilder {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] buildGraphicColor() {
|
||||||
|
return buildGrColor();
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] buildGColor() {
|
public byte[] buildGColor() {
|
||||||
return buildGrColor();
|
return buildGrColor();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,4 +49,23 @@ public interface ECLConstants {
|
|||||||
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
|
int INHIBIT_OVERFLOW = 4; // X > (Insert mode buffer overflow)
|
||||||
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
|
int INHIBIT_COMM_CHECK = 5; // X COMM (Communication or socket check)
|
||||||
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
|
int INHIBIT_OPERATOR_DUE = 6; // X OP (Operator Intervention Due)
|
||||||
|
|
||||||
|
// Alphanumeric entry types (ECLOIA.getAlphanumericType())
|
||||||
|
int TYPE_ALPHANUMERIC = 0;
|
||||||
|
int TYPE_NUMERIC = 1;
|
||||||
|
int TYPE_DBCS = 2;
|
||||||
|
int ALPHANUMERIC_NORMAL = 0;
|
||||||
|
int ALPHANUMERIC_NUMERIC = 1;
|
||||||
|
int ALPHANUMERIC_DBCS = 2;
|
||||||
|
|
||||||
|
// Status condition flags
|
||||||
|
int STATUS_READY = 0;
|
||||||
|
int STATUS_X_SYSTEM = 1;
|
||||||
|
int STATUS_X_NUM = 2;
|
||||||
|
int STATUS_X_PROT = 3;
|
||||||
|
int STATUS_X_WAIT = 4;
|
||||||
|
int STATUS_X_INSERT = 5;
|
||||||
|
int STATUS_X_COMM = 6;
|
||||||
|
int STATUS_X_OVERFLOW = 7;
|
||||||
|
int STATUS_X_OP = 8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package haus.nightmare.lib3270j.ecl;
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,24 +58,34 @@ public class ECLField {
|
|||||||
return cols > 0 ? endPos % cols : 0;
|
return cols > 0 ? endPos % cols : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte getLiveAttribute() {
|
||||||
|
if (ps != null && ps.getScreenBuffer() != null) {
|
||||||
|
ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos);
|
||||||
|
if (cell.isFieldAttribute()) {
|
||||||
|
return cell.fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isModified() {
|
public boolean isModified() {
|
||||||
return faIsModified(attribute & 0xFF);
|
return faIsModified(getLiveAttribute() & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isProtected() {
|
public boolean isProtected() {
|
||||||
return faIsProtected(attribute & 0xFF);
|
return faIsProtected(getLiveAttribute() & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isNumeric() {
|
public boolean isNumeric() {
|
||||||
return faIsNumeric(attribute & 0xFF);
|
return faIsNumeric(getLiveAttribute() & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isHighIntensity() {
|
public boolean isHighIntensity() {
|
||||||
return faIsHigh(attribute & 0xFF);
|
return faIsHigh(getLiveAttribute() & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isHidden() {
|
public boolean isHidden() {
|
||||||
return faIsZero(attribute & 0xFF);
|
return faIsZero(getLiveAttribute() & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDisplay() {
|
public boolean isDisplay() {
|
||||||
@@ -81,11 +93,11 @@ public class ECLField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPenSelectable() {
|
public boolean isPenSelectable() {
|
||||||
return faIsSelectable(attribute & 0xFF);
|
return faIsSelectable(getLiveAttribute() & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public short getAttribute() {
|
public short getAttribute() {
|
||||||
return (short) (attribute & 0xFF);
|
return (short) (getLiveAttribute() & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,6 +152,80 @@ public class ECLField {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Return true if this field wraps from bottom of screen to top. */
|
||||||
|
public boolean isWrapped() {
|
||||||
|
return startPos > endPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Last data buffer address (same as getEnd). */
|
||||||
|
public int getDataEnd() {
|
||||||
|
return endPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the specified buffer address is contained within this field (including its FA).
|
||||||
|
*/
|
||||||
|
public boolean contains(int pos) {
|
||||||
|
if (ps == null) return false;
|
||||||
|
int size = ps.getSize();
|
||||||
|
if (size <= 0) return false;
|
||||||
|
pos = ((pos % size) + size) % size;
|
||||||
|
|
||||||
|
if (startPos <= endPos) {
|
||||||
|
return pos >= startPos && pos <= endPos;
|
||||||
|
} else {
|
||||||
|
// Wrapped field across screen boundary
|
||||||
|
return pos >= startPos || pos <= endPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the specified row and column is contained within this field.
|
||||||
|
*/
|
||||||
|
public boolean contains(int row, int col) {
|
||||||
|
if (ps == null) return false;
|
||||||
|
int cols = ps.getCols();
|
||||||
|
return contains(row * cols + col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Modified Data Tag (MDT) for this field.
|
||||||
|
*/
|
||||||
|
public void setModified(boolean modified) {
|
||||||
|
if (ps != null && ps.getScreenBuffer() != null) {
|
||||||
|
ExtendedAttribute cell = ps.getScreenBuffer().getCell(startPos);
|
||||||
|
if (cell.isFieldAttribute()) {
|
||||||
|
if (modified) {
|
||||||
|
cell.fa = (byte) (cell.fa | FA_MODIFY);
|
||||||
|
} else {
|
||||||
|
cell.fa = (byte) (cell.fa & ~FA_MODIFY);
|
||||||
|
}
|
||||||
|
ps.getScreenBuffer().markAllChanged();
|
||||||
|
ps.getScreenBuffer().updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase all character data within this field to nulls.
|
||||||
|
*/
|
||||||
|
public void erase() {
|
||||||
|
if (isProtected() || length <= 0 || ps == null || ps.getScreenBuffer() == null) return;
|
||||||
|
ScreenBuffer sb = ps.getScreenBuffer();
|
||||||
|
int size = sb.getRows() * sb.getCols();
|
||||||
|
if (size <= 0) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int addr = (dataStart + i) % size;
|
||||||
|
ExtendedAttribute ea = sb.getCell(addr);
|
||||||
|
ea.ec = 0;
|
||||||
|
ea.ucs4 = 0;
|
||||||
|
}
|
||||||
|
setModified(false);
|
||||||
|
sb.markAllChanged();
|
||||||
|
sb.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]",
|
return String.format("ECLField[start=%d, end=%d, len=%d, prot=%b, mod=%b, text=\"%s\"]",
|
||||||
|
|||||||
@@ -123,6 +123,52 @@ public class ECLFieldList {
|
|||||||
return findField(row * cols + col);
|
return findField(row * cols + col);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the field preceding the given field in the field list.
|
||||||
|
*/
|
||||||
|
public synchronized ECLField getPreviousField(ECLField next) {
|
||||||
|
if (next == null || fields.isEmpty()) return null;
|
||||||
|
int idx = fields.indexOf(next);
|
||||||
|
if (idx > 0) {
|
||||||
|
return fields.get(idx - 1);
|
||||||
|
} else if (idx == 0) {
|
||||||
|
return fields.get(fields.size() - 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the field at the given buffer position (alias for findField).
|
||||||
|
*/
|
||||||
|
public ECLField findFieldAt(int pos) {
|
||||||
|
return findField(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the field at the given row and column.
|
||||||
|
*/
|
||||||
|
public ECLField findFieldAt(int row, int col) {
|
||||||
|
return findField(row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the field preceding the one at the given buffer position.
|
||||||
|
*/
|
||||||
|
public synchronized ECLField findPrevField(int pos) {
|
||||||
|
ECLField curr = findField(pos);
|
||||||
|
if (curr == null) return null;
|
||||||
|
return getPreviousField(curr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the field succeeding the one at the given buffer position.
|
||||||
|
*/
|
||||||
|
public synchronized ECLField findNextField(int pos) {
|
||||||
|
ECLField curr = findField(pos);
|
||||||
|
if (curr == null) return null;
|
||||||
|
return getNextField(curr);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find field containing the given text string.
|
* Find field containing the given text string.
|
||||||
*/
|
*/
|
||||||
@@ -147,6 +193,13 @@ public class ECLFieldList {
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Wrap around search to beginning of field list
|
||||||
|
for (int i = 0; i < startIdx; i++) {
|
||||||
|
ECLField f = fields.get(i);
|
||||||
|
if (f.getText().contains(text)) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
@@ -73,6 +74,92 @@ public class ECLOIA implements ECLConstants {
|
|||||||
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
|
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int inhibitOverride = INHIBIT_NOT_INHIBITED;
|
||||||
|
|
||||||
|
public void setInputInhibited(int reason) {
|
||||||
|
if (this.inhibitOverride != reason) {
|
||||||
|
this.inhibitOverride = reason;
|
||||||
|
notifyOIAChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the alphanumeric character entry type allowed at current cursor position.
|
||||||
|
* Returns TYPE_ALPHANUMERIC (0), TYPE_NUMERIC (1), or TYPE_DBCS (2).
|
||||||
|
*/
|
||||||
|
public int getAlphanumericType() {
|
||||||
|
if (screen == null || !screen.isFormatted()) {
|
||||||
|
return TYPE_ALPHANUMERIC;
|
||||||
|
}
|
||||||
|
int cur = screen.getCursorAddress();
|
||||||
|
ExtendedAttribute ea = screen.getCell(cur);
|
||||||
|
if (ea != null && (ea.cs == ExtendedAttribute.CS_DBCS || ea.db != 0)) {
|
||||||
|
return TYPE_DBCS;
|
||||||
|
}
|
||||||
|
byte fa = screen.getFieldAttributeAt(cur);
|
||||||
|
if (faIsNumeric(fa & 0xFF)) {
|
||||||
|
return TYPE_NUMERIC;
|
||||||
|
}
|
||||||
|
return TYPE_ALPHANUMERIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlphanumericTypeString() {
|
||||||
|
switch (getAlphanumericType()) {
|
||||||
|
case TYPE_NUMERIC: return "N";
|
||||||
|
case TYPE_DBCS: return "D";
|
||||||
|
case TYPE_ALPHANUMERIC:
|
||||||
|
default: return "A";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isXSystem() {
|
||||||
|
return getInputInhibited() == INHIBIT_SYSTEM_LOCK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isXProt() {
|
||||||
|
return getInputInhibited() == INHIBIT_PROTECTED_FIELD;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isXNum() {
|
||||||
|
return getInputInhibited() == INHIBIT_NUMERIC_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isXWait() {
|
||||||
|
return isXSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isXInsert() {
|
||||||
|
return isInsertMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isXComm() {
|
||||||
|
return isCommError() || getInputInhibited() == INHIBIT_COMM_CHECK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isXOverflow() {
|
||||||
|
return getInputInhibited() == INHIBIT_OVERFLOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isXOperatorDue() {
|
||||||
|
return getInputInhibited() == INHIBIT_OPERATOR_DUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatusString() {
|
||||||
|
if (isCommError()) return "X-COMM";
|
||||||
|
int inhibit = getInputInhibited();
|
||||||
|
switch (inhibit) {
|
||||||
|
case INHIBIT_SYSTEM_LOCK: return "X-SYSTEM";
|
||||||
|
case INHIBIT_NUMERIC_ONLY: return "X-NUM";
|
||||||
|
case INHIBIT_PROTECTED_FIELD: return "X-PROT";
|
||||||
|
case INHIBIT_OVERFLOW: return "X-OVERFLOW";
|
||||||
|
case INHIBIT_COMM_CHECK: return "X-COMM";
|
||||||
|
case INHIBIT_OPERATOR_DUE: return "X-OP";
|
||||||
|
default:
|
||||||
|
if (isInsertMode()) return "X-INSERT";
|
||||||
|
return "READY";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current Input Inhibited code.
|
* Get the current Input Inhibited code.
|
||||||
* Returns one of INHIBIT_* constants from ECLConstants.
|
* Returns one of INHIBIT_* constants from ECLConstants.
|
||||||
@@ -81,6 +168,9 @@ public class ECLOIA implements ECLConstants {
|
|||||||
if (isCommError()) {
|
if (isCommError()) {
|
||||||
return INHIBIT_COMM_CHECK;
|
return INHIBIT_COMM_CHECK;
|
||||||
}
|
}
|
||||||
|
if (inhibitOverride != INHIBIT_NOT_INHIBITED) {
|
||||||
|
return inhibitOverride;
|
||||||
|
}
|
||||||
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
|
if (inputProcessor != null && inputProcessor.isKeyboardLocked()) {
|
||||||
return INHIBIT_SYSTEM_LOCK;
|
return INHIBIT_SYSTEM_LOCK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ public class ECLPS implements ECLConstants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for a string in the presentation space.
|
* Search for a string in the presentation space (0-based indexing).
|
||||||
* Returns 1-based or 0-based position, or -1 if not found.
|
* Returns 0-based position, or -1 if not found.
|
||||||
*/
|
*/
|
||||||
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
|
public synchronized int searchString(String target, int startRow, int startCol, int dir, boolean ignoreCase) {
|
||||||
if (target == null || target.isEmpty() || screen == null) return -1;
|
if (target == null || target.isEmpty() || screen == null) return -1;
|
||||||
@@ -174,7 +174,6 @@ public class ECLPS implements ECLConstants {
|
|||||||
|
|
||||||
int targetLen = target.length();
|
int targetLen = target.length();
|
||||||
if (dir == SEARCH_FORWARD) {
|
if (dir == SEARCH_FORWARD) {
|
||||||
// Forward search with wrapping
|
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
int pos = (startPos + i) % size;
|
int pos = (startPos + i) % size;
|
||||||
if (matchesAt(screenText, target, pos, size)) {
|
if (matchesAt(screenText, target, pos, size)) {
|
||||||
@@ -182,7 +181,6 @@ public class ECLPS implements ECLConstants {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Backward search with wrapping
|
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
int pos = (startPos - i + size) % size;
|
int pos = (startPos - i + size) % size;
|
||||||
if (matchesAt(screenText, target, pos, size)) {
|
if (matchesAt(screenText, target, pos, size)) {
|
||||||
@@ -193,6 +191,78 @@ public class ECLPS implements ECLConstants {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int searchString(String target) {
|
||||||
|
return searchString(target, 0, 0, SEARCH_FORWARD, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int searchString(String target, int startRow, int startCol) {
|
||||||
|
return searchString(target, startRow, startCol, SEARCH_FORWARD, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== IBM HoD SearchPS / SearchPSExt (1-based API) ==========
|
||||||
|
|
||||||
|
public int SearchPS(String text) {
|
||||||
|
return SearchPSExt(text, 1, getSize(), SEARCH_FORWARD, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SearchPS(String text, int startRow, int startCol) {
|
||||||
|
int pos = (startRow - 1) * getCols() + startCol;
|
||||||
|
return SearchPSExt(text, pos, getSize(), SEARCH_FORWARD, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SearchPS(String text, int startRow, int startCol, int dir) {
|
||||||
|
int pos = (startRow - 1) * getCols() + startCol;
|
||||||
|
return SearchPSExt(text, pos, getSize(), dir, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SearchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) {
|
||||||
|
int pos = (startRow - 1) * getCols() + startCol;
|
||||||
|
return SearchPSExt(text, pos, getSize(), dir, ignoreCase, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SearchPS extended method conforming to IBM ECL specification.
|
||||||
|
* Uses 1-based positions and returns 1-based index (or 0 if not found).
|
||||||
|
*/
|
||||||
|
public synchronized int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
|
||||||
|
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return 0;
|
||||||
|
|
||||||
|
int s0 = Math.max(0, Math.min(startPos - 1, size - 1));
|
||||||
|
int e0 = Math.max(0, Math.min(endPos - 1, size - 1));
|
||||||
|
|
||||||
|
char[] fullScreen = new char[size];
|
||||||
|
getPlane(PLANE_TEXT, fullScreen, 0, size);
|
||||||
|
String screenText = new String(fullScreen);
|
||||||
|
|
||||||
|
if (ignoreCase) {
|
||||||
|
screenText = screenText.toLowerCase();
|
||||||
|
text = text.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = wrap ? size : (dir == SEARCH_FORWARD ? (e0 >= s0 ? e0 - s0 + 1 : size - s0 + e0 + 1)
|
||||||
|
: (s0 >= e0 ? s0 - e0 + 1 : s0 + size - e0 + 1));
|
||||||
|
if (dir == SEARCH_FORWARD) {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int pos = (s0 + i) % size;
|
||||||
|
if (!wrap && e0 >= s0 && pos > e0) break;
|
||||||
|
if (matchesAt(screenText, text, pos, size)) {
|
||||||
|
return pos + 1; // 1-based
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
int pos = (s0 - i + size) % size;
|
||||||
|
if (!wrap && s0 >= e0 && pos < e0) break;
|
||||||
|
if (matchesAt(screenText, text, pos, size)) {
|
||||||
|
return pos + 1; // 1-based
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean matchesAt(String screenText, String target, int pos, int size) {
|
private boolean matchesAt(String screenText, String target, int pos, int size) {
|
||||||
int len = target.length();
|
int len = target.length();
|
||||||
for (int j = 0; j < len; j++) {
|
for (int j = 0; j < len; j++) {
|
||||||
@@ -204,6 +274,62 @@ public class ECLPS implements ECLConstants {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Rectangular Block Copy & Paste ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a rectangular text region from (sRow, sCol) to (eRow, eCol) inclusive.
|
||||||
|
*/
|
||||||
|
public synchronized String copyString(int sRow, int sCol, int eRow, int eCol) {
|
||||||
|
if (screen == null) return "";
|
||||||
|
int rows = screen.getRows();
|
||||||
|
int cols = screen.getCols();
|
||||||
|
if (rows <= 0 || cols <= 0) return "";
|
||||||
|
|
||||||
|
int minR = Math.max(0, Math.min(sRow, eRow));
|
||||||
|
int maxR = Math.min(rows - 1, Math.max(sRow, eRow));
|
||||||
|
int minC = Math.max(0, Math.min(sCol, eCol));
|
||||||
|
int maxC = Math.min(cols - 1, Math.max(sCol, eCol));
|
||||||
|
int sliceWidth = maxC - minC + 1;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int r = minR; r <= maxR; r++) {
|
||||||
|
char[] rowBuf = new char[sliceWidth];
|
||||||
|
getPlane(PLANE_TEXT, rowBuf, r * cols + minC, sliceWidth);
|
||||||
|
sb.append(rowBuf);
|
||||||
|
if (r < maxR) {
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paste a multi-line rectangular block of text starting at (row, col).
|
||||||
|
*/
|
||||||
|
public synchronized int pasteString(String text, int row, int col) {
|
||||||
|
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||||
|
int rows = screen.getRows();
|
||||||
|
int cols = screen.getCols();
|
||||||
|
if (rows <= 0 || cols <= 0) return 0;
|
||||||
|
|
||||||
|
String[] lines = text.split("\r?\n");
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.length; i++) {
|
||||||
|
int targetRow = (row + i) % rows;
|
||||||
|
String line = lines[i];
|
||||||
|
for (int c = 0; c < line.length() && (col + c) < cols; c++) {
|
||||||
|
int pos = targetRow * cols + (col + c);
|
||||||
|
setCursorPos(pos);
|
||||||
|
if (inputProcessor != null) {
|
||||||
|
inputProcessor.typeCharacter(line.charAt(c));
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paste text with line wrapping across unprotected fields.
|
* Paste text with line wrapping across unprotected fields.
|
||||||
*/
|
*/
|
||||||
@@ -224,15 +350,15 @@ public class ECLPS implements ECLConstants {
|
|||||||
int curPos = screen.getCursorAddress();
|
int curPos = screen.getCursorAddress();
|
||||||
int curCol = curPos % cols;
|
int curCol = curPos % cols;
|
||||||
if (endCol > 0 && curCol >= endCol) {
|
if (endCol > 0 && curCol >= endCol) {
|
||||||
// Advance to next row
|
|
||||||
int nextRow = (curPos / cols + 1) % rows;
|
int nextRow = (curPos / cols + 1) % rows;
|
||||||
setCursorPos(nextRow * cols);
|
setCursorPos(nextRow * cols);
|
||||||
}
|
}
|
||||||
inputProcessor.typeCharacter(line.charAt(i));
|
if (inputProcessor != null) {
|
||||||
|
inputProcessor.typeCharacter(line.charAt(i));
|
||||||
|
}
|
||||||
charsPasted++;
|
charsPasted++;
|
||||||
}
|
}
|
||||||
if (l < lines.length - 1) {
|
if (l < lines.length - 1 && inputProcessor != null) {
|
||||||
// Newline key between lines
|
|
||||||
inputProcessor.newline();
|
inputProcessor.newline();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,4 +373,103 @@ public class ECLPS implements ECLConstants {
|
|||||||
inputProcessor.sendKeys(keys);
|
inputProcessor.sendKeys(keys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send keystrokes with configurable inter-character or inter-mnemonic delay.
|
||||||
|
*/
|
||||||
|
public void sendCharacters(String keys, int delayMs) {
|
||||||
|
if (keys == null || keys.isEmpty()) return;
|
||||||
|
if (delayMs <= 0) {
|
||||||
|
sendKeys(keys);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
int len = keys.length();
|
||||||
|
while (i < len) {
|
||||||
|
if (keys.charAt(i) == '[') {
|
||||||
|
int close = keys.indexOf(']', i);
|
||||||
|
if (close > i) {
|
||||||
|
String mnemonic = keys.substring(i, close + 1);
|
||||||
|
sendKeys(mnemonic);
|
||||||
|
i = close + 1;
|
||||||
|
} else {
|
||||||
|
sendKeys(keys.substring(i, i + 1));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendKeys(keys.substring(i, i + 1));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayMs > 0 && i < len) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(delayMs);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Synchronization & ECL Automation Waits ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block until the specified text appears anywhere on the presentation space.
|
||||||
|
*/
|
||||||
|
public boolean waitForScreen(String text, long timeoutMs) {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||||
|
if (searchString(text) >= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(25);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return searchString(text) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block until the specified text appears at the given (row, col) coordinate.
|
||||||
|
*/
|
||||||
|
public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||||
|
String onScreen = getString(row, col, text.length());
|
||||||
|
if (text.equals(onScreen)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(25);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text.equals(getString(row, col, text.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block until the cursor moves to (row, col).
|
||||||
|
*/
|
||||||
|
public boolean waitForCursor(int row, int col, long timeoutMs) {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||||
|
if (getCursorRow() == row && getCursorCol() == col) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(25);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getCursorRow() == row && getCursorCol() == col;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,21 +19,36 @@ public class FillArea {
|
|||||||
public static class Edge {
|
public static class Edge {
|
||||||
public final double x1, y1;
|
public final double x1, y1;
|
||||||
public final double x2, y2;
|
public final double x2, y2;
|
||||||
|
public final int direction; // +1 if y1 < y2 (upward), -1 if y1 > y2 (downward)
|
||||||
|
|
||||||
public Edge(double x1, double y1, double x2, double y2) {
|
public Edge(double x1, double y1, double x2, double y2) {
|
||||||
this.x1 = x1;
|
this.x1 = x1;
|
||||||
this.y1 = y1;
|
this.y1 = y1;
|
||||||
this.x2 = x2;
|
this.x2 = x2;
|
||||||
this.y2 = y2;
|
this.y2 = y2;
|
||||||
|
this.direction = (y2 > y1) ? 1 : -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int fillRule = GocaConstants.FILL_RULE_EVEN_ODD;
|
||||||
private final List<Edge> edges = new ArrayList<>();
|
private final List<Edge> edges = new ArrayList<>();
|
||||||
private final List<double[]> subpathsX = new ArrayList<>();
|
private final List<double[]> subpathsX = new ArrayList<>();
|
||||||
private final List<double[]> subpathsY = new ArrayList<>();
|
private final List<double[]> subpathsY = new ArrayList<>();
|
||||||
|
|
||||||
public FillArea() {}
|
public FillArea() {}
|
||||||
|
|
||||||
|
public FillArea(int fillRule) {
|
||||||
|
this.fillRule = fillRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setFillRule(int fillRule) {
|
||||||
|
this.fillRule = fillRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getFillRule() {
|
||||||
|
return fillRule;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a single directed edge to the edge table.
|
* Adds a single directed edge to the edge table.
|
||||||
*/
|
*/
|
||||||
@@ -104,12 +119,47 @@ public class FillArea {
|
|||||||
subpathsY.clear();
|
subpathsY.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rasterizes and fills a direct polygon on the target GraphicsPlane.
|
||||||
|
*/
|
||||||
|
public synchronized void fill(GraphicsPlane plane, int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
|
||||||
|
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||||
|
int bgMix, int bgColorArgb) {
|
||||||
|
clear();
|
||||||
|
addPolygon(px, py, numPoints);
|
||||||
|
fill(plane, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
|
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
|
||||||
*/
|
*/
|
||||||
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
|
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
|
||||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||||
int bgMix, int bgColorArgb, ProgramSymbolManager psm) {
|
int bgMix, int bgColorArgb, ProgramSymbolManager psm) {
|
||||||
|
fill(plane, fillColorArgb, patternSet, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, this.fillRule, psm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NodeIntersection implements Comparable<NodeIntersection> {
|
||||||
|
final double x;
|
||||||
|
final int dir;
|
||||||
|
|
||||||
|
NodeIntersection(double x, int dir) {
|
||||||
|
this.x = x;
|
||||||
|
this.dir = dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(NodeIntersection other) {
|
||||||
|
return Double.compare(this.x, other.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule.
|
||||||
|
*/
|
||||||
|
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
|
||||||
|
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||||
|
int bgMix, int bgColorArgb, int fillRule, ProgramSymbolManager psm) {
|
||||||
if (plane == null) return;
|
if (plane == null) return;
|
||||||
if (edges.isEmpty() && subpathsX.isEmpty()) return;
|
if (edges.isEmpty() && subpathsX.isEmpty()) return;
|
||||||
|
|
||||||
@@ -140,7 +190,7 @@ public class FillArea {
|
|||||||
int iMinY = Math.max(0, (int) Math.floor(minY));
|
int iMinY = Math.max(0, (int) Math.floor(minY));
|
||||||
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
|
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
|
||||||
|
|
||||||
List<Double> nodeX = new ArrayList<>();
|
List<NodeIntersection> intersections = new ArrayList<>();
|
||||||
byte[] patRows = null;
|
byte[] patRows = null;
|
||||||
ProgramSymbolSet.SymbolSlot psSlot = null;
|
ProgramSymbolSet.SymbolSlot psSlot = null;
|
||||||
|
|
||||||
@@ -156,45 +206,84 @@ public class FillArea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int y = iMinY; y <= iMaxY; y++) {
|
for (int y = iMinY; y <= iMaxY; y++) {
|
||||||
nodeX.clear();
|
intersections.clear();
|
||||||
double scanY = y + 0.5;
|
double scanY = y + 0.5;
|
||||||
|
|
||||||
for (Edge e : edges) {
|
for (Edge e : edges) {
|
||||||
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) {
|
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) {
|
||||||
double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1);
|
double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1);
|
||||||
nodeX.add(x);
|
intersections.add(new NodeIntersection(x, e.direction));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Collections.sort(nodeX);
|
Collections.sort(intersections);
|
||||||
|
|
||||||
for (int i = 0; i < nodeX.size(); i += 2) {
|
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
|
||||||
if (i + 1 >= nodeX.size()) break;
|
// Non-Zero Winding Rule: evaluate winding count
|
||||||
int leftX = Math.max(0, (int) Math.round(nodeX.get(i)));
|
int winding = 0;
|
||||||
int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1)));
|
for (int i = 0; i < intersections.size() - 1; i++) {
|
||||||
|
winding += intersections.get(i).dir;
|
||||||
|
if (winding != 0) {
|
||||||
|
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
|
||||||
|
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
|
||||||
|
|
||||||
for (int x = leftX; x <= rightX; x++) {
|
for (int x = leftX; x <= rightX; x++) {
|
||||||
if (psSlot != null) {
|
if (psSlot != null) {
|
||||||
int psW = psSlot.getWidth();
|
int psW = psSlot.getWidth();
|
||||||
int psH = psSlot.getHeight();
|
int psH = psSlot.getHeight();
|
||||||
int psX = (psW > 0) ? (x % psW) : 0;
|
int psX = (psW > 0) ? (x % psW) : 0;
|
||||||
int psY = (psH > 0) ? (y % psH) : 0;
|
int psY = (psH > 0) ? (y % psH) : 0;
|
||||||
byte[] psPix = psSlot.getPixelData();
|
byte[] psPix = psSlot.getPixelData();
|
||||||
int pIdx = psY * psW + psX;
|
int pIdx = psY * psW + psX;
|
||||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||||
if (bit) {
|
if (bit) {
|
||||||
plane.setPixel(x, y, fill);
|
plane.setPixel(x, y, fill);
|
||||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||||
plane.setPixel(x, y, bg);
|
plane.setPixel(x, y, bg);
|
||||||
|
}
|
||||||
|
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||||
|
plane.setPixel(x, y, fill);
|
||||||
|
} else if (patRows != null) {
|
||||||
|
int b = patRows[y & 7] & 0xFF;
|
||||||
|
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||||
|
plane.setPixel(x, y, fill);
|
||||||
|
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||||
|
plane.setPixel(x, y, bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
}
|
||||||
plane.setPixel(x, y, fill);
|
}
|
||||||
} else if (patRows != null) {
|
} else {
|
||||||
int b = patRows[y & 7] & 0xFF;
|
// Even-Odd / Alternate Rule
|
||||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
for (int i = 0; i < intersections.size(); i += 2) {
|
||||||
|
if (i + 1 >= intersections.size()) break;
|
||||||
|
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
|
||||||
|
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
|
||||||
|
|
||||||
|
for (int x = leftX; x <= rightX; x++) {
|
||||||
|
if (psSlot != null) {
|
||||||
|
int psW = psSlot.getWidth();
|
||||||
|
int psH = psSlot.getHeight();
|
||||||
|
int psX = (psW > 0) ? (x % psW) : 0;
|
||||||
|
int psY = (psH > 0) ? (y % psH) : 0;
|
||||||
|
byte[] psPix = psSlot.getPixelData();
|
||||||
|
int pIdx = psY * psW + psX;
|
||||||
|
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||||
|
if (bit) {
|
||||||
|
plane.setPixel(x, y, fill);
|
||||||
|
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||||
|
plane.setPixel(x, y, bg);
|
||||||
|
}
|
||||||
|
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||||
plane.setPixel(x, y, fill);
|
plane.setPixel(x, y, fill);
|
||||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
} else if (patRows != null) {
|
||||||
plane.setPixel(x, y, bg);
|
int b = patRows[y & 7] & 0xFF;
|
||||||
|
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||||
|
plane.setPixel(x, y, fill);
|
||||||
|
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||||
|
plane.setPixel(x, y, bg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ public final class GocaConstants {
|
|||||||
public static final int G_GSMS = 0x1B; // Set Marker Size
|
public static final int G_GSMS = 0x1B; // Set Marker Size
|
||||||
public static final int G_GSCP = 0x21; // Set Current Position
|
public static final int G_GSCP = 0x21; // Set Current Position
|
||||||
public static final int G_GSAP = 0x22; // Arc Parameters
|
public static final int G_GSAP = 0x22; // Arc Parameters
|
||||||
|
public static final int G_GSC = 0x22; // Segment Characteristics
|
||||||
|
public static final int G_GSVW_DEF = 0x23; // Set Viewing Window Definition
|
||||||
public static final int G_GSECOL = 0x26; // Set Extended Color
|
public static final int G_GSECOL = 0x26; // Set Extended Color
|
||||||
public static final int G_GSVW = 0x27; // Set Viewing Window
|
public static final int G_GSVW = 0x27; // Set Viewing Window
|
||||||
public static final int G_GSPT = 0x28; // Set Pattern Symbol
|
public static final int G_GSPT = 0x28; // Set Pattern Symbol
|
||||||
@@ -157,6 +159,19 @@ public final class GocaConstants {
|
|||||||
public static final int MIX_XOR = 4;
|
public static final int MIX_XOR = 4;
|
||||||
public static final int MIX_UNDER = 5;
|
public static final int MIX_UNDER = 5;
|
||||||
|
|
||||||
|
// Fill Rules (GBAR 0x68 flags)
|
||||||
|
public static final int FILL_RULE_EVEN_ODD = 0;
|
||||||
|
public static final int FILL_RULE_WINDING = 1;
|
||||||
|
|
||||||
|
// Image Formats & Compression (GBIMG 0xD1)
|
||||||
|
public static final int IMG_UNCOMPRESSED = 0;
|
||||||
|
public static final int IMG_RLE = 1;
|
||||||
|
public static final int IMG_MMR = 2;
|
||||||
|
public static final int BPP_1 = 1;
|
||||||
|
public static final int BPP_2 = 2;
|
||||||
|
public static final int BPP_4 = 4;
|
||||||
|
public static final int BPP_8 = 8;
|
||||||
|
|
||||||
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
|
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
|
||||||
public static final int[] GOCA_COLORS = new int[] {
|
public static final int[] GOCA_COLORS = new int[] {
|
||||||
0xFF00FF00, // 0: Default (Green)
|
0xFF00FF00, // 0: Default (Green)
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public class GocaDecoder {
|
|||||||
private int fillColor = GocaConstants.GOCA_COLORS[0];
|
private int fillColor = GocaConstants.GOCA_COLORS[0];
|
||||||
private int charDir = GocaConstants.CD_LR;
|
private int charDir = GocaConstants.CD_LR;
|
||||||
private double charAngle = 0.0;
|
private double charAngle = 0.0;
|
||||||
|
private double charShear = 0.0;
|
||||||
|
private double fractionalLineWidth = 1.0;
|
||||||
private int charWidth = 9;
|
private int charWidth = 9;
|
||||||
private int charHeight = 16;
|
private int charHeight = 16;
|
||||||
private int charSet = 0;
|
private int charSet = 0;
|
||||||
@@ -42,11 +44,16 @@ public class GocaDecoder {
|
|||||||
private int arcParamR = 0;
|
private int arcParamR = 0;
|
||||||
private int arcParamS = 1;
|
private int arcParamS = 1;
|
||||||
|
|
||||||
|
private boolean segChained = false;
|
||||||
|
private boolean segDynamic = false;
|
||||||
|
private boolean segVisible = true;
|
||||||
|
|
||||||
private ProgramSymbolManager programSymbolManager;
|
private ProgramSymbolManager programSymbolManager;
|
||||||
|
|
||||||
// Area accumulation
|
// Area accumulation
|
||||||
private boolean inArea = false;
|
private boolean inArea = false;
|
||||||
private boolean areaDrawBoundary = true;
|
private boolean areaDrawBoundary = true;
|
||||||
|
private int areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
|
||||||
private boolean areaFill = true;
|
private boolean areaFill = true;
|
||||||
private final List<Integer> areaPointsX = new ArrayList<>();
|
private final List<Integer> areaPointsX = new ArrayList<>();
|
||||||
private final List<Integer> areaPointsY = new ArrayList<>();
|
private final List<Integer> areaPointsY = new ArrayList<>();
|
||||||
@@ -59,6 +66,8 @@ public class GocaDecoder {
|
|||||||
private int imgY = 0;
|
private int imgY = 0;
|
||||||
private int imgWidth = 0;
|
private int imgWidth = 0;
|
||||||
private int imgHeight = 0;
|
private int imgHeight = 0;
|
||||||
|
private int imgBitDepth = GocaConstants.BPP_1;
|
||||||
|
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
private final List<Byte> imgBuffer = new ArrayList<>();
|
private final List<Byte> imgBuffer = new ArrayList<>();
|
||||||
|
|
||||||
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
|
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
|
||||||
@@ -198,6 +207,89 @@ public class GocaDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double getCharAngle() {
|
||||||
|
return charAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setCharAngle(double angle) {
|
||||||
|
this.charAngle = angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCharShear() {
|
||||||
|
return charShear;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setCharShear(double shear) {
|
||||||
|
this.charShear = shear;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getFractionalLineWidth() {
|
||||||
|
return fractionalLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setFractionalLineWidth(double flw) {
|
||||||
|
this.fractionalLineWidth = Math.max(0.1, flw);
|
||||||
|
if (plane != null) {
|
||||||
|
plane.setFractionalLineWidth(this.fractionalLineWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSegChained() {
|
||||||
|
return segChained;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSegDynamic() {
|
||||||
|
return segDynamic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSegVisible() {
|
||||||
|
return segVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes GOCA order 0x23 / 0x27 Viewing Window clipping viewport.
|
||||||
|
*/
|
||||||
|
public synchronized void processViewingWindow(byte[] data) {
|
||||||
|
if (data == null || data.length < 8) return;
|
||||||
|
int xMin = readCoord(data, 0);
|
||||||
|
int yMin = readCoord(data, 2);
|
||||||
|
int xMax = readCoord(data, 4);
|
||||||
|
int yMax = readCoord(data, 6);
|
||||||
|
logger.info(String.format("GOCA processViewingWindow: [%d..%d, %d..%d]", xMin, xMax, yMin, yMax));
|
||||||
|
if (plane != null) {
|
||||||
|
plane.setViewingWindow(xMin, yMin, xMax, yMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes GOCA order 0x22 Segment Characteristics (chained/non-chained, dynamic, visible).
|
||||||
|
*/
|
||||||
|
public synchronized void processSegmentCharacteristics(byte[] data) {
|
||||||
|
if (data == null || data.length < 1) return;
|
||||||
|
int flags = data[0] & 0xFF;
|
||||||
|
this.segChained = (flags & 0x80) != 0;
|
||||||
|
this.segDynamic = (flags & 0x40) != 0;
|
||||||
|
this.segVisible = (flags & 0x20) == 0;
|
||||||
|
logger.info(String.format("GOCA processSegmentCharacteristics: flags=0x%02x (chained=%b, dynamic=%b, visible=%b)",
|
||||||
|
flags, segChained, segDynamic, segVisible));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes GOCA order 0x11 Fractional Line Width calculation.
|
||||||
|
*/
|
||||||
|
public synchronized void processFractionalLineWidth(byte[] data) {
|
||||||
|
if (data == null || data.length < 1) return;
|
||||||
|
int intPart = data[0] & 0xFF;
|
||||||
|
int fracPart = (data.length > 1) ? (data[1] & 0xFF) : 0;
|
||||||
|
double flw = intPart + (fracPart / 256.0);
|
||||||
|
if (flw <= 0.0) flw = 1.0;
|
||||||
|
this.fractionalLineWidth = flw;
|
||||||
|
if (plane != null) {
|
||||||
|
plane.setFractionalLineWidth(flw);
|
||||||
|
}
|
||||||
|
logger.info("GOCA processFractionalLineWidth: flw=" + flw);
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void resetDefaults() {
|
public synchronized void resetDefaults() {
|
||||||
curX = 0;
|
curX = 0;
|
||||||
curY = 0;
|
curY = 0;
|
||||||
@@ -217,6 +309,10 @@ public class GocaDecoder {
|
|||||||
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
|
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
|
||||||
lineType = GocaConstants.LT_SOLID;
|
lineType = GocaConstants.LT_SOLID;
|
||||||
lineWidth = GocaConstants.LW_NORMAL;
|
lineWidth = GocaConstants.LW_NORMAL;
|
||||||
|
fractionalLineWidth = 1.0;
|
||||||
|
if (plane != null) {
|
||||||
|
plane.setFractionalLineWidth(1.0);
|
||||||
|
}
|
||||||
markerType = GocaConstants.MK_PLUS;
|
markerType = GocaConstants.MK_PLUS;
|
||||||
markerSize = 5;
|
markerSize = 5;
|
||||||
markerColor = curColor;
|
markerColor = curColor;
|
||||||
@@ -226,14 +322,18 @@ public class GocaDecoder {
|
|||||||
fillColor = curColor;
|
fillColor = curColor;
|
||||||
charDir = GocaConstants.CD_LR;
|
charDir = GocaConstants.CD_LR;
|
||||||
charAngle = 0.0;
|
charAngle = 0.0;
|
||||||
|
charShear = 0.0;
|
||||||
charSet = 0;
|
charSet = 0;
|
||||||
charPrecision = GocaConstants.CP_STRING;
|
charPrecision = GocaConstants.CP_STRING;
|
||||||
inArea = false;
|
inArea = false;
|
||||||
areaDrawBoundary = true;
|
areaDrawBoundary = true;
|
||||||
|
areaFillRule = GocaConstants.FILL_RULE_EVEN_ODD;
|
||||||
areaFill = true;
|
areaFill = true;
|
||||||
areaPointsX.clear();
|
areaPointsX.clear();
|
||||||
areaPointsY.clear();
|
areaPointsY.clear();
|
||||||
inImage = false;
|
inImage = false;
|
||||||
|
imgBitDepth = GocaConstants.BPP_1;
|
||||||
|
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
imgBuffer.clear();
|
imgBuffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +370,10 @@ public class GocaDecoder {
|
|||||||
if (idx + 1 >= end) {
|
if (idx + 1 >= end) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
// Fractional Line Width (0x11): 2-byte operand [int][frac] or 1-byte operand [int]
|
||||||
|
if (order == GocaConstants.G_GSFLW) {
|
||||||
|
return (idx + 2 < end) ? 3 : 2;
|
||||||
|
}
|
||||||
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
|
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
|
||||||
if (order < 0x20) {
|
if (order < 0x20) {
|
||||||
return 2;
|
return 2;
|
||||||
@@ -497,7 +601,15 @@ public class GocaDecoder {
|
|||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_GSVW: { // Set Viewing Window (0x27)
|
case GocaConstants.G_GSVW_DEF:
|
||||||
|
case GocaConstants.G_GSVW: { // Set Viewing Window (0x23 / 0x27)
|
||||||
|
if (payloadLen >= 8 && idx + 9 < end) {
|
||||||
|
byte[] vwData = new byte[8];
|
||||||
|
System.arraycopy(inputData, idx + 2, vwData, 0, 8);
|
||||||
|
processViewingWindow(vwData);
|
||||||
|
} else if (payloadLen == 0 && plane != null) {
|
||||||
|
plane.clearViewingWindow();
|
||||||
|
}
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -558,6 +670,17 @@ public class GocaDecoder {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
|
case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
|
||||||
|
if (payloadLen >= 4 && idx + 5 < end) {
|
||||||
|
int sx = readCoord(inputData, idx + 2);
|
||||||
|
int sy = readCoord(inputData, idx + 4);
|
||||||
|
if (sx != 0 || sy != 0) {
|
||||||
|
charShear = Math.toDegrees(Math.atan2(sx, sy));
|
||||||
|
}
|
||||||
|
} else if (payloadLen >= 2 && idx + 3 < end) {
|
||||||
|
int intPart = inputData[idx + 2];
|
||||||
|
int fracPart = (payloadLen >= 2) ? (inputData[idx + 3] & 0xFF) : 0;
|
||||||
|
charShear = (intPart + fracPart / 256.0) * 45.0;
|
||||||
|
}
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -581,8 +704,16 @@ public class GocaDecoder {
|
|||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case GocaConstants.G_GSFLW: { // Set Fractional Line Width (0x11)
|
||||||
|
if (orderLen >= 2 && idx + 1 < end) {
|
||||||
|
byte[] flwData = new byte[orderLen - 1];
|
||||||
|
System.arraycopy(inputData, idx + 1, flwData, 0, flwData.length);
|
||||||
|
processFractionalLineWidth(flwData);
|
||||||
|
}
|
||||||
|
idx += orderLen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 0x06:
|
case 0x06:
|
||||||
case 0x11:
|
|
||||||
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
||||||
lineType = inputData[idx + 1] & 0xFF;
|
lineType = inputData[idx + 1] & 0xFF;
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
@@ -661,8 +792,9 @@ public class GocaDecoder {
|
|||||||
case GocaConstants.G_GBAR: { // Begin Area (0x68)
|
case GocaConstants.G_GBAR: { // Begin Area (0x68)
|
||||||
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||||
boolean drawBoundary = (flags & 0x80) != 0;
|
boolean drawBoundary = (flags & 0x80) != 0;
|
||||||
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary));
|
int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
|
||||||
beginArea(drawBoundary);
|
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
|
||||||
|
beginArea(drawBoundary, fillRule);
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -770,7 +902,18 @@ public class GocaDecoder {
|
|||||||
int y = readCoord(inputData, idx + 4);
|
int y = readCoord(inputData, idx + 4);
|
||||||
int w = readCoord(inputData, idx + 6);
|
int w = readCoord(inputData, idx + 6);
|
||||||
int h = readCoord(inputData, idx + 8);
|
int h = readCoord(inputData, idx + 8);
|
||||||
beginImage(x, y, w, h);
|
int bitDepth = GocaConstants.BPP_1;
|
||||||
|
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||||
|
if (payloadLen >= 9) {
|
||||||
|
int fmt = inputData[idx + 10] & 0xFF;
|
||||||
|
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||||
|
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||||
|
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||||
|
}
|
||||||
|
if (payloadLen >= 10) {
|
||||||
|
compression = inputData[idx + 11] & 0xFF;
|
||||||
|
}
|
||||||
|
beginImage(x, y, w, h, bitDepth, compression);
|
||||||
}
|
}
|
||||||
idx += orderLen;
|
idx += orderLen;
|
||||||
break;
|
break;
|
||||||
@@ -887,16 +1030,21 @@ public class GocaDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void beginArea(boolean drawBoundary) {
|
private void beginArea(boolean drawBoundary) {
|
||||||
|
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginArea(boolean drawBoundary, int fillRule) {
|
||||||
this.inArea = true;
|
this.inArea = true;
|
||||||
this.areaDrawBoundary = drawBoundary;
|
this.areaDrawBoundary = drawBoundary;
|
||||||
|
this.areaFillRule = fillRule;
|
||||||
this.areaFill = true;
|
this.areaFill = true;
|
||||||
this.fillColor = this.curColor;
|
this.fillColor = this.curColor;
|
||||||
this.areaPointsX.clear();
|
this.areaPointsX.clear();
|
||||||
this.areaPointsY.clear();
|
this.areaPointsY.clear();
|
||||||
this.areaPolygons.clear();
|
this.areaPolygons.clear();
|
||||||
this.currentPolyPts = 0;
|
this.currentPolyPts = 0;
|
||||||
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d",
|
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillRule=%d fillColor=0x%08x patternSet=%d pattern=%d",
|
||||||
drawBoundary, fillColor, patternSet, pattern));
|
drawBoundary, fillRule, fillColor, patternSet, pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void endArea() {
|
private void endArea() {
|
||||||
@@ -928,11 +1076,11 @@ public class GocaDecoder {
|
|||||||
polyCounts[i] = areaPolygons.get(i);
|
polyCounts[i] = areaPolygons.get(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d",
|
logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d fillRule=%d",
|
||||||
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix));
|
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix, areaFillRule));
|
||||||
|
|
||||||
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet,
|
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet,
|
||||||
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor);
|
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor, areaFillRule);
|
||||||
inArea = false;
|
inArea = false;
|
||||||
areaPointsX.clear();
|
areaPointsX.clear();
|
||||||
areaPointsY.clear();
|
areaPointsY.clear();
|
||||||
@@ -971,11 +1119,17 @@ public class GocaDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void beginImage(int x, int y, int w, int h) {
|
private void beginImage(int x, int y, int w, int h) {
|
||||||
|
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
|
||||||
this.inImage = true;
|
this.inImage = true;
|
||||||
this.imgX = x;
|
this.imgX = x;
|
||||||
this.imgY = y;
|
this.imgY = y;
|
||||||
this.imgWidth = w;
|
this.imgWidth = w;
|
||||||
this.imgHeight = h;
|
this.imgHeight = h;
|
||||||
|
this.imgBitDepth = bitDepth;
|
||||||
|
this.imgCompression = compression;
|
||||||
this.imgBuffer.clear();
|
this.imgBuffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -991,7 +1145,7 @@ public class GocaDecoder {
|
|||||||
|
|
||||||
int px = plane.mapX(imgX);
|
int px = plane.mapX(imgX);
|
||||||
int py = plane.mapY(imgY);
|
int py = plane.mapY(imgY);
|
||||||
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor);
|
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
|
||||||
inImage = false;
|
inImage = false;
|
||||||
imgBuffer.clear();
|
imgBuffer.clear();
|
||||||
}
|
}
|
||||||
@@ -1328,7 +1482,7 @@ public class GocaDecoder {
|
|||||||
}
|
}
|
||||||
String text = new String(chars);
|
String text = new String(chars);
|
||||||
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
|
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
|
||||||
curColor, cw, ch, charDir, charAngle);
|
curColor, cw, ch, charDir, charAngle, charShear);
|
||||||
} else if (charSet != 0 && programSymbolManager != null) {
|
} else if (charSet != 0 && programSymbolManager != null) {
|
||||||
for (int i = 0; i < textLen; i++) {
|
for (int i = 0; i < textLen; i++) {
|
||||||
int code = data[pos + i] & 0xFF;
|
int code = data[pos + i] & 0xFF;
|
||||||
@@ -1366,7 +1520,7 @@ public class GocaDecoder {
|
|||||||
}
|
}
|
||||||
String text = new String(chars);
|
String text = new String(chars);
|
||||||
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
|
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY), text,
|
||||||
curColor, cw, ch, charDir, charAngle);
|
curColor, cw, ch, charDir, charAngle, charShear);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (charDir) {
|
switch (charDir) {
|
||||||
@@ -1395,11 +1549,15 @@ public class GocaDecoder {
|
|||||||
* Draws a transformed character string (matching HODDecoder.drawGCS).
|
* Draws a transformed character string (matching HODDecoder.drawGCS).
|
||||||
*/
|
*/
|
||||||
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) {
|
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) {
|
||||||
|
drawGcs(x, y, text, color, cw, ch, dir, angle, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle, double shearAngle) {
|
||||||
if (plane != null && text != null && !text.isEmpty()) {
|
if (plane != null && text != null && !text.isEmpty()) {
|
||||||
if (charPrecision == GocaConstants.CP_STROKE) {
|
if (charPrecision == GocaConstants.CP_STROKE) {
|
||||||
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle);
|
plane.drawVectorText(x, y, text, color, cw, ch, dir, angle, shearAngle);
|
||||||
} else {
|
} else {
|
||||||
plane.drawText(x, y, text, color, cw, ch, dir, angle);
|
plane.drawText(x, y, text, color, cw, ch, dir, angle, shearAngle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1408,12 +1566,16 @@ public class GocaDecoder {
|
|||||||
* Draws an EBCDIC byte buffer as a transformed character string.
|
* Draws an EBCDIC byte buffer as a transformed character string.
|
||||||
*/
|
*/
|
||||||
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) {
|
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) {
|
||||||
|
drawGcs(ebcdicData, offset, length, color, cw, ch, dir, angle, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle, double shearAngle) {
|
||||||
if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return;
|
if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return;
|
||||||
char[] chars = new char[length];
|
char[] chars = new char[length];
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]);
|
chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]);
|
||||||
}
|
}
|
||||||
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle);
|
drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle, shearAngle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -106,4 +106,11 @@ public class GraphicInputBuilder {
|
|||||||
|
|
||||||
return sf;
|
return sf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the 56-byte Graphic Input Structured Field for pick correlation with aperture.
|
||||||
|
*/
|
||||||
|
public static byte[] buildPickCorrelation(int gocaX, int gocaY, int aperture) {
|
||||||
|
return buildGraphicInput(gocaX, gocaY, 1, true, false, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ public class GraphicsPlane {
|
|||||||
this.canvasWidth = w;
|
this.canvasWidth = w;
|
||||||
this.canvasHeight = h;
|
this.canvasHeight = h;
|
||||||
this.rgbBuffer = newBuffer;
|
this.rgbBuffer = newBuffer;
|
||||||
|
updateViewingWindowPixels();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void clear() {
|
public synchronized void clear() {
|
||||||
@@ -170,6 +171,59 @@ public class GraphicsPlane {
|
|||||||
return rgbBuffer;
|
return rgbBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int viewingWindowXMin = 0;
|
||||||
|
private int viewingWindowYMin = 0;
|
||||||
|
private int viewingWindowXMax = 0;
|
||||||
|
private int viewingWindowYMax = 0;
|
||||||
|
private boolean viewingWindowActive = false;
|
||||||
|
private int clipPixelXMin = 0;
|
||||||
|
private int clipPixelYMin = 0;
|
||||||
|
private int clipPixelXMax = 0;
|
||||||
|
private int clipPixelYMax = 0;
|
||||||
|
private double fractionalLineWidth = 1.0;
|
||||||
|
|
||||||
|
public synchronized void setViewingWindow(int xMin, int yMin, int xMax, int yMax) {
|
||||||
|
this.viewingWindowXMin = xMin;
|
||||||
|
this.viewingWindowYMin = yMin;
|
||||||
|
this.viewingWindowXMax = xMax;
|
||||||
|
this.viewingWindowYMax = yMax;
|
||||||
|
this.viewingWindowActive = true;
|
||||||
|
updateViewingWindowPixels();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void clearViewingWindow() {
|
||||||
|
this.viewingWindowActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isViewingWindowActive() {
|
||||||
|
return viewingWindowActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getViewingWindowXMin() { return viewingWindowXMin; }
|
||||||
|
public synchronized int getViewingWindowYMin() { return viewingWindowYMin; }
|
||||||
|
public synchronized int getViewingWindowXMax() { return viewingWindowXMax; }
|
||||||
|
public synchronized int getViewingWindowYMax() { return viewingWindowYMax; }
|
||||||
|
|
||||||
|
public synchronized void setFractionalLineWidth(double flw) {
|
||||||
|
this.fractionalLineWidth = Math.max(0.1, flw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized double getFractionalLineWidth() {
|
||||||
|
return fractionalLineWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateViewingWindowPixels() {
|
||||||
|
if (!viewingWindowActive) return;
|
||||||
|
int px1 = mapX(viewingWindowXMin);
|
||||||
|
int px2 = mapX(viewingWindowXMax);
|
||||||
|
int py1 = mapY(viewingWindowYMin);
|
||||||
|
int py2 = mapY(viewingWindowYMax);
|
||||||
|
this.clipPixelXMin = Math.max(0, Math.min(px1, px2));
|
||||||
|
this.clipPixelXMax = Math.min(canvasWidth - 1, Math.max(px1, px2));
|
||||||
|
this.clipPixelYMin = Math.max(0, Math.min(py1, py2));
|
||||||
|
this.clipPixelYMax = Math.min(canvasHeight - 1, Math.max(py1, py2));
|
||||||
|
}
|
||||||
|
|
||||||
public int getCanvasWidth() {
|
public int getCanvasWidth() {
|
||||||
return canvasWidth;
|
return canvasWidth;
|
||||||
}
|
}
|
||||||
@@ -313,6 +367,11 @@ public class GraphicsPlane {
|
|||||||
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
|
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
|
||||||
*/
|
*/
|
||||||
public synchronized void setPixel(int x, int y, int colorArgb) {
|
public synchronized void setPixel(int x, int y, int colorArgb) {
|
||||||
|
if (viewingWindowActive) {
|
||||||
|
if (x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
||||||
int srcA = (colorArgb >>> 24) & 0xFF;
|
int srcA = (colorArgb >>> 24) & 0xFF;
|
||||||
if (srcA == 0) return;
|
if (srcA == 0) return;
|
||||||
@@ -447,11 +506,15 @@ public class GraphicsPlane {
|
|||||||
|
|
||||||
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
|
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
|
||||||
if (brightness <= 0.0) return;
|
if (brightness <= 0.0) return;
|
||||||
if (lineWidth == GocaConstants.LW_THICK) {
|
if (lineWidth == GocaConstants.LW_THICK || fractionalLineWidth >= 1.5) {
|
||||||
|
int extra = (int) Math.round(Math.max(1, fractionalLineWidth - 0.5));
|
||||||
setPixelCoverage(x, y, colorRgb, 1.0);
|
setPixelCoverage(x, y, colorRgb, 1.0);
|
||||||
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness));
|
for (int dx = -extra; dx <= extra; dx++) {
|
||||||
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness));
|
for (int dy = -extra; dy <= extra; dy++) {
|
||||||
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7));
|
if (dx == 0 && dy == 0) continue;
|
||||||
|
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Perceptual gamma correction for crisp contrast on dark backgrounds
|
// Perceptual gamma correction for crisp contrast on dark backgrounds
|
||||||
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
|
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
|
||||||
@@ -641,6 +704,9 @@ public class GraphicsPlane {
|
|||||||
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
|
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
|
||||||
|
*/
|
||||||
/**
|
/**
|
||||||
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
|
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
|
||||||
*/
|
*/
|
||||||
@@ -648,25 +714,41 @@ public class GraphicsPlane {
|
|||||||
int fillColorArgb, int patternSet, int pattern,
|
int fillColorArgb, int patternSet, int pattern,
|
||||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||||
int bgMix, int bgColorArgb) {
|
int bgMix, int bgColorArgb) {
|
||||||
|
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, patternSet, pattern,
|
||||||
|
drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, GocaConstants.FILL_RULE_EVEN_ODD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NodeInter implements Comparable<NodeInter> {
|
||||||
|
final int x;
|
||||||
|
final int dir;
|
||||||
|
|
||||||
|
NodeInter(int x, int dir) {
|
||||||
|
this.x = x;
|
||||||
|
this.dir = dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(NodeInter o) {
|
||||||
|
return Integer.compare(this.x, o.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
|
||||||
|
*/
|
||||||
|
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
|
||||||
|
int fillColorArgb, int patternSet, int pattern,
|
||||||
|
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||||
|
int bgMix, int bgColorArgb, int fillRule) {
|
||||||
if (px == null || py == null || numPoints < 3) return;
|
if (px == null || py == null || numPoints < 3) return;
|
||||||
|
|
||||||
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||||
int bg = bgColorArgb;
|
int bg = bgColorArgb;
|
||||||
|
|
||||||
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
|
|
||||||
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
|
|
||||||
// Background Mix (GSBMX / bgMix):
|
|
||||||
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
|
|
||||||
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
|
|
||||||
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
|
|
||||||
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
|
|
||||||
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
|
|
||||||
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
|
|
||||||
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
||||||
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
|
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
|
||||||
|
|
||||||
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
||||||
// Find polygon vertical bounds across all points
|
|
||||||
int minY = py[0];
|
int minY = py[0];
|
||||||
int maxY = py[0];
|
int maxY = py[0];
|
||||||
for (int i = 1; i < numPoints; i++) {
|
for (int i = 1; i < numPoints; i++) {
|
||||||
@@ -676,7 +758,7 @@ public class GraphicsPlane {
|
|||||||
minY = Math.max(0, minY);
|
minY = Math.max(0, minY);
|
||||||
maxY = Math.min(canvasHeight - 1, maxY);
|
maxY = Math.min(canvasHeight - 1, maxY);
|
||||||
|
|
||||||
List<Integer> nodeX = new ArrayList<>();
|
List<NodeInter> nodeIntersections = new ArrayList<>();
|
||||||
byte[] patRows = null;
|
byte[] patRows = null;
|
||||||
ProgramSymbolSet.SymbolSlot psSlot = null;
|
ProgramSymbolSet.SymbolSlot psSlot = null;
|
||||||
if (patternSet >= 0x40 && programSymbolManager != null) {
|
if (patternSet >= 0x40 && programSymbolManager != null) {
|
||||||
@@ -691,7 +773,7 @@ public class GraphicsPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int y = minY; y <= maxY; y++) {
|
for (int y = minY; y <= maxY; y++) {
|
||||||
nodeX.clear();
|
nodeIntersections.clear();
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
|
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
|
||||||
for (int p = 0; p < polyCount; p++) {
|
for (int p = 0; p < polyCount; p++) {
|
||||||
@@ -705,7 +787,8 @@ public class GraphicsPlane {
|
|||||||
int xj = px[offset + j];
|
int xj = px[offset + j];
|
||||||
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
|
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
|
||||||
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
|
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
|
||||||
nodeX.add(x);
|
int dir = (yj > yi) ? 1 : -1;
|
||||||
|
nodeIntersections.add(new NodeInter(x, dir));
|
||||||
}
|
}
|
||||||
j = i;
|
j = i;
|
||||||
}
|
}
|
||||||
@@ -713,35 +796,72 @@ public class GraphicsPlane {
|
|||||||
offset += pLen;
|
offset += pLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
Collections.sort(nodeX);
|
Collections.sort(nodeIntersections);
|
||||||
|
|
||||||
for (int i = 0; i < nodeX.size(); i += 2) {
|
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
|
||||||
if (i + 1 >= nodeX.size()) break;
|
int winding = 0;
|
||||||
int leftX = Math.max(0, nodeX.get(i));
|
for (int i = 0; i < nodeIntersections.size() - 1; i++) {
|
||||||
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
|
winding += nodeIntersections.get(i).dir;
|
||||||
|
if (winding != 0) {
|
||||||
|
int leftX = Math.max(0, nodeIntersections.get(i).x);
|
||||||
|
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
|
||||||
|
|
||||||
for (int x = leftX; x <= rightX; x++) {
|
for (int x = leftX; x <= rightX; x++) {
|
||||||
if (psSlot != null) {
|
if (psSlot != null) {
|
||||||
int psW = psSlot.getWidth();
|
int psW = psSlot.getWidth();
|
||||||
int psH = psSlot.getHeight();
|
int psH = psSlot.getHeight();
|
||||||
int psX = (psW > 0) ? (x % psW) : 0;
|
int psX = (psW > 0) ? (x % psW) : 0;
|
||||||
int psY = (psH > 0) ? (y % psH) : 0;
|
int psY = (psH > 0) ? (y % psH) : 0;
|
||||||
byte[] psPix = psSlot.getPixelData();
|
byte[] psPix = psSlot.getPixelData();
|
||||||
int pIdx = psY * psW + psX;
|
int pIdx = psY * psW + psX;
|
||||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||||
if (bit) {
|
if (bit) {
|
||||||
setPixel(x, y, fill);
|
setPixel(x, y, fill);
|
||||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||||
setPixel(x, y, bg);
|
setPixel(x, y, bg);
|
||||||
|
}
|
||||||
|
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||||
|
setPixel(x, y, fill);
|
||||||
|
} else {
|
||||||
|
int b = patRows[y & 7] & 0xFF;
|
||||||
|
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||||
|
setPixel(x, y, fill);
|
||||||
|
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||||
|
setPixel(x, y, bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
}
|
||||||
setPixel(x, y, fill);
|
}
|
||||||
} else {
|
} else {
|
||||||
int b = patRows[y & 7] & 0xFF;
|
for (int i = 0; i < nodeIntersections.size(); i += 2) {
|
||||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
if (i + 1 >= nodeIntersections.size()) break;
|
||||||
|
int leftX = Math.max(0, nodeIntersections.get(i).x);
|
||||||
|
int rightX = Math.min(canvasWidth - 1, nodeIntersections.get(i + 1).x);
|
||||||
|
|
||||||
|
for (int x = leftX; x <= rightX; x++) {
|
||||||
|
if (psSlot != null) {
|
||||||
|
int psW = psSlot.getWidth();
|
||||||
|
int psH = psSlot.getHeight();
|
||||||
|
int psX = (psW > 0) ? (x % psW) : 0;
|
||||||
|
int psY = (psH > 0) ? (y % psH) : 0;
|
||||||
|
byte[] psPix = psSlot.getPixelData();
|
||||||
|
int pIdx = psY * psW + psX;
|
||||||
|
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||||
|
if (bit) {
|
||||||
|
setPixel(x, y, fill);
|
||||||
|
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||||
|
setPixel(x, y, bg);
|
||||||
|
}
|
||||||
|
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||||
setPixel(x, y, fill);
|
setPixel(x, y, fill);
|
||||||
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background)
|
} else {
|
||||||
setPixel(x, y, bg);
|
int b = patRows[y & 7] & 0xFF;
|
||||||
|
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||||
|
setPixel(x, y, fill);
|
||||||
|
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||||
|
setPixel(x, y, bg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -857,19 +977,29 @@ public class GraphicsPlane {
|
|||||||
*/
|
*/
|
||||||
public synchronized void drawText(double x, double y, String text, int colorArgb,
|
public synchronized void drawText(double x, double y, String text, int colorArgb,
|
||||||
double cellWidth, double cellHeight, int dir, double angle) {
|
double cellWidth, double cellHeight, int dir, double angle) {
|
||||||
|
drawText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void drawText(double x, double y, String text, int colorArgb,
|
||||||
|
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
|
||||||
if (text == null || text.isEmpty()) return;
|
if (text == null || text.isEmpty()) return;
|
||||||
if (textRenderer != null) {
|
if (textRenderer != null) {
|
||||||
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
updateCount++;
|
updateCount++;
|
||||||
} else {
|
} else {
|
||||||
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void drawText(int x, int y, String text, int colorArgb,
|
public synchronized void drawText(int x, int y, String text, int colorArgb,
|
||||||
int cellWidth, int cellHeight, int dir, double angle) {
|
int cellWidth, int cellHeight, int dir, double angle) {
|
||||||
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
|
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void drawText(int x, int y, String text, int colorArgb,
|
||||||
|
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
|
||||||
|
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -877,6 +1007,11 @@ public class GraphicsPlane {
|
|||||||
*/
|
*/
|
||||||
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
|
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
|
||||||
double cellWidth, double cellHeight, int dir, double angle) {
|
double cellWidth, double cellHeight, int dir, double angle) {
|
||||||
|
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
|
||||||
|
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
|
||||||
if (text == null || text.isEmpty()) return;
|
if (text == null || text.isEmpty()) return;
|
||||||
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||||
|
|
||||||
@@ -884,25 +1019,37 @@ public class GraphicsPlane {
|
|||||||
double ch = cellHeight > 0 ? cellHeight : 20.0;
|
double ch = cellHeight > 0 ? cellHeight : 20.0;
|
||||||
double curX = x;
|
double curX = x;
|
||||||
double curY = y;
|
double curY = y;
|
||||||
if (dir == GocaConstants.CD_TB) {
|
|
||||||
curY += ch;
|
double radAngle = Math.toRadians(angle);
|
||||||
} else if (dir == GocaConstants.CD_RL) {
|
double cosA = Math.cos(radAngle);
|
||||||
curX -= cw;
|
double sinA = Math.sin(radAngle);
|
||||||
|
|
||||||
|
if (angle == 0.0) {
|
||||||
|
if (dir == GocaConstants.CD_TB) {
|
||||||
|
curY += ch;
|
||||||
|
} else if (dir == GocaConstants.CD_RL) {
|
||||||
|
curX -= cw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
for (int i = 0; i < text.length(); i++) {
|
||||||
char c = text.charAt(i);
|
char c = text.charAt(i);
|
||||||
drawVssChar(curX, curY, c, color, cw, ch);
|
drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle);
|
||||||
|
|
||||||
switch (dir) {
|
if (angle != 0.0) {
|
||||||
case GocaConstants.CD_TB: curY += ch; break;
|
curX += cw * cosA;
|
||||||
case GocaConstants.CD_RL: curX -= cw; break;
|
curY += cw * sinA;
|
||||||
case GocaConstants.CD_BT: curY -= ch; break;
|
} else {
|
||||||
case GocaConstants.CD_LR:
|
switch (dir) {
|
||||||
case GocaConstants.CD_DEFAULT:
|
case GocaConstants.CD_TB: curY += ch; break;
|
||||||
default:
|
case GocaConstants.CD_RL: curX -= cw; break;
|
||||||
curX += cw;
|
case GocaConstants.CD_BT: curY -= ch; break;
|
||||||
break;
|
case GocaConstants.CD_LR:
|
||||||
|
case GocaConstants.CD_DEFAULT:
|
||||||
|
default:
|
||||||
|
curX += cw;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
@@ -911,10 +1058,19 @@ public class GraphicsPlane {
|
|||||||
|
|
||||||
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
||||||
int cellWidth, int cellHeight, int dir, double angle) {
|
int cellWidth, int cellHeight, int dir, double angle) {
|
||||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
|
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
||||||
|
int cellWidth, int cellHeight, int dir, double angle, double shearAngle) {
|
||||||
|
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
|
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
|
||||||
|
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawVssChar(double x, double y, char c, int color, double cw, double ch, double angle, double shearAngle) {
|
||||||
int code = (int) c;
|
int code = (int) c;
|
||||||
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
|
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
|
||||||
return;
|
return;
|
||||||
@@ -924,6 +1080,11 @@ public class GraphicsPlane {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double radAngle = Math.toRadians(angle);
|
||||||
|
double cosA = Math.cos(radAngle);
|
||||||
|
double sinA = Math.sin(radAngle);
|
||||||
|
double tanShear = Math.tan(Math.toRadians(shearAngle));
|
||||||
|
|
||||||
int ptr = offset;
|
int ptr = offset;
|
||||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||||
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||||
@@ -941,8 +1102,18 @@ public class GraphicsPlane {
|
|||||||
for (int p = 0; p < numPoints; p++) {
|
for (int p = 0; p < numPoints; p++) {
|
||||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||||
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
|
||||||
py[p] = y - ((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
|
double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||||
|
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||||
|
|
||||||
|
double sx = nx - ny * tanShear;
|
||||||
|
double sy = ny;
|
||||||
|
|
||||||
|
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
|
||||||
|
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
|
||||||
|
|
||||||
|
px[p] = x + rx;
|
||||||
|
py[p] = y + ry;
|
||||||
ipx[p] = (int) Math.round(px[p]);
|
ipx[p] = (int) Math.round(px[p]);
|
||||||
ipy[p] = (int) Math.round(py[p]);
|
ipy[p] = (int) Math.round(py[p]);
|
||||||
}
|
}
|
||||||
@@ -970,21 +1141,91 @@ public class GraphicsPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws raw image pixel bitmap.
|
* Draws raw image pixel bitmap with default 1-bit depth and uncompressed format.
|
||||||
*/
|
*/
|
||||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
|
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
|
||||||
|
drawImage(x, y, width, height, imageData, fgColorArgb, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws bitmap image with support for 1-bit, 2-bit, 4-bit, 8-bit depth and RLE decompression.
|
||||||
|
*/
|
||||||
|
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
|
||||||
|
int bitDepth, int compressionMode) {
|
||||||
if (imageData == null || width <= 0 || height <= 0) return;
|
if (imageData == null || width <= 0 || height <= 0) return;
|
||||||
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
|
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||||
int bytesPerRow = (width + 7) / 8;
|
|
||||||
|
|
||||||
for (int row = 0; row < height; row++) {
|
byte[] rawData = imageData;
|
||||||
int rowOffset = row * bytesPerRow;
|
if (compressionMode == GocaConstants.IMG_RLE) {
|
||||||
for (int col = 0; col < width; col++) {
|
rawData = decompressGocaRle(imageData, width, height, bitDepth);
|
||||||
int byteIdx = rowOffset + (col / 8);
|
}
|
||||||
if (byteIdx < imageData.length) {
|
|
||||||
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
|
||||||
if (bit) {
|
|
||||||
setPixel(x + col, y + row, fgColor);
|
if (depth == 1) {
|
||||||
|
int bytesPerRow = (width + 7) / 8;
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
int rowOffset = row * bytesPerRow;
|
||||||
|
for (int col = 0; col < width; col++) {
|
||||||
|
int byteIdx = rowOffset + (col / 8);
|
||||||
|
if (byteIdx < rawData.length) {
|
||||||
|
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
||||||
|
if (bit) {
|
||||||
|
setPixel(x + col, y + row, fgColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (depth == 2) {
|
||||||
|
int pixelsPerByte = 4;
|
||||||
|
int bytesPerRow = (width + 3) / 4;
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
int rowOffset = row * bytesPerRow;
|
||||||
|
for (int col = 0; col < width; col++) {
|
||||||
|
int byteIdx = rowOffset + (col / pixelsPerByte);
|
||||||
|
if (byteIdx < rawData.length) {
|
||||||
|
int shift = (3 - (col % 4)) * 2;
|
||||||
|
int val = (rawData[byteIdx] >> shift) & 0x03;
|
||||||
|
if (val != 0) {
|
||||||
|
int pixelColor;
|
||||||
|
switch (val) {
|
||||||
|
case 1: pixelColor = GocaConstants.GOCA_COLORS[1]; break; // Blue
|
||||||
|
case 2: pixelColor = GocaConstants.GOCA_COLORS[2]; break; // Red
|
||||||
|
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
|
||||||
|
default: pixelColor = fgColor; break;
|
||||||
|
}
|
||||||
|
setPixel(x + col, y + row, pixelColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (depth == 4) {
|
||||||
|
int pixelsPerByte = 2;
|
||||||
|
int bytesPerRow = (width + 1) / 2;
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
int rowOffset = row * bytesPerRow;
|
||||||
|
for (int col = 0; col < width; col++) {
|
||||||
|
int byteIdx = rowOffset + (col / pixelsPerByte);
|
||||||
|
if (byteIdx < rawData.length) {
|
||||||
|
int shift = (1 - (col % 2)) * 4;
|
||||||
|
int val = (rawData[byteIdx] >> shift) & 0x0F;
|
||||||
|
if (val != 0) {
|
||||||
|
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (depth == 8) {
|
||||||
|
int bytesPerRow = width;
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
int rowOffset = row * bytesPerRow;
|
||||||
|
for (int col = 0; col < width; col++) {
|
||||||
|
int byteIdx = rowOffset + col;
|
||||||
|
if (byteIdx < rawData.length) {
|
||||||
|
int val = rawData[byteIdx] & 0xFF;
|
||||||
|
if (val != 0) {
|
||||||
|
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -992,4 +1233,35 @@ public class GraphicsPlane {
|
|||||||
hasContent = true;
|
hasContent = true;
|
||||||
updateCount++;
|
updateCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompresses IBM GOCA Run-Length Encoded (RLE) bitmap raster streams.
|
||||||
|
*/
|
||||||
|
public static byte[] decompressGocaRle(byte[] rleData, int width, int height, int bitDepth) {
|
||||||
|
if (rleData == null || rleData.length == 0) return new byte[0];
|
||||||
|
int depth = (bitDepth == 2 || bitDepth == 4 || bitDepth == 8) ? bitDepth : 1;
|
||||||
|
int bytesPerRow = (width * depth + 7) / 8;
|
||||||
|
int expectedTotalBytes = bytesPerRow * height;
|
||||||
|
byte[] out = new byte[expectedTotalBytes];
|
||||||
|
int outIdx = 0;
|
||||||
|
int inIdx = 0;
|
||||||
|
|
||||||
|
while (inIdx < rleData.length && outIdx < expectedTotalBytes) {
|
||||||
|
int count = rleData[inIdx++] & 0xFF;
|
||||||
|
if (count == 0) {
|
||||||
|
if (inIdx < rleData.length) {
|
||||||
|
int litLen = rleData[inIdx++] & 0xFF;
|
||||||
|
for (int k = 0; k < litLen && inIdx < rleData.length && outIdx < expectedTotalBytes; k++) {
|
||||||
|
out[outIdx++] = rleData[inIdx++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (inIdx < rleData.length) {
|
||||||
|
byte val = rleData[inIdx++];
|
||||||
|
for (int k = 0; k < count && outIdx < expectedTotalBytes; k++) {
|
||||||
|
out[outIdx++] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,15 +212,14 @@ public class ProgramSymbolManager {
|
|||||||
int remaining = data.length - offset;
|
int remaining = data.length - offset;
|
||||||
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
||||||
|
|
||||||
int bytesPerSymbol;
|
int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
|
||||||
if (loadFormat == 1) {
|
if (sliceBytes <= 0) sliceBytes = 18;
|
||||||
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
|
|
||||||
} else {
|
|
||||||
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytesPerSymbol <= 0) {
|
int bytesPerSymbol;
|
||||||
bytesPerSymbol = 18;
|
if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
|
||||||
|
bytesPerSymbol = sliceBytes * 3;
|
||||||
|
} else {
|
||||||
|
bytesPerSymbol = sliceBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||||
@@ -230,10 +229,23 @@ public class ProgramSymbolManager {
|
|||||||
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadFormat == 1) {
|
if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
|
||||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
// 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
|
||||||
|
if (loadFormat == 1) {
|
||||||
|
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
|
||||||
|
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
|
||||||
|
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
|
||||||
|
} else {
|
||||||
|
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
|
||||||
|
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
|
||||||
|
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
if (loadFormat == 1) {
|
||||||
|
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||||
|
} else {
|
||||||
|
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
|||||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@@ -22,6 +24,7 @@ public class InputProcessor {
|
|||||||
private final ScreenBuffer screen;
|
private final ScreenBuffer screen;
|
||||||
private final EbcdicTranslator translator;
|
private final EbcdicTranslator translator;
|
||||||
private final TelnetFSM fsm;
|
private final TelnetFSM fsm;
|
||||||
|
private ECLOIA oia;
|
||||||
private int lastAid = AID_NO;
|
private int lastAid = AID_NO;
|
||||||
private boolean keyboardLocked;
|
private boolean keyboardLocked;
|
||||||
private boolean insertMode;
|
private boolean insertMode;
|
||||||
@@ -32,6 +35,14 @@ public class InputProcessor {
|
|||||||
this.fsm = fsm;
|
this.fsm = fsm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOIA(ECLOIA oia) {
|
||||||
|
this.oia = oia;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ECLOIA getOIA() {
|
||||||
|
return oia;
|
||||||
|
}
|
||||||
|
|
||||||
public TelnetFSM getFsm() {
|
public TelnetFSM getFsm() {
|
||||||
return fsm;
|
return fsm;
|
||||||
}
|
}
|
||||||
@@ -102,7 +113,9 @@ public class InputProcessor {
|
|||||||
* Enter a character at the current cursor position.
|
* Enter a character at the current cursor position.
|
||||||
*/
|
*/
|
||||||
public void typeCharacter(char ch) {
|
public void typeCharacter(char ch) {
|
||||||
if (keyboardLocked) return;
|
if (keyboardLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isNvtMode()) {
|
if (isNvtMode()) {
|
||||||
try {
|
try {
|
||||||
@@ -118,18 +131,37 @@ public class InputProcessor {
|
|||||||
int baddr = screen.getCursorAddress();
|
int baddr = screen.getCursorAddress();
|
||||||
baddr = ((baddr % size) + size) % size;
|
baddr = ((baddr % size) + size) % size;
|
||||||
|
|
||||||
// Check if cursor is at a field attribute or in a protected field
|
if (screen.isFormatted()) {
|
||||||
ExtendedAttribute ea = screen.getCell(baddr);
|
// Check if cursor is at a field attribute
|
||||||
if (ea.isFieldAttribute()) {
|
ExtendedAttribute ea = screen.getCell(baddr);
|
||||||
// Move to next position
|
if (ea.isFieldAttribute()) {
|
||||||
baddr = (baddr + 1) % size;
|
// Move to next position
|
||||||
ea = screen.getCell(baddr);
|
baddr = (baddr + 1) % size;
|
||||||
}
|
ea = screen.getCell(baddr);
|
||||||
|
}
|
||||||
|
|
||||||
byte faVal = screen.getFieldAttributeAt(baddr);
|
byte faVal = screen.getFieldAttributeAt(baddr);
|
||||||
if (faIsProtected(faVal & 0xFF)) {
|
if (faIsProtected(faVal & 0xFF)) {
|
||||||
// Protected field — can't type here
|
// Protected field — cannot type here
|
||||||
return;
|
if (oia != null) {
|
||||||
|
oia.setInputInhibited(ECLConstants.INHIBIT_PROTECTED_FIELD);
|
||||||
|
}
|
||||||
|
setKeyboardLocked(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric-only field check: digits 0-9, minus (-), period (.), space ( ), DUP, FM
|
||||||
|
if (faIsNumeric(faVal & 0xFF)) {
|
||||||
|
boolean isValidNumeric = (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == ' '
|
||||||
|
|| ch == '*' || ch == ';' || ch == (char) FCORDER_DUP || ch == (char) FCORDER_FM;
|
||||||
|
if (!isValidNumeric) {
|
||||||
|
if (oia != null) {
|
||||||
|
oia.setInputInhibited(ECLConstants.INHIBIT_NUMERIC_ONLY);
|
||||||
|
}
|
||||||
|
setKeyboardLocked(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate character to EBCDIC
|
// Translate character to EBCDIC
|
||||||
@@ -138,7 +170,6 @@ public class InputProcessor {
|
|||||||
|
|
||||||
if (insertMode) {
|
if (insertMode) {
|
||||||
// Insert mode: shift characters right from cursor to end of field
|
// Insert mode: shift characters right from cursor to end of field
|
||||||
// Find end of field
|
|
||||||
int endAddr = baddr;
|
int endAddr = baddr;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
|
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
|
||||||
@@ -146,9 +177,13 @@ public class InputProcessor {
|
|||||||
count++;
|
count++;
|
||||||
if (endAddr == baddr) break; // wrapped around (unformatted)
|
if (endAddr == baddr) break; // wrapped around (unformatted)
|
||||||
}
|
}
|
||||||
// Check if last position is non-null (field overflow)
|
// Check if last position is non-null/non-blank (field overflow)
|
||||||
if (screen.getCell(endAddr).ec != 0) {
|
ExtendedAttribute eaEnd = screen.getCell(endAddr);
|
||||||
// Field overflow — can't insert
|
if (eaEnd.ec != 0 && (eaEnd.ec & 0xFF) != 0x40 && eaEnd.ucs4 != 0 && eaEnd.ucs4 != ' ') {
|
||||||
|
if (oia != null) {
|
||||||
|
oia.setInputInhibited(ECLConstants.INHIBIT_OVERFLOW);
|
||||||
|
}
|
||||||
|
setKeyboardLocked(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Shift right from endAddr-1 down to baddr
|
// Shift right from endAddr-1 down to baddr
|
||||||
@@ -164,48 +199,291 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write character — preserve existing fg/bg/gr/cs attributes
|
// Write character — preserve existing fg/bg/gr/cs attributes
|
||||||
// so the character inherits the field's color scheme
|
ExtendedAttribute ea = screen.getCell(baddr);
|
||||||
ea = screen.getCell(baddr);
|
|
||||||
ea.ec = (byte) ebc;
|
ea.ec = (byte) ebc;
|
||||||
ea.ucs4 = ch;
|
ea.ucs4 = ch;
|
||||||
|
|
||||||
// Set MDT on field attribute
|
// Set MDT on field attribute
|
||||||
int faAddr = screen.findFieldAttribute(baddr);
|
if (screen.isFormatted()) {
|
||||||
if (faAddr >= 0) {
|
int faAddr = screen.findFieldAttribute(baddr);
|
||||||
ExtendedAttribute faEa = screen.getCell(faAddr);
|
if (faAddr >= 0) {
|
||||||
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
|
ExtendedAttribute faEa = screen.getCell(faAddr);
|
||||||
|
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance cursor
|
// Advance cursor and handle auto-skip
|
||||||
int startAdvance = baddr;
|
if (screen.isFormatted()) {
|
||||||
baddr = (baddr + 1) % size;
|
int nextAddr = screen.incrementAddress(baddr);
|
||||||
int advCount = 0;
|
if (screen.getCell(nextAddr).isFieldAttribute()) {
|
||||||
// Skip over field attributes safely
|
byte nextFa = screen.getCell(nextAddr).fa;
|
||||||
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance && advCount < size) {
|
if (faIsSkip(nextFa & 0xFF)) {
|
||||||
baddr = (baddr + 1) % size;
|
// Auto-skip field (Protected + Numeric): jump to next unprotected field
|
||||||
advCount++;
|
int skipTarget = screen.findNextUnprotected(nextAddr);
|
||||||
|
screen.setCursorAddress(skipTarget);
|
||||||
|
} else {
|
||||||
|
int adv = nextAddr;
|
||||||
|
int advCount = 0;
|
||||||
|
while (screen.getCell(adv).isFieldAttribute() && adv != baddr && advCount < size) {
|
||||||
|
adv = screen.incrementAddress(adv);
|
||||||
|
advCount++;
|
||||||
|
}
|
||||||
|
screen.setCursorAddress(adv);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
screen.setCursorAddress(nextAddr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
screen.setCursorAddress((baddr + 1) % size);
|
||||||
}
|
}
|
||||||
screen.setCursorAddress(baddr);
|
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
screen.updateDisplaySnapshot();
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process character input at current cursor position (HoD source compatibility).
|
||||||
|
*/
|
||||||
|
public void processChar(char ch) {
|
||||||
|
typeCharacter(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process character input with explicit position and insert mode flag.
|
||||||
|
*/
|
||||||
|
public void processChar(char ch, int pos, boolean insert) {
|
||||||
|
if (pos >= 0) {
|
||||||
|
screen.setCursorAddress(pos);
|
||||||
|
}
|
||||||
|
boolean oldInsert = insertMode;
|
||||||
|
insertMode = insert;
|
||||||
|
try {
|
||||||
|
typeCharacter(ch);
|
||||||
|
} finally {
|
||||||
|
insertMode = oldInsert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields).
|
||||||
|
*/
|
||||||
|
public byte[] buildReadModifiedInboundData() {
|
||||||
|
return buildReadModifiedInboundData(lastAid != 0 ? lastAid : AID_ENTER, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build inbound 3270 Read Modified data stream with specified AID code.
|
||||||
|
*/
|
||||||
|
public byte[] buildReadModifiedInboundData(int aidCode) {
|
||||||
|
return buildReadModifiedInboundData(aidCode, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build inbound 3270 Read Modified data stream with specified AID code and 'all' fields flag.
|
||||||
|
*/
|
||||||
|
public byte[] buildReadModifiedInboundData(int aidCode, boolean all) {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(aidCode);
|
||||||
|
|
||||||
|
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
|
out.write(caddr[0] & 0xFF);
|
||||||
|
out.write(caddr[1] & 0xFF);
|
||||||
|
|
||||||
|
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3 || aidCode == AID_CLEAR) {
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aidCode == AID_SELECT) {
|
||||||
|
// 3270 Selector Pen / Light Pen Immediate Selection (AID 0x7E):
|
||||||
|
// Per 3270 DS spec: Send AID (0x7E) + Cursor Address (2 bytes) + SBA (0x11) + Designator Addr for each modified field.
|
||||||
|
// NO character data is transmitted for AID_SELECT.
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute cell = screen.getCell(i);
|
||||||
|
if (cell.isFieldAttribute() && (all || faIsModified(cell.fa & 0xFF))) {
|
||||||
|
int designatorAddr = (i + 1) % size;
|
||||||
|
out.write(ORDER_SBA);
|
||||||
|
byte[] addr = encodeAddress(designatorAddr, screen.getRows(), screen.getCols());
|
||||||
|
out.write(addr[0] & 0xFF);
|
||||||
|
out.write(addr[1] & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte replyMode = screen.getReplyMode();
|
||||||
|
boolean extendedMode = (replyMode == SF_SRM_XFIELD || replyMode == SF_SRM_CHAR);
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute cell = screen.getCell(i);
|
||||||
|
if (cell.isFieldAttribute() && (all || faIsModified(cell.fa & 0xFF))) {
|
||||||
|
int fieldStart = (i + 1) % size;
|
||||||
|
|
||||||
|
out.write(ORDER_SBA);
|
||||||
|
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||||
|
out.write(addr[0] & 0xFF);
|
||||||
|
out.write(addr[1] & 0xFF);
|
||||||
|
|
||||||
|
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
|
||||||
|
int pos = fieldStart;
|
||||||
|
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||||
|
ExtendedAttribute charCell = screen.getCell(pos);
|
||||||
|
int b = charCell.ec & 0xFF;
|
||||||
|
if (b != 0x00) {
|
||||||
|
if (extendedMode) {
|
||||||
|
if (charCell.fg != curFg) {
|
||||||
|
out.write(ORDER_SA); out.write(XA_FOREGROUND); out.write(charCell.fg & 0xFF);
|
||||||
|
curFg = charCell.fg;
|
||||||
|
}
|
||||||
|
if (charCell.bg != curBg) {
|
||||||
|
out.write(ORDER_SA); out.write(XA_BACKGROUND); out.write(charCell.bg & 0xFF);
|
||||||
|
curBg = charCell.bg;
|
||||||
|
}
|
||||||
|
if (charCell.gr != curGr) {
|
||||||
|
out.write(ORDER_SA); out.write(XA_HIGHLIGHTING);
|
||||||
|
int xah = XAH_NORMAL;
|
||||||
|
if ((charCell.gr & GR_BLINK) != 0) xah = XAH_BLINK;
|
||||||
|
else if ((charCell.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
|
||||||
|
else if ((charCell.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
|
||||||
|
else if ((charCell.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
|
||||||
|
out.write(xah);
|
||||||
|
curGr = charCell.gr;
|
||||||
|
}
|
||||||
|
if (charCell.cs != curCs) {
|
||||||
|
out.write(ORDER_SA); out.write(XA_CHARSET); out.write(charCell.cs & 0xFF);
|
||||||
|
curCs = charCell.cs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.write(b);
|
||||||
|
}
|
||||||
|
pos = (pos + 1) % size;
|
||||||
|
if (pos == fieldStart) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unformatted screen in 3270 mode: send all non-null characters
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
int b = screen.getCell(i).ec & 0xFF;
|
||||||
|
if (b != 0x00) {
|
||||||
|
out.write(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build inbound 3270 Read Buffer data stream.
|
||||||
|
*/
|
||||||
|
public byte[] buildReadBufferInboundData() {
|
||||||
|
return buildReadBufferInboundData(lastAid != 0 ? lastAid : AID_NO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build inbound 3270 Read Buffer data stream with specified AID code.
|
||||||
|
*/
|
||||||
|
public byte[] buildReadBufferInboundData(int aidCode) {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(aidCode);
|
||||||
|
|
||||||
|
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
|
out.write(caddr[0] & 0xFF);
|
||||||
|
out.write(caddr[1] & 0xFF);
|
||||||
|
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
byte mode = screen.getReplyMode();
|
||||||
|
|
||||||
|
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
|
||||||
|
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0;
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
int count = 1;
|
||||||
|
if (ea.fg != 0) count++;
|
||||||
|
if (ea.bg != 0) count++;
|
||||||
|
if (ea.gr != 0) count++;
|
||||||
|
if (ea.cs != 0) count++;
|
||||||
|
if (ea.ol != 0) count++;
|
||||||
|
|
||||||
|
out.write(ORDER_SFE);
|
||||||
|
out.write(count);
|
||||||
|
out.write(XA_3270);
|
||||||
|
out.write(ea.fa & 0xFF);
|
||||||
|
if (ea.fg != 0) { out.write(XA_FOREGROUND); out.write(ea.fg & 0xFF); }
|
||||||
|
if (ea.bg != 0) { out.write(XA_BACKGROUND); out.write(ea.bg & 0xFF); }
|
||||||
|
if (ea.gr != 0) {
|
||||||
|
out.write(XA_HIGHLIGHTING);
|
||||||
|
int xah = XAH_NORMAL;
|
||||||
|
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
|
||||||
|
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
|
||||||
|
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
|
||||||
|
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
|
||||||
|
out.write(xah);
|
||||||
|
}
|
||||||
|
if (ea.cs != 0) { out.write(XA_CHARSET); out.write(ea.cs & 0xFF); }
|
||||||
|
if (ea.ol != 0) { out.write(XA_OUTLINING); out.write(ea.ol & 0xFF); }
|
||||||
|
} else {
|
||||||
|
if (mode == SF_SRM_CHAR) {
|
||||||
|
if (ea.fg != curFg) {
|
||||||
|
out.write(ORDER_SA); out.write(XA_FOREGROUND); out.write(ea.fg & 0xFF);
|
||||||
|
curFg = ea.fg;
|
||||||
|
}
|
||||||
|
if (ea.bg != curBg) {
|
||||||
|
out.write(ORDER_SA); out.write(XA_BACKGROUND); out.write(ea.bg & 0xFF);
|
||||||
|
curBg = ea.bg;
|
||||||
|
}
|
||||||
|
if (ea.gr != curGr) {
|
||||||
|
out.write(ORDER_SA); out.write(XA_HIGHLIGHTING);
|
||||||
|
int xah = XAH_NORMAL;
|
||||||
|
if ((ea.gr & GR_BLINK) != 0) xah = XAH_BLINK;
|
||||||
|
else if ((ea.gr & GR_REVERSE) != 0) xah = XAH_REVERSE;
|
||||||
|
else if ((ea.gr & GR_UNDERLINE) != 0) xah = XAH_UNDERSCORE;
|
||||||
|
else if ((ea.gr & GR_INTENSIFY) != 0) xah = XAH_INTENSIFY;
|
||||||
|
out.write(xah);
|
||||||
|
curGr = ea.gr;
|
||||||
|
}
|
||||||
|
if (ea.cs != curCs) {
|
||||||
|
out.write(ORDER_SA); out.write(XA_CHARSET); out.write(ea.cs & 0xFF);
|
||||||
|
curCs = ea.cs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.write(ea.ec & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard Field mode
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
out.write(ORDER_SF);
|
||||||
|
out.write(ea.fa & 0xFF);
|
||||||
|
} else {
|
||||||
|
out.write(ea.ec & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
|
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
|
||||||
*/
|
*/
|
||||||
public void sendAid(int aidCode) {
|
public void sendAid(int aidCode) {
|
||||||
System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
|
log.fine("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
|
||||||
if (aidCode == AID_SYSREQ) {
|
if (aidCode == AID_SYSREQ) {
|
||||||
sysReq();
|
if (fsm != null && fsm.isTn3270eNegotiated()) {
|
||||||
return;
|
fsm.handleSysReq();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (keyboardLocked && aidCode != AID_CLEAR) {
|
|
||||||
System.err.println("Keyboard locked, dropping AID");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastAid = aidCode;
|
|
||||||
setKeyboardLocked(true);
|
|
||||||
|
|
||||||
if (aidCode == AID_CLEAR) {
|
if (aidCode == AID_CLEAR) {
|
||||||
screen.clear();
|
screen.clear();
|
||||||
@@ -213,22 +491,27 @@ public class InputProcessor {
|
|||||||
if (graphicsPlane != null) {
|
if (graphicsPlane != null) {
|
||||||
graphicsPlane.clear();
|
graphicsPlane.clear();
|
||||||
}
|
}
|
||||||
// Send just the AID
|
|
||||||
byte[] data = new byte[] { (byte) aidCode };
|
byte[] data = new byte[] { (byte) aidCode };
|
||||||
sendAidResponse(data);
|
sendAidResponse(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyboardLocked || (oia != null && oia.getInputInhibited() != ECLConstants.INHIBIT_NOT_INHIBITED)) {
|
||||||
|
log.fine("Keyboard locked or OIA inhibited, dropping AID 0x" + Integer.toHexString(aidCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAid = aidCode;
|
||||||
|
setKeyboardLocked(true);
|
||||||
|
|
||||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
||||||
// SSCP-LU Mode (e.g. VM line mode / CP console before conmode 3270):
|
// SSCP-LU Mode: send raw EBCDIC line data without AID or 3270 cursor address
|
||||||
// Per RFC 2355: Send raw EBCDIC line data without AID or 3270 cursor address.
|
|
||||||
int cols = screen.getCols();
|
int cols = screen.getCols();
|
||||||
int curAddr = screen.getCursorAddress();
|
int curAddr = screen.getCursorAddress();
|
||||||
int row = curAddr / cols;
|
int row = curAddr / cols;
|
||||||
int rowStart = row * cols;
|
int rowStart = row * cols;
|
||||||
int rowEnd = rowStart + cols;
|
int rowEnd = rowStart + cols;
|
||||||
|
|
||||||
// Find last non-null, non-blank character in the current row
|
|
||||||
int lastChar = rowStart - 1;
|
int lastChar = rowStart - 1;
|
||||||
for (int i = rowEnd - 1; i >= rowStart; i--) {
|
for (int i = rowEnd - 1; i >= rowStart; i--) {
|
||||||
int ec = screen.getCell(i).ec & 0xFF;
|
int ec = screen.getCell(i).ec & 0xFF;
|
||||||
@@ -246,7 +529,6 @@ public class InputProcessor {
|
|||||||
|
|
||||||
fsm.sendSscpLuData(sscpData.toByteArray());
|
fsm.sendSscpLuData(sscpData.toByteArray());
|
||||||
|
|
||||||
// Advance cursor to beginning of next row
|
|
||||||
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
||||||
screen.setCursorAddress(nextRowAddr);
|
screen.setCursorAddress(nextRowAddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
@@ -255,111 +537,14 @@ public class InputProcessor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
byte[] payload = buildReadModifiedInboundData(aidCode, false);
|
||||||
// PA keys: send AID + optional PID + cursor address only (no modified data)
|
sendAidResponse(payload);
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
out.write(aidCode);
|
|
||||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
|
||||||
out.write(caddr[0] & 0xFF);
|
|
||||||
out.write(caddr[1] & 0xFF);
|
|
||||||
sendAidResponse(out.toByteArray());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aidCode == AID_SELECT) {
|
|
||||||
// 3270 Selector Pen / Light Pen Immediate Selection (AID 0x7E):
|
|
||||||
// Per IBM 3270 Data Stream Architecture (GA23-0059) and IBM Host On-Demand (DS3270.sendAid lines 727-1019):
|
|
||||||
// The inbound data stream consists of:
|
|
||||||
// 1. AID byte (0x7E)
|
|
||||||
// 2. Cursor address (2 bytes)
|
|
||||||
// 3. For each field with MDT=1:
|
|
||||||
// - SBA order (0x11)
|
|
||||||
// - Designator character address (faAddr + 1)
|
|
||||||
// CRITICAL: NO FIELD CHARACTER DATA IS TRANSMITTED FOR AID_SELECT!
|
|
||||||
// Sending character data in an AID_SELECT stream violates 3270 protocol and causes the host to reject the selection.
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
out.write(AID_SELECT);
|
|
||||||
|
|
||||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
|
||||||
out.write(caddr[0] & 0xFF);
|
|
||||||
out.write(caddr[1] & 0xFF);
|
|
||||||
|
|
||||||
if (screen.isFormatted()) {
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
ExtendedAttribute ea = screen.getCell(i);
|
|
||||||
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
|
||||||
int designatorAddr = (i + 1) % size;
|
|
||||||
out.write(ORDER_SBA);
|
|
||||||
byte[] addr = encodeAddress(designatorAddr, screen.getRows(), screen.getCols());
|
|
||||||
out.write(addr[0] & 0xFF);
|
|
||||||
out.write(addr[1] & 0xFF);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendAidResponse(out.toByteArray());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enter, PF keys: send AID + optional PID + cursor address + modified field data
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
out.write(aidCode);
|
|
||||||
|
|
||||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
|
||||||
out.write(caddr[0] & 0xFF);
|
|
||||||
out.write(caddr[1] & 0xFF);
|
|
||||||
|
|
||||||
if (screen.isFormatted()) {
|
|
||||||
// Send modified fields with SBA
|
|
||||||
// Per 3270 Data Stream Architecture: suppress NULLs (0x00) from field data
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
ExtendedAttribute ea = screen.getCell(i);
|
|
||||||
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
|
||||||
int fieldStart = (i + 1) % size;
|
|
||||||
|
|
||||||
// Always send SBA and address of first character in field
|
|
||||||
out.write(ORDER_SBA);
|
|
||||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
|
||||||
out.write(addr[0] & 0xFF);
|
|
||||||
out.write(addr[1] & 0xFF);
|
|
||||||
|
|
||||||
// Send all non-null characters in field (suppressing 0x00)
|
|
||||||
int pos = fieldStart;
|
|
||||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
|
||||||
int b = screen.getCell(pos).ec & 0xFF;
|
|
||||||
if (b != 0x00) {
|
|
||||||
out.write(b);
|
|
||||||
}
|
|
||||||
pos = (pos + 1) % size;
|
|
||||||
if (pos == fieldStart) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Unformatted screen in 3270 mode:
|
|
||||||
// Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line
|
|
||||||
// or we can just send everything up to the last non-null on the screen.
|
|
||||||
// IBM spec: "all alphanumeric characters... Nulls are suppressed."
|
|
||||||
// Actually, the simplest is to send everything, but suppress nulls.
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
int b = screen.getCell(i).ec & 0xFF;
|
|
||||||
if (b != 0x00) {
|
|
||||||
out.write(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendAidResponse(out.toByteArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void sendAidResponse(byte[] data) {
|
protected void sendAidResponse(byte[] data) {
|
||||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
||||||
fsm.sendSscpLuData(data);
|
fsm.sendSscpLuData(data);
|
||||||
} else if (fsm != null) {
|
} else if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
|
||||||
fsm.send3270Data(data);
|
fsm.send3270Data(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -758,29 +943,244 @@ public class InputProcessor {
|
|||||||
screen.updateDisplaySnapshot();
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attention key (sends Telnet IP). */
|
/** Attention key (sends Telnet Interrupt Process + IAC EOR break signal). */
|
||||||
public void attn() {
|
public void processAttn() {
|
||||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
|
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
|
||||||
byte[] ip = new byte[] { (byte) TelnetConstants.IAC, (byte) TelnetConstants.IP };
|
byte[] ipEor = new byte[] {
|
||||||
fsm.sendRecord(ip);
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.IP,
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
|
||||||
|
};
|
||||||
|
fsm.sendRecord(ipEor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** SysReq key. */
|
public void attn() {
|
||||||
public void sysReq() {
|
processAttn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SysReq key (generates TN3270E SYSREQ signal or 3270 Test Request AID). */
|
||||||
|
public void processSysReq() {
|
||||||
if (fsm != null && fsm.isTn3270eNegotiated()) {
|
if (fsm != null && fsm.isTn3270eNegotiated()) {
|
||||||
fsm.handleSysReq();
|
fsm.handleSysReq();
|
||||||
} else {
|
} else {
|
||||||
|
byte[] payload = buildReadModifiedInboundData(AID_SYSREQ, false);
|
||||||
|
sendAidResponse(payload);
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset (unlock keyboard, cancel insert mode). */
|
public void sysReq() {
|
||||||
|
processSysReq();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset (unlock keyboard, cancel insert mode, clear OIA error states). */
|
||||||
public void reset() {
|
public void reset() {
|
||||||
setKeyboardLocked(false);
|
setKeyboardLocked(false);
|
||||||
insertMode = false;
|
insertMode = false;
|
||||||
|
if (oia != null) {
|
||||||
|
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void processReset() {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move cursor backwards to the beginning of the previous/current word within the unprotected field.
|
||||||
|
*/
|
||||||
|
public void processWordLeft() {
|
||||||
|
if (keyboardLocked || screen == null) return;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return;
|
||||||
|
int cur = screen.getCursorAddress();
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
int faAddr = screen.findFieldAttribute(cur);
|
||||||
|
if (faAddr < 0) return;
|
||||||
|
if (faIsProtected(screen.getCell(faAddr).fa & 0xFF)) return;
|
||||||
|
|
||||||
|
int fieldStart = screen.incrementAddress(faAddr);
|
||||||
|
int pos = cur;
|
||||||
|
if (pos == fieldStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Scan backwards to skip any whitespace/nulls immediately preceding cursor
|
||||||
|
pos = screen.decrementAddress(pos);
|
||||||
|
while (pos != faAddr && isWhitespaceOrNull(pos)) {
|
||||||
|
pos = screen.decrementAddress(pos);
|
||||||
|
}
|
||||||
|
if (pos == faAddr) {
|
||||||
|
screen.setCursorAddress(fieldStart);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Scan backwards across non-whitespace characters until whitespace or field start
|
||||||
|
while (pos != faAddr && !isWhitespaceOrNull(pos)) {
|
||||||
|
pos = screen.decrementAddress(pos);
|
||||||
|
}
|
||||||
|
int wordStart = screen.incrementAddress(pos);
|
||||||
|
screen.setCursorAddress(wordStart);
|
||||||
|
} else {
|
||||||
|
int pos = cur;
|
||||||
|
if (pos > 0) pos--;
|
||||||
|
while (pos > 0 && isWhitespaceOrNull(pos)) {
|
||||||
|
pos--;
|
||||||
|
}
|
||||||
|
while (pos > 0 && !isWhitespaceOrNull(pos - 1)) {
|
||||||
|
pos--;
|
||||||
|
}
|
||||||
|
screen.setCursorAddress(pos);
|
||||||
|
}
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move cursor forwards to the beginning of the next word within the unprotected field.
|
||||||
|
*/
|
||||||
|
public void processWordRight() {
|
||||||
|
if (keyboardLocked || screen == null) return;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return;
|
||||||
|
int cur = screen.getCursorAddress();
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
int faAddr = screen.findFieldAttribute(cur);
|
||||||
|
if (faAddr < 0) return;
|
||||||
|
if (faIsProtected(screen.getCell(faAddr).fa & 0xFF)) return;
|
||||||
|
|
||||||
|
int pos = cur;
|
||||||
|
// Step 1: Scan forwards across current word (non-whitespace)
|
||||||
|
while (!screen.getCell(pos).isFieldAttribute() && !isWhitespaceOrNull(pos)) {
|
||||||
|
pos = screen.incrementAddress(pos);
|
||||||
|
}
|
||||||
|
// Step 2: Scan forwards across whitespace
|
||||||
|
while (!screen.getCell(pos).isFieldAttribute() && isWhitespaceOrNull(pos)) {
|
||||||
|
pos = screen.incrementAddress(pos);
|
||||||
|
}
|
||||||
|
if (screen.getCell(pos).isFieldAttribute()) {
|
||||||
|
pos = screen.findNextUnprotected(pos);
|
||||||
|
}
|
||||||
|
screen.setCursorAddress(pos);
|
||||||
|
} else {
|
||||||
|
int pos = cur;
|
||||||
|
while (pos < size && !isWhitespaceOrNull(pos)) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
while (pos < size && isWhitespaceOrNull(pos)) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (pos >= size) pos = size - 1;
|
||||||
|
screen.setCursorAddress(pos);
|
||||||
|
}
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move cursor to the position immediately following the last non-blank character in the current field.
|
||||||
|
*/
|
||||||
|
public void processFieldEnd() {
|
||||||
|
if (keyboardLocked || screen == null) return;
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return;
|
||||||
|
int cur = screen.getCursorAddress();
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
int faAddr = screen.findFieldAttribute(cur);
|
||||||
|
if (faAddr < 0) return;
|
||||||
|
if (faIsProtected(screen.getCell(faAddr).fa & 0xFF)) return;
|
||||||
|
|
||||||
|
int fieldStart = screen.incrementAddress(faAddr);
|
||||||
|
int endAddr = fieldStart;
|
||||||
|
int count = 0;
|
||||||
|
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
|
||||||
|
endAddr = screen.incrementAddress(endAddr);
|
||||||
|
count++;
|
||||||
|
if (endAddr == fieldStart) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int check = endAddr;
|
||||||
|
int lastNonBlank = -1;
|
||||||
|
int scanCount = 0;
|
||||||
|
while (scanCount <= count) {
|
||||||
|
ExtendedAttribute ea = screen.getCell(check);
|
||||||
|
if (ea.ec != 0 && (ea.ec & 0xFF) != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') {
|
||||||
|
lastNonBlank = check;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (check == fieldStart) break;
|
||||||
|
check = screen.decrementAddress(check);
|
||||||
|
scanCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastNonBlank < 0) {
|
||||||
|
screen.setCursorAddress(fieldStart);
|
||||||
|
} else if (lastNonBlank == endAddr) {
|
||||||
|
screen.setCursorAddress(endAddr);
|
||||||
|
} else {
|
||||||
|
screen.setCursorAddress(screen.incrementAddress(lastNonBlank));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int cols = screen.getCols();
|
||||||
|
int row = cur / cols;
|
||||||
|
int rowStart = row * cols;
|
||||||
|
int rowEnd = rowStart + cols - 1;
|
||||||
|
int lastNonBlank = -1;
|
||||||
|
for (int i = rowEnd; i >= rowStart; i--) {
|
||||||
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
|
if (ea.ec != 0 && (ea.ec & 0xFF) != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') {
|
||||||
|
lastNonBlank = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastNonBlank < 0) {
|
||||||
|
screen.setCursorAddress(rowStart);
|
||||||
|
} else if (lastNonBlank == rowEnd) {
|
||||||
|
screen.setCursorAddress(rowEnd);
|
||||||
|
} else {
|
||||||
|
screen.setCursorAddress(lastNonBlank + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emulate light pen / cursor selection on the current cursor position. */
|
||||||
|
public boolean processLightPen() {
|
||||||
|
if (screen == null) return false;
|
||||||
|
return lightPenSelect(screen.getCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emulate light pen / cursor selection on the specified screen address. */
|
||||||
|
public boolean processLightPen(int address) {
|
||||||
|
return lightPenSelect(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== IBM Host On-Demand 1:1 Compatible Method Overloads ==========
|
||||||
|
|
||||||
|
public void processEnter() { sendAid(AID_ENTER); }
|
||||||
|
public void processPF(int pfNum) { if (pfNum >= 1 && pfNum <= 24) sendAid(AID_PF1 + (pfNum - 1)); }
|
||||||
|
public void processPA(int paNum) { if (paNum >= 1 && paNum <= 3) sendAid(AID_PA1 + (paNum - 1)); }
|
||||||
|
public void processClear() { sendAid(AID_CLEAR); }
|
||||||
|
public void processCursorUp() { cursorUp(); }
|
||||||
|
public void processCursorDown() { cursorDown(); }
|
||||||
|
public void processCursorLeft() { cursorLeft(); }
|
||||||
|
public void processCursorRight() { cursorRight(); }
|
||||||
|
public void processTab() { tab(); }
|
||||||
|
public void processBackTab() { backTab(); }
|
||||||
|
public void processHome() { cursorHome(); }
|
||||||
|
public void processNewline() { newline(); }
|
||||||
|
public void processDelete() { deleteChar(); }
|
||||||
|
public void processBackspace() { backspace(); }
|
||||||
|
public void processEraseEOF() { eraseEof(); }
|
||||||
|
public void processEraseInput() { eraseInput(); }
|
||||||
|
public void processDup() { dup(); }
|
||||||
|
public void processFieldMark() { fieldMark(); }
|
||||||
|
public void processToggleInsert() { setInsertMode(!isInsertMode()); }
|
||||||
|
public boolean processCurSel() { return cursorSelect(); }
|
||||||
|
public boolean processCursorSelect() { return cursorSelect(); }
|
||||||
|
|
||||||
// ========== File Transfer Support ==========
|
// ========== File Transfer Support ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1080,9 +1480,27 @@ public class InputProcessor {
|
|||||||
case "wordbacktab":
|
case "wordbacktab":
|
||||||
processWordTab(false);
|
processWordTab(false);
|
||||||
break;
|
break;
|
||||||
|
case "wordleft":
|
||||||
|
processWordLeft();
|
||||||
|
break;
|
||||||
|
case "wordright":
|
||||||
|
processWordRight();
|
||||||
|
break;
|
||||||
|
case "fieldend":
|
||||||
|
case "end":
|
||||||
|
processFieldEnd();
|
||||||
|
break;
|
||||||
case "deleteword":
|
case "deleteword":
|
||||||
processDeleteWord();
|
processDeleteWord();
|
||||||
break;
|
break;
|
||||||
|
case "cursel":
|
||||||
|
case "cursorselect":
|
||||||
|
case "select":
|
||||||
|
cursorSelect();
|
||||||
|
break;
|
||||||
|
case "lightpen":
|
||||||
|
processLightPen();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (token.startsWith("pf")) {
|
if (token.startsWith("pf")) {
|
||||||
try {
|
try {
|
||||||
@@ -1122,43 +1540,11 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jump cursor to next or previous word boundary.
|
* Jump cursor to next or previous word boundary or tab stop.
|
||||||
*/
|
*/
|
||||||
public void processWordTab(boolean forward) {
|
public void processWordTab(boolean forward) {
|
||||||
if (keyboardLocked || screen == null) return;
|
if (keyboardLocked || screen == null) return;
|
||||||
int size = screen.getRows() * screen.getCols();
|
screen.processWordTab(forward);
|
||||||
if (size <= 0) return;
|
|
||||||
|
|
||||||
int cur = screen.getCursorAddress();
|
|
||||||
if (forward) {
|
|
||||||
// Find next whitespace then next non-whitespace
|
|
||||||
int pos = screen.incrementAddress(cur);
|
|
||||||
int count = 0;
|
|
||||||
while (count < size && !isWhitespaceOrNull(pos)) {
|
|
||||||
pos = screen.incrementAddress(pos);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
while (count < size && isWhitespaceOrNull(pos)) {
|
|
||||||
pos = screen.incrementAddress(pos);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
screen.setCursorAddress(pos);
|
|
||||||
} else {
|
|
||||||
// Find previous non-whitespace after whitespace
|
|
||||||
int pos = screen.decrementAddress(cur);
|
|
||||||
int count = 0;
|
|
||||||
while (count < size && isWhitespaceOrNull(pos)) {
|
|
||||||
pos = screen.decrementAddress(pos);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
while (count < size && !isWhitespaceOrNull(screen.decrementAddress(pos))) {
|
|
||||||
pos = screen.decrementAddress(pos);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
screen.setCursorAddress(pos);
|
|
||||||
}
|
|
||||||
screen.markAllChanged();
|
|
||||||
screen.updateDisplaySnapshot();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1166,23 +1552,7 @@ public class InputProcessor {
|
|||||||
*/
|
*/
|
||||||
public void processDeleteWord() {
|
public void processDeleteWord() {
|
||||||
if (keyboardLocked || screen == null) return;
|
if (keyboardLocked || screen == null) return;
|
||||||
int cur = screen.getCursorAddress();
|
screen.processDeleteWord();
|
||||||
if (screen.isFormatted()) {
|
|
||||||
byte fa = screen.getFieldAttributeAt(cur);
|
|
||||||
if (faIsProtected(fa & 0xFF)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int count = 0;
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
|
||||||
while (count < size && !isWhitespaceOrNull(screen.getCursorAddress())) {
|
|
||||||
deleteChar();
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
// Also delete trailing spaces
|
|
||||||
while (count < size && isWhitespaceOrNull(screen.getCursorAddress()) && screen.getCell(screen.getCursorAddress()).ec != 0) {
|
|
||||||
deleteChar();
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWhitespaceOrNull(int pos) {
|
private boolean isWhitespaceOrNull(int pos) {
|
||||||
|
|||||||
@@ -204,44 +204,52 @@ public final class DS3270Constants {
|
|||||||
public static final int SF_DESTROY_PART = 0x0d;
|
public static final int SF_DESTROY_PART = 0x0d;
|
||||||
public static final int SF_ACTIVATE_PART = 0x0e;
|
public static final int SF_ACTIVATE_PART = 0x0e;
|
||||||
public static final int SF_MODIFY_PART = 0x0f;
|
public static final int SF_MODIFY_PART = 0x0f;
|
||||||
|
public static final int SF_SET_WINDOW = 0x0f; // Set Window (GOCA/Modify Partition)
|
||||||
|
public static final int SF_3270_GRAPHICS = 0x20; // 3270 Graphics / Object Control
|
||||||
|
public static final int SF_OBJECT_CONTROL = 0x20; // Object Control
|
||||||
|
public static final int SF_DOCUMENT_DATA = 0x24; // Document Data (embedded SCS / GOCA)
|
||||||
|
public static final int SF_DOC_DATA = 0x24; // Alias for Document Data
|
||||||
public static final int SF_OUTBOUND_DS = 0x40;
|
public static final int SF_OUTBOUND_DS = 0x40;
|
||||||
public static final int SF_TRANSFER_DATA = 0xd0;
|
public static final int SF_TRANSFER_DATA = 0xd0;
|
||||||
|
|
||||||
// ========== Query Reply codes ==========
|
// ========== Query Reply codes ==========
|
||||||
public static final int QR_SUMMARY = 0x80; // Summary
|
public static final int QR_SUMMARY = 0x80; // Summary
|
||||||
public static final int QR_USABLE_AREA = 0x81; // Usable Area
|
public static final int QR_USABLE_AREA = 0x81; // Usable Area
|
||||||
public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
|
public static final int QR_IMAGE = 0x82; // Image (non-GOCA)
|
||||||
public static final int QR_TEXT_PART = 0x83; // Text Partitions
|
public static final int QR_TEXT_PART = 0x83; // Text Partitions
|
||||||
public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
|
public static final int QR_ALPHA_PART = 0x84; // Alphanumeric Partitions
|
||||||
public static final int QR_CHARSETS = 0x85; // Character Sets
|
public static final int QR_CHARSETS = 0x85; // Character Sets
|
||||||
public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
|
public static final int QR_COLOR = 0x86; // Color Table / Alphanumeric Color
|
||||||
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
|
public static final int QR_HIGHLIGHTING = 0x87; // Extended Highlighting
|
||||||
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
|
public static final int QR_REPLY_MODES = 0x88; // Reply Modes
|
||||||
public static final int QR_OUTLINING = 0x8c; // Field Outlining
|
public static final int QR_OUTLINING = 0x8c; // Field Outlining
|
||||||
public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
|
public static final int QR_SAVE_RESTORE = 0x8c; // Legacy alias for QR_OUTLINING
|
||||||
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
|
public static final int QR_DBCS_ASIA = 0x91; // DBCS Asia
|
||||||
public static final int QR_DDM = 0x95; // Distributed Data Management
|
public static final int QR_DDM = 0x95; // Distributed Data Management
|
||||||
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
|
public static final int QR_AUXDA = 0x99; // Auxiliary Devices
|
||||||
public static final int QR_FILE = 0x9f; // File Transfer
|
public static final int QR_FILE = 0x9f; // File Transfer
|
||||||
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
|
public static final int QR_DEVICECHAR = 0xa0; // Device Characteristics (Printer)
|
||||||
public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
|
public static final int QR_RPQNAMES = 0xa1; // RPQ Names (legacy)
|
||||||
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
|
public static final int QR_IMP_PART = 0xa6; // Implicit Partition Sizes
|
||||||
public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
|
public static final int QR_IMPLICIT = 0xa6; // Alias for QR_IMP_PART
|
||||||
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
|
public static final int QR_TRANSPARENCY = 0xa8; // Background Transparency
|
||||||
public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
|
public static final int QR_RPQ_NAMES = 0xa8; // Legacy alias pointing to 0xa8
|
||||||
public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
|
public static final int QR_SEGMENT = 0xb0; // Segment Characteristics (3270-PC Vector Graphics)
|
||||||
public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
|
public static final int QR_GRAPHICS = 0xb0; // Legacy alias for QR_SEGMENT
|
||||||
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA)
|
public static final int QR_PROCEDURE = 0xb1; // Procedure Characteristics (Image/GOCA)
|
||||||
public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
|
public static final int QR_GIMAGE = 0xb1; // Legacy alias for QR_PROCEDURE
|
||||||
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev)
|
public static final int QR_LINETYPE = 0xb2; // Line Type Support (Aux Dev)
|
||||||
public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE
|
public static final int QR_AUX_DEV = 0xb2; // Legacy alias for QR_LINETYPE
|
||||||
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
|
public static final int QR_AUX_DEVICE = 0xb2; // Aux Device alias
|
||||||
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
|
public static final int QR_PORT = 0xb3; // Port Characteristics (OEM Format)
|
||||||
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
|
public static final int QR_OEM_FMT = 0xb3; // Legacy alias for QR_PORT
|
||||||
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
|
public static final int QR_OEM_FORMAT = 0xb3; // OEM Format alias
|
||||||
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets
|
public static final int QR_GRCOLOR = 0xb4; // Graphic Color Table
|
||||||
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET
|
public static final int QR_GCOLOR = 0xb4; // Legacy alias for QR_GRCOLOR
|
||||||
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported)
|
public static final int QR_GRAPHIC_COLOR = 0xb4; // Graphic Color alias
|
||||||
|
public static final int QR_GRSYMBOLSET = 0xb6; // Graphic Symbol Sets
|
||||||
|
public static final int QR_GSYMBOLS = 0xb6; // Legacy alias for QR_GRSYMBOLSET
|
||||||
|
public static final int QR_NULL = 0xff; // Query Reply Null (Unsupported)
|
||||||
|
|
||||||
// ========== Screen model sizes ==========
|
// ========== Screen model sizes ==========
|
||||||
public static final int MODEL_2_ROWS = 24;
|
public static final int MODEL_2_ROWS = 24;
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ public class ExtendedAttribute {
|
|||||||
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */
|
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */
|
||||||
public byte bg;
|
public byte bg;
|
||||||
|
|
||||||
|
// Character set constants
|
||||||
|
public static final byte CS_BASE = 0;
|
||||||
|
public static final byte CS_APL = 1;
|
||||||
|
public static final byte CS_LINEDRAW = 2;
|
||||||
|
public static final byte CS_DBCS = 3;
|
||||||
|
public static final byte CS_GE = 0x04;
|
||||||
|
|
||||||
|
// DBCS state constants
|
||||||
|
public static final byte DB_NONE = 0;
|
||||||
|
public static final byte DB_LEFT = 1; // Left / first half of double-byte char
|
||||||
|
public static final byte DB_RIGHT = 2; // Right / second half of double-byte char
|
||||||
|
public static final byte DB_SI = 3; // Shift-In control char
|
||||||
|
public static final byte DB_SO = 4; // Shift-Out control char
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Graphics rendition bits.
|
* Graphics rendition bits.
|
||||||
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
|
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.screen;
|
|||||||
|
|
||||||
import haus.nightmare.lib3270j.TerminalModel;
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +42,14 @@ public class ScreenBuffer {
|
|||||||
private byte defaultGr = 0x00;
|
private byte defaultGr = 0x00;
|
||||||
private byte defaultCs = 0x00;
|
private byte defaultCs = 0x00;
|
||||||
private byte defaultIc = 0x00;
|
private byte defaultIc = 0x00;
|
||||||
|
|
||||||
|
// Entry Assist / DOC mode state
|
||||||
|
private boolean docMode = false;
|
||||||
|
private boolean wordWrap = false;
|
||||||
|
private int docStartCol = 0;
|
||||||
|
private int docEndCol = -1;
|
||||||
|
private int[] tabStops = null;
|
||||||
|
|
||||||
private final EbcdicTranslator translator;
|
private final EbcdicTranslator translator;
|
||||||
private final Object renderLock = new Object();
|
private final Object renderLock = new Object();
|
||||||
|
|
||||||
@@ -216,6 +227,10 @@ public class ScreenBuffer {
|
|||||||
this.explicitPartitionActive = (pid != 0);
|
this.explicitPartitionActive = (pid != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized PartitionInfo getPartition(int pid) {
|
||||||
|
return partitions.get(pid);
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void eraseReset(boolean alt) {
|
public synchronized void eraseReset(boolean alt) {
|
||||||
partitions.clear();
|
partitions.clear();
|
||||||
this.activePartition = 0;
|
this.activePartition = 0;
|
||||||
@@ -269,7 +284,7 @@ public class ScreenBuffer {
|
|||||||
public void setFieldAttribute(int pos, byte fa) {
|
public void setFieldAttribute(int pos, byte fa) {
|
||||||
ExtendedAttribute ea = buffer[pos];
|
ExtendedAttribute ea = buffer[pos];
|
||||||
ea.clear();
|
ea.clear();
|
||||||
ea.fa = fa;
|
ea.fa = (fa != 0) ? fa : (byte) FA_PRINTABLE;
|
||||||
if (!formatted) {
|
if (!formatted) {
|
||||||
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
|
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
|
||||||
}
|
}
|
||||||
@@ -471,39 +486,10 @@ public class ScreenBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private char getAplGraphic(int ec) {
|
private char getAplGraphic(int ec) {
|
||||||
switch (ec) {
|
if (translator != null) {
|
||||||
// Box-drawing line and corner characters (standard IBM 3270 GE / APL)
|
return translator.mapAPL(ec);
|
||||||
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
|
|
||||||
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
|
|
||||||
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
|
|
||||||
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
|
|
||||||
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
|
|
||||||
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
|
|
||||||
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
|
|
||||||
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
|
|
||||||
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
|
|
||||||
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
|
|
||||||
case 0xCB: return '\u253C'; // Cross -> '┼'
|
|
||||||
|
|
||||||
// Special math and APL symbols (matching x3270 cg.c / apl.c)
|
|
||||||
case 0x8C: return '\u2264'; // Less-than or equal '≤'
|
|
||||||
case 0xAE: return '\u2265'; // Greater-than or equal '≥'
|
|
||||||
case 0xBE: return '\u2260'; // Not equal '≠'
|
|
||||||
case 0xAD: return '['; // Left bracket
|
|
||||||
case 0xBD: return ']'; // Right bracket
|
|
||||||
case 0x8D: return '{'; // Left brace
|
|
||||||
case 0x9D: return '}'; // Right brace
|
|
||||||
case 0xB0: return '\u00B0'; // Degree '°'
|
|
||||||
case 0xB1: return '\u00B1'; // Plus-minus '±'
|
|
||||||
case 0xB2: return '\u00B2'; // Superscript 2 '²'
|
|
||||||
case 0xB3: return '\u00B3'; // Superscript 3 '³'
|
|
||||||
case 0xAF: return '\u00AF'; // Overbar '¯'
|
|
||||||
case 0xBA: return '\u03A9'; // Omega 'Ω'
|
|
||||||
case 0xBF: return '\u00B5'; // Micro 'µ'
|
|
||||||
case 0x5F: return '\u00AC'; // Not sign '¬'
|
|
||||||
|
|
||||||
default: return translator.ebcdicToUnicode(ec);
|
|
||||||
}
|
}
|
||||||
|
return (char) (ec & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -567,4 +553,511 @@ public class ScreenBuffer {
|
|||||||
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
|
if (baddr < 0 || baddr >= maxRows * maxCols) return 0;
|
||||||
return buffer[baddr].fa;
|
return buffer[baddr].fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Phase 3: Field Management & Navigation ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an ECLFieldList representation of the presentation space.
|
||||||
|
*/
|
||||||
|
public synchronized ECLFieldList buildFieldList() {
|
||||||
|
return new ECLFieldList(new ECLPS(this, null, translator), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the ECLField at the specified row and column.
|
||||||
|
*/
|
||||||
|
public ECLField findFieldAt(int row, int col) {
|
||||||
|
return buildFieldList().findFieldAt(row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the ECLField containing the specified buffer position.
|
||||||
|
*/
|
||||||
|
public ECLField findFieldAt(int pos) {
|
||||||
|
return buildFieldList().findFieldAt(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the ECLField containing the specified buffer position.
|
||||||
|
*/
|
||||||
|
public ECLField findField(int pos) {
|
||||||
|
return buildFieldList().findField(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the field preceding the field at the given position.
|
||||||
|
*/
|
||||||
|
public ECLField findPrevField(int pos) {
|
||||||
|
return buildFieldList().findPrevField(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the field succeeding the field at the given position.
|
||||||
|
*/
|
||||||
|
public ECLField findNextField(int pos) {
|
||||||
|
return buildFieldList().findNextField(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first field in the presentation space.
|
||||||
|
*/
|
||||||
|
public ECLField getFirstField() {
|
||||||
|
return buildFieldList().getFirstField();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Presentation Space Accessors & Convenience Methods ==========
|
||||||
|
|
||||||
|
public int getSize() {
|
||||||
|
return rows * cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized char getChar(int row, int col) {
|
||||||
|
int addr = rowColToAddress(row, col);
|
||||||
|
if (addr < 0 || addr >= rows * cols) return ' ';
|
||||||
|
ExtendedAttribute ea = buffer[addr];
|
||||||
|
if (ea.isFieldAttribute()) return ' ';
|
||||||
|
if (ea.ucs4 != 0) return (char) ea.ucs4;
|
||||||
|
if (ea.ec != 0) return translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||||
|
return ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setChar(int row, int col, char c) {
|
||||||
|
int addr = rowColToAddress(row, col);
|
||||||
|
if (addr < 0 || addr >= rows * cols) return;
|
||||||
|
int ebc = translator.unicodeToEbcdic(c);
|
||||||
|
buffer[addr].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||||
|
buffer[addr].ucs4 = c;
|
||||||
|
screenChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void writeChar(int pos, byte ebc) {
|
||||||
|
if (pos < 0 || pos >= rows * cols) return;
|
||||||
|
buffer[pos].ec = ebc;
|
||||||
|
buffer[pos].ucs4 = translator.ebcdicToUnicode(ebc & 0xFF);
|
||||||
|
screenChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getAttr(int row, int col) {
|
||||||
|
return getFieldAttributeAt(rowColToAddress(row, col));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtendedAttribute getExtAttr(int row, int col) {
|
||||||
|
return getCell(rowColToAddress(row, col));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setExtAttr(int row, int col, ExtendedAttribute ea) {
|
||||||
|
int addr = rowColToAddress(row, col);
|
||||||
|
if (addr >= 0 && addr < rows * cols && ea != null) {
|
||||||
|
buffer[addr].copyFrom(ea);
|
||||||
|
screenChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String getText() {
|
||||||
|
int size = rows * cols;
|
||||||
|
char[] buf = new char[size];
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute ea = buffer[i];
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
buf[i] = ' ';
|
||||||
|
} else if (ea.ucs4 != 0) {
|
||||||
|
buf[i] = (char) ea.ucs4;
|
||||||
|
} else if (ea.ec != 0) {
|
||||||
|
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||||
|
} else {
|
||||||
|
buf[i] = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new String(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String getString(int pos, int len) {
|
||||||
|
if (len <= 0) return "";
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return "";
|
||||||
|
char[] buf = new char[len];
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
int addr = (pos + i) % size;
|
||||||
|
ExtendedAttribute ea = buffer[addr];
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
buf[i] = ' ';
|
||||||
|
} else if (ea.ucs4 != 0) {
|
||||||
|
buf[i] = (char) ea.ucs4;
|
||||||
|
} else if (ea.ec != 0) {
|
||||||
|
buf[i] = translator.ebcdicToUnicode(ea.ec & 0xFF);
|
||||||
|
} else {
|
||||||
|
buf[i] = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new String(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setText(String text) {
|
||||||
|
if (text == null) return;
|
||||||
|
int size = rows * cols;
|
||||||
|
int len = Math.min(text.length(), size);
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
char ch = text.charAt(i);
|
||||||
|
int ebc = translator.unicodeToEbcdic(ch);
|
||||||
|
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||||
|
buffer[i].ucs4 = ch;
|
||||||
|
}
|
||||||
|
screenChanged = true;
|
||||||
|
updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int searchString(String target) {
|
||||||
|
if (target == null || target.isEmpty()) return -1;
|
||||||
|
String full = getText();
|
||||||
|
return full.indexOf(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isModified() {
|
||||||
|
int size = rows * cols;
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
if (buffer[i].isFieldAttribute() && faIsModified(buffer[i].fa & 0xFF)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isModified(int pos) {
|
||||||
|
return faIsModified(getFieldAttributeAt(pos) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProtected(int pos) {
|
||||||
|
return faIsProtected(getFieldAttributeAt(pos) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNumeric(int pos) {
|
||||||
|
return faIsNumeric(getFieldAttributeAt(pos) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDisplay(int pos) {
|
||||||
|
return !faIsZero(getFieldAttributeAt(pos) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== DBCS Character Insertion & Deletion ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a character at the specified buffer address with field boundary and DBCS preservation.
|
||||||
|
*/
|
||||||
|
public synchronized boolean insertChar(int pos, char ch) {
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return false;
|
||||||
|
pos = ((pos % size) + size) % size;
|
||||||
|
|
||||||
|
if (formatted) {
|
||||||
|
byte faVal = getFieldAttributeAt(pos);
|
||||||
|
if (faIsProtected(faVal & 0xFF)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isDbcsChar = translator != null && translator.isDBCS() && translator.unicodeToDbcs(ch) >= 0;
|
||||||
|
int shiftAmount = isDbcsChar ? 2 : 1;
|
||||||
|
|
||||||
|
// Find end of field
|
||||||
|
int endAddr = pos;
|
||||||
|
int count = 0;
|
||||||
|
while (!getCell(incrementAddress(endAddr)).isFieldAttribute() && count < size) {
|
||||||
|
endAddr = incrementAddress(endAddr);
|
||||||
|
count++;
|
||||||
|
if (endAddr == pos) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check overflow
|
||||||
|
for (int s = 0; s < shiftAmount; s++) {
|
||||||
|
int checkAddr = endAddr;
|
||||||
|
for (int k = 0; k < s; k++) checkAddr = decrementAddress(checkAddr);
|
||||||
|
ExtendedAttribute eaEnd = getCell(checkAddr);
|
||||||
|
if (eaEnd.ec != 0 && eaEnd.ec != 0x40 && eaEnd.ucs4 != 0 && eaEnd.ucs4 != ' ') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift characters right
|
||||||
|
for (int s = 0; s < shiftAmount; s++) {
|
||||||
|
int dst = endAddr;
|
||||||
|
int shiftCount = 0;
|
||||||
|
while (dst != pos && shiftCount < size) {
|
||||||
|
int src = decrementAddress(dst);
|
||||||
|
getCell(dst).copyFrom(getCell(src));
|
||||||
|
dst = src;
|
||||||
|
shiftCount++;
|
||||||
|
}
|
||||||
|
getCell(pos).clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDbcsChar) {
|
||||||
|
int dbcs = translator.unicodeToDbcs(ch);
|
||||||
|
int b1 = (dbcs >> 8) & 0xFF;
|
||||||
|
int b2 = dbcs & 0xFF;
|
||||||
|
int nextPos = incrementAddress(pos);
|
||||||
|
|
||||||
|
ExtendedAttribute ea1 = getCell(pos);
|
||||||
|
ea1.ec = (byte) b1;
|
||||||
|
ea1.ucs4 = ch;
|
||||||
|
ea1.cs = ExtendedAttribute.CS_DBCS;
|
||||||
|
ea1.db = ExtendedAttribute.DB_LEFT;
|
||||||
|
|
||||||
|
ExtendedAttribute ea2 = getCell(nextPos);
|
||||||
|
ea2.ec = (byte) b2;
|
||||||
|
ea2.ucs4 = ch;
|
||||||
|
ea2.cs = ExtendedAttribute.CS_DBCS;
|
||||||
|
ea2.db = ExtendedAttribute.DB_RIGHT;
|
||||||
|
|
||||||
|
setCursorAddress(incrementAddress(nextPos));
|
||||||
|
} else {
|
||||||
|
int ebc = translator.unicodeToEbcdic(ch);
|
||||||
|
ExtendedAttribute ea = getCell(pos);
|
||||||
|
ea.ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||||
|
ea.ucs4 = ch;
|
||||||
|
ea.cs = ExtendedAttribute.CS_BASE;
|
||||||
|
ea.db = ExtendedAttribute.DB_NONE;
|
||||||
|
|
||||||
|
setCursorAddress(incrementAddress(pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set MDT
|
||||||
|
if (formatted) {
|
||||||
|
int faAddr = findFieldAttribute(pos);
|
||||||
|
if (faAddr >= 0) {
|
||||||
|
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screenChanged = true;
|
||||||
|
updateDisplaySnapshot();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean insertChar(char ch) {
|
||||||
|
return insertChar(cursorAddress, ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a character at the specified buffer address, pulling trailing field text and preserving DBCS glyphs.
|
||||||
|
*/
|
||||||
|
public synchronized boolean deleteChar(int pos) {
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return false;
|
||||||
|
pos = ((pos % size) + size) % size;
|
||||||
|
|
||||||
|
if (getCell(pos).isFieldAttribute()) return false;
|
||||||
|
if (formatted) {
|
||||||
|
byte faVal = getFieldAttributeAt(pos);
|
||||||
|
if (faIsProtected(faVal & 0xFF)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtendedAttribute curCell = getCell(pos);
|
||||||
|
boolean isDbcs = curCell.db == ExtendedAttribute.DB_LEFT || curCell.db == ExtendedAttribute.DB_RIGHT
|
||||||
|
|| curCell.cs == ExtendedAttribute.CS_DBCS;
|
||||||
|
int deleteAmount = isDbcs ? 2 : 1;
|
||||||
|
if (curCell.db == ExtendedAttribute.DB_RIGHT) {
|
||||||
|
pos = decrementAddress(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int d = 0; d < deleteAmount; d++) {
|
||||||
|
int shiftAddr = pos;
|
||||||
|
int count = 0;
|
||||||
|
while (count < size) {
|
||||||
|
int next = incrementAddress(shiftAddr);
|
||||||
|
if (getCell(next).isFieldAttribute()) {
|
||||||
|
getCell(shiftAddr).clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
getCell(shiftAddr).copyFrom(getCell(next));
|
||||||
|
shiftAddr = next;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanAdjacentSISO(pos);
|
||||||
|
|
||||||
|
if (formatted) {
|
||||||
|
int faAddr = findFieldAttribute(pos);
|
||||||
|
if (faAddr >= 0) {
|
||||||
|
getCell(faAddr).fa = (byte) (getCell(faAddr).fa | FA_MODIFY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screenChanged = true;
|
||||||
|
updateDisplaySnapshot();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean deleteChar() {
|
||||||
|
return deleteChar(cursorAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanAdjacentSISO(int nearPos) {
|
||||||
|
int size = rows * cols;
|
||||||
|
int start = Math.max(0, nearPos - 5);
|
||||||
|
int end = Math.min(size, nearPos + 10);
|
||||||
|
for (int i = start; i < end - 1; i++) {
|
||||||
|
ExtendedAttribute ea1 = getCell(i);
|
||||||
|
ExtendedAttribute ea2 = getCell(i + 1);
|
||||||
|
if (!ea1.isFieldAttribute() && !ea2.isFieldAttribute()) {
|
||||||
|
if ((ea1.ec & 0xFF) == 0x0E && (ea2.ec & 0xFF) == 0x0F) {
|
||||||
|
ea1.clear();
|
||||||
|
ea2.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Entry Assist & DOC Mode Operations ==========
|
||||||
|
|
||||||
|
public boolean isEntryAssistDOCmode() { return docMode; }
|
||||||
|
public void setEntryAssistDOCmode(boolean bl) { this.docMode = bl; }
|
||||||
|
|
||||||
|
public boolean isEntryAssistWordWrap() { return wordWrap; }
|
||||||
|
public void setEntryAssistWordWrap(boolean bl) { this.wordWrap = bl; }
|
||||||
|
|
||||||
|
public int getEntryAssistStartColumn() { return docStartCol; }
|
||||||
|
public void setEntryAssistStartColumn(int n) { this.docStartCol = Math.max(0, n); }
|
||||||
|
|
||||||
|
public int getEntryAssistEndColumn() { return docEndCol >= 0 ? docEndCol : (cols - 1); }
|
||||||
|
public void setEntryAssistEndColumn(int n) { this.docEndCol = n; }
|
||||||
|
|
||||||
|
public int[] getEntryAssistTabStops() { return tabStops; }
|
||||||
|
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
|
||||||
|
|
||||||
|
public synchronized void processWordTab(boolean forward) {
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return;
|
||||||
|
int cur = cursorAddress;
|
||||||
|
int curRow = cur / cols;
|
||||||
|
int curCol = cur % cols;
|
||||||
|
|
||||||
|
if (tabStops != null && tabStops.length > 0) {
|
||||||
|
if (forward) {
|
||||||
|
for (int stop : tabStops) {
|
||||||
|
if (stop > curCol && stop < cols) {
|
||||||
|
setCursorPosition(curRow, stop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int nextRow = (curRow + 1) % rows;
|
||||||
|
setCursorPosition(nextRow, tabStops[0]);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
for (int i = tabStops.length - 1; i >= 0; i--) {
|
||||||
|
int stop = tabStops[i];
|
||||||
|
if (stop < curCol && stop >= 0) {
|
||||||
|
setCursorPosition(curRow, stop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int prevRow = (curRow - 1 + rows) % rows;
|
||||||
|
setCursorPosition(prevRow, tabStops[tabStops.length - 1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Word Tab: Jump to next / prev word boundary
|
||||||
|
if (forward) {
|
||||||
|
int addr = cur;
|
||||||
|
int count = 0;
|
||||||
|
while (count < size && getChar(addr / cols, addr % cols) != ' ' && !getCell(addr).isFieldAttribute()) {
|
||||||
|
addr = incrementAddress(addr);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
while (count < size && (getChar(addr / cols, addr % cols) == ' ' || getCell(addr).isFieldAttribute())) {
|
||||||
|
addr = incrementAddress(addr);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
setCursorAddress(addr);
|
||||||
|
} else {
|
||||||
|
int addr = decrementAddress(cur);
|
||||||
|
int count = 0;
|
||||||
|
while (count < size && (getChar(addr / cols, addr % cols) == ' ' || getCell(addr).isFieldAttribute())) {
|
||||||
|
addr = decrementAddress(addr);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
while (count < size && getChar(decrementAddress(addr) / cols, decrementAddress(addr) % cols) != ' '
|
||||||
|
&& !getCell(decrementAddress(addr)).isFieldAttribute()) {
|
||||||
|
addr = decrementAddress(addr);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
setCursorAddress(addr);
|
||||||
|
}
|
||||||
|
updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void processDeleteWord() {
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return;
|
||||||
|
int cur = cursorAddress;
|
||||||
|
if (formatted) {
|
||||||
|
byte fa = getFieldAttributeAt(cur);
|
||||||
|
if (faIsProtected(fa & 0xFF)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int endWord = cur;
|
||||||
|
int count = 0;
|
||||||
|
while (count < size && !getCell(endWord).isFieldAttribute()) {
|
||||||
|
char ch = getChar(endWord / cols, endWord % cols);
|
||||||
|
endWord = incrementAddress(endWord);
|
||||||
|
count++;
|
||||||
|
if (ch == ' ') break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
deleteChar(cur);
|
||||||
|
}
|
||||||
|
screenChanged = true;
|
||||||
|
updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== DBCS Shift-Out / Shift-In Display Transformation ==========
|
||||||
|
|
||||||
|
public synchronized void processSOSI() {
|
||||||
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return;
|
||||||
|
boolean insideDBCS = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute ea = buffer[i];
|
||||||
|
if (ea.isFieldAttribute()) {
|
||||||
|
insideDBCS = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int ec = ea.ec & 0xFF;
|
||||||
|
if (ec == 0x0E) { // Shift-Out
|
||||||
|
insideDBCS = true;
|
||||||
|
ea.db = ExtendedAttribute.DB_SO;
|
||||||
|
ea.ucs4 = ' ';
|
||||||
|
} else if (ec == 0x0F) { // Shift-In
|
||||||
|
insideDBCS = false;
|
||||||
|
ea.db = ExtendedAttribute.DB_SI;
|
||||||
|
ea.ucs4 = ' ';
|
||||||
|
} else if (insideDBCS) {
|
||||||
|
int nextIdx = (i + 1) % size;
|
||||||
|
ExtendedAttribute nextEa = buffer[nextIdx];
|
||||||
|
if (!nextEa.isFieldAttribute() && (nextEa.ec & 0xFF) != 0x0F) {
|
||||||
|
int b1 = ec;
|
||||||
|
int b2 = nextEa.ec & 0xFF;
|
||||||
|
ea.cs = ExtendedAttribute.CS_DBCS;
|
||||||
|
ea.db = ExtendedAttribute.DB_LEFT;
|
||||||
|
nextEa.cs = ExtendedAttribute.CS_DBCS;
|
||||||
|
nextEa.db = ExtendedAttribute.DB_RIGHT;
|
||||||
|
|
||||||
|
if (translator != null && translator.isDBCS() && translator.getCodePage() != null) {
|
||||||
|
char uni = translator.getCodePage().dbcsToUnicode(b1, b2);
|
||||||
|
ea.ucs4 = uni;
|
||||||
|
nextEa.ucs4 = uni;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ea.db = ExtendedAttribute.DB_NONE;
|
||||||
|
if (ea.cs == ExtendedAttribute.CS_DBCS) {
|
||||||
|
ea.cs = ExtendedAttribute.CS_BASE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screenChanged = true;
|
||||||
|
updateDisplaySnapshot();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
package haus.nightmare.lib3270j.datastream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||||
|
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||||
|
import haus.nightmare.lib3270j.printer.PrintSCS3270;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprehensive test suite verifying all Phase 2 functional updates:
|
||||||
|
* 3270 Data Stream parsing, Graphic Escape & APL mapping, Partitioning,
|
||||||
|
* Structured Fields (0x0F, 0x20, 0x24, 0x40), and Query Reply builders.
|
||||||
|
*/
|
||||||
|
public class DataStreamProcessorPhase2FullTest {
|
||||||
|
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private DataStreamProcessor processor;
|
||||||
|
private ByteArrayOutputStream output;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||||
|
processor = new DataStreamProcessor(screen, translator);
|
||||||
|
output = new ByteArrayOutputStream();
|
||||||
|
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGraphicEscapeOrderWithAPLTranslation() {
|
||||||
|
// Write record: CMD_EW (0x05), WCC (0xC3), SBA to 0 (0x11, 0x40, 0x40), ORDER_GE (0x08), 0xA2 (s -> ─)
|
||||||
|
// followed by ORDER_GE, 0x85 (e -> │), ORDER_GE, 0xCB (Cross -> ┼), ORDER_GE, 0xBA (Omega -> Ω)
|
||||||
|
byte[] record = new byte[] {
|
||||||
|
(byte) CMD_EW, (byte) 0xC3,
|
||||||
|
(byte) ORDER_SBA, 0x40, 0x40,
|
||||||
|
(byte) ORDER_GE, (byte) 0xA2, // Horizontal Line
|
||||||
|
(byte) ORDER_GE, (byte) 0x85, // Vertical Line
|
||||||
|
(byte) ORDER_GE, (byte) 0xCB, // Cross
|
||||||
|
(byte) ORDER_GE, (byte) 0xBA // Greek Omega
|
||||||
|
};
|
||||||
|
|
||||||
|
processor.processRecord(record);
|
||||||
|
|
||||||
|
ExtendedAttribute cell0 = screen.getCell(0);
|
||||||
|
assertEquals(CS_GE, cell0.cs);
|
||||||
|
assertEquals((byte) 0xA2, cell0.ec);
|
||||||
|
assertEquals('─', cell0.ucs4);
|
||||||
|
|
||||||
|
ExtendedAttribute cell1 = screen.getCell(1);
|
||||||
|
assertEquals(CS_GE, cell1.cs);
|
||||||
|
assertEquals((byte) 0x85, cell1.ec);
|
||||||
|
assertEquals('│', cell1.ucs4);
|
||||||
|
|
||||||
|
ExtendedAttribute cell2 = screen.getCell(2);
|
||||||
|
assertEquals(CS_GE, cell2.cs);
|
||||||
|
assertEquals((byte) 0xCB, cell2.ec);
|
||||||
|
assertEquals('┼', cell2.ucs4);
|
||||||
|
|
||||||
|
ExtendedAttribute cell3 = screen.getCell(3);
|
||||||
|
assertEquals(CS_GE, cell3.cs);
|
||||||
|
assertEquals((byte) 0xBA, cell3.ec);
|
||||||
|
assertEquals('Ω', cell3.ucs4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRepeatToAddressWithGraphicEscape() {
|
||||||
|
// Repeat to address 5 with ORDER_GE prefix + 0xA2 ('─')
|
||||||
|
byte[] record = new byte[] {
|
||||||
|
(byte) CMD_EW, (byte) 0xC3,
|
||||||
|
(byte) ORDER_SBA, 0x40, 0x40, // Addr 0
|
||||||
|
(byte) ORDER_RA, 0x40, (byte) 0x45, // Addr 5
|
||||||
|
(byte) ORDER_GE, (byte) 0xA2
|
||||||
|
};
|
||||||
|
|
||||||
|
processor.processRecord(record);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
ExtendedAttribute cell = screen.getCell(i);
|
||||||
|
assertEquals(CS_GE, cell.cs, "Cell " + i + " should have CS_GE");
|
||||||
|
assertEquals('─', cell.ucs4, "Cell " + i + " should have '─'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOutbound3270DSNonZeroPartition() {
|
||||||
|
// First create partition 2 (32 rows x 80 cols)
|
||||||
|
byte[] createPart = new byte[] {
|
||||||
|
(byte) CMD_WSF,
|
||||||
|
0x00, 0x08,
|
||||||
|
(byte) SF_CREATE_PART, 0x02,
|
||||||
|
0x00, 80,
|
||||||
|
0x00, 32
|
||||||
|
};
|
||||||
|
processor.processRecord(createPart);
|
||||||
|
assertEquals(2, screen.getActivePartition());
|
||||||
|
|
||||||
|
// Now activate partition 0
|
||||||
|
screen.setActivePartition(0);
|
||||||
|
assertEquals(0, screen.getActivePartition());
|
||||||
|
|
||||||
|
// Send Outbound 3270DS targeting Partition 2 containing Write command to write 'H' at pos 0
|
||||||
|
byte[] outboundDs = new byte[] {
|
||||||
|
(byte) CMD_WSF,
|
||||||
|
0x00, 0x0A, // Field length = 10
|
||||||
|
(byte) SF_OUTBOUND_DS, 0x02, // Target PID = 2
|
||||||
|
(byte) CMD_W, 0x00, // Write command with null WCC
|
||||||
|
(byte) ORDER_SBA, 0x40, 0x40, // Pos 0
|
||||||
|
(byte) 0xC8 // EBCDIC 'H'
|
||||||
|
};
|
||||||
|
|
||||||
|
processor.processRecord(outboundDs);
|
||||||
|
|
||||||
|
// Verify active partition was switched to PID 2
|
||||||
|
assertEquals(2, screen.getActivePartition());
|
||||||
|
assertEquals('H', screen.getCell(0).ucs4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStructuredField0x0FSetWindow() {
|
||||||
|
// SF 0x0F with explicit viewport window (xmin=10, ymin=20, xmax=500, ymax=300)
|
||||||
|
byte[] setWindowSf = new byte[] {
|
||||||
|
(byte) CMD_WSF,
|
||||||
|
0x00, 0x0B, // Length = 11
|
||||||
|
(byte) SF_SET_WINDOW,
|
||||||
|
0x00, 10, // xMin = 10
|
||||||
|
0x00, 20, // yMin = 20
|
||||||
|
0x01, (byte) 0xF4, // xMax = 500
|
||||||
|
0x01, 0x2C // yMax = 300
|
||||||
|
};
|
||||||
|
|
||||||
|
processor.processRecord(setWindowSf);
|
||||||
|
|
||||||
|
// Test direct overload processSFSetWindow
|
||||||
|
processor.processSFSetWindow(setWindowSf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStructuredField0x0FDataUnitActivatesGraphicCursor() {
|
||||||
|
assertFalse(processor.getGocaDecoder().isGraphicsCursorActive());
|
||||||
|
|
||||||
|
byte[] objDataSf = new byte[] {
|
||||||
|
(byte) CMD_WSF,
|
||||||
|
0x00, 0x04,
|
||||||
|
(byte) SF_SET_WINDOW,
|
||||||
|
(byte) GocaConstants.SF_OBJDATA_SUB // 0x0F
|
||||||
|
};
|
||||||
|
|
||||||
|
processor.processRecord(objDataSf);
|
||||||
|
assertTrue(processor.getGocaDecoder().isGraphicsCursorActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStructuredField0x20ObjectControl() {
|
||||||
|
// SF 0x20 (3270 Graphics / Object Control) with GOCA begin/end area orders
|
||||||
|
byte[] objControl = new byte[] {
|
||||||
|
(byte) CMD_WSF,
|
||||||
|
0x00, 0x07,
|
||||||
|
(byte) SF_OBJECT_CONTROL,
|
||||||
|
(byte) GocaConstants.G_GBAR, 0x00, // Begin area
|
||||||
|
(byte) GocaConstants.G_GEAR, 0x00 // End area
|
||||||
|
};
|
||||||
|
|
||||||
|
processor.processRecord(objControl);
|
||||||
|
processor.processSFObjectControl(objControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStructuredField0x24DocumentDataWithEmbeddedSCS() {
|
||||||
|
AtomicReference<byte[]> scsCaptured = new AtomicReference<>();
|
||||||
|
PrintSCS3270 mockScs = new PrintSCS3270(null, null, translator) {
|
||||||
|
@Override
|
||||||
|
public void processHostData(byte[] data, int offset, int length) {
|
||||||
|
byte[] b = new byte[length];
|
||||||
|
System.arraycopy(data, offset, b, 0, length);
|
||||||
|
scsCaptured.set(b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
processor.setEmbeddedScsProcessor(mockScs);
|
||||||
|
|
||||||
|
// SF 0x24 (Document Data) with 4 bytes of SCS printer data
|
||||||
|
byte[] docData = new byte[] {
|
||||||
|
(byte) CMD_WSF,
|
||||||
|
0x00, 0x07,
|
||||||
|
(byte) SF_DOCUMENT_DATA,
|
||||||
|
(byte) 0x15, (byte) 0xC8, (byte) 0xC9, (byte) 0x15 // NL, 'H', 'I', NL
|
||||||
|
};
|
||||||
|
|
||||||
|
processor.processRecord(docData);
|
||||||
|
assertNotNull(scsCaptured.get());
|
||||||
|
assertEquals(4, scsCaptured.get().length);
|
||||||
|
assertEquals(0x15, scsCaptured.get()[0]);
|
||||||
|
assertEquals((byte) 0xC8, scsCaptured.get()[1]);
|
||||||
|
|
||||||
|
// Test direct method
|
||||||
|
processor.processSFDocumentData(docData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQueryReplyBuilderGraphicColor109Bytes() {
|
||||||
|
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||||
|
byte[] grColor = qrBuilder.buildGraphicColor();
|
||||||
|
|
||||||
|
assertNotNull(grColor);
|
||||||
|
assertEquals(105, grColor.length); // 105 bytes payload + 4 bytes header = 109 bytes in complete SF
|
||||||
|
|
||||||
|
// Check header bytes in payload: 0x00, 0x04, 0x00, 0xFF, 0xFF, 0x00, 0x10, 0x00, 0x10
|
||||||
|
assertEquals(0x00, grColor[0]);
|
||||||
|
assertEquals(0x04, grColor[1]);
|
||||||
|
assertEquals(0x00, grColor[2]);
|
||||||
|
assertEquals((byte) 0xFF, grColor[3]);
|
||||||
|
assertEquals((byte) 0xFF, grColor[4]);
|
||||||
|
assertEquals(0x00, grColor[5]);
|
||||||
|
assertEquals(0x10, grColor[6]);
|
||||||
|
assertEquals(0x00, grColor[7]);
|
||||||
|
assertEquals(0x10, grColor[8]);
|
||||||
|
|
||||||
|
// Check aliases
|
||||||
|
assertArrayEquals(grColor, qrBuilder.buildGrColor());
|
||||||
|
assertArrayEquals(grColor, qrBuilder.buildGColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQueryReplyBuilderAuxDeviceAndLineType() {
|
||||||
|
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||||
|
byte[] lineType = qrBuilder.buildAuxDevice();
|
||||||
|
|
||||||
|
assertNotNull(lineType);
|
||||||
|
assertEquals(20, lineType.length); // 20 bytes payload + 4 bytes header = 24 bytes
|
||||||
|
|
||||||
|
assertArrayEquals(lineType, qrBuilder.buildLineType());
|
||||||
|
assertArrayEquals(lineType, qrBuilder.buildAuxDev());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQueryReplyBuilderOemFormatAndPort() {
|
||||||
|
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||||
|
byte[] port = qrBuilder.buildOemFormat();
|
||||||
|
|
||||||
|
assertNotNull(port);
|
||||||
|
assertTrue(port.length > 0);
|
||||||
|
assertArrayEquals(port, qrBuilder.buildPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQueryReplyBuilderUsableAreaAspectRatiosAndNoArgOverloads() {
|
||||||
|
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||||
|
|
||||||
|
// 1. Vector Graphics enabled (3179G standard: SDH = 16, flags = 0x03)
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||||
|
byte[] uaVector = qrBuilder.buildUsableArea();
|
||||||
|
assertNotNull(uaVector);
|
||||||
|
assertEquals(19, uaVector.length);
|
||||||
|
assertEquals(0x03, uaVector[0] & 0xFF); // Flags: 12/14 bit + Graphics
|
||||||
|
assertEquals(9, uaVector[15] & 0xFF); // AW = 9
|
||||||
|
assertEquals(16, uaVector[16] & 0xFF); // AH = 16
|
||||||
|
|
||||||
|
// 2. Text mode (GraphicsMode.NONE: SDH = 12, flags = 0x01)
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
|
||||||
|
byte[] uaText = qrBuilder.buildUsableArea();
|
||||||
|
assertNotNull(uaText);
|
||||||
|
assertEquals(0x01, uaText[0] & 0xFF); // Flags: 12/14 bit only
|
||||||
|
assertEquals(9, uaText[15] & 0xFF); // AW = 9
|
||||||
|
assertEquals(12, uaText[16] & 0xFF); // AH = 12
|
||||||
|
|
||||||
|
// 3. No-arg overloads check
|
||||||
|
assertNotNull(qrBuilder.buildSummary());
|
||||||
|
assertNotNull(qrBuilder.buildAlphaPartitions());
|
||||||
|
assertNotNull(qrBuilder.buildCharsets());
|
||||||
|
assertNotNull(qrBuilder.buildColor());
|
||||||
|
assertNotNull(qrBuilder.buildHighlighting());
|
||||||
|
assertNotNull(qrBuilder.buildReplyModes());
|
||||||
|
assertNotNull(qrBuilder.buildDdm());
|
||||||
|
assertNotNull(qrBuilder.buildImplicitPartition());
|
||||||
|
assertNotNull(qrBuilder.buildSegment());
|
||||||
|
assertNotNull(qrBuilder.buildProcedure());
|
||||||
|
assertNotNull(qrBuilder.buildGraphics());
|
||||||
|
assertNotNull(qrBuilder.buildGImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQueryReplyBuilderColorTable18Bytes() {
|
||||||
|
QueryReplyBuilder qrBuilder = processor.getQueryReplyBuilder();
|
||||||
|
byte[] colorTable = qrBuilder.buildColor();
|
||||||
|
|
||||||
|
assertNotNull(colorTable);
|
||||||
|
assertEquals(18, colorTable.length, "Color table payload must be exactly 18 bytes matching HoD DS3270.java:1850");
|
||||||
|
assertEquals(0x00, colorTable[0]);
|
||||||
|
assertEquals(0x08, colorTable[1]);
|
||||||
|
assertEquals(0x00, colorTable[2]);
|
||||||
|
assertEquals((byte) 0xF4, colorTable[3]); // Default green
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDataStreamProcessorConvenienceOverloads() {
|
||||||
|
// Test processEraseAllUnprotected
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.getCell(1).ucs4 = 'X';
|
||||||
|
screen.getCell(1).ec = (byte) 0xE7;
|
||||||
|
processor.processEraseAllUnprotected();
|
||||||
|
assertEquals(0, screen.getCell(1).ucs4);
|
||||||
|
|
||||||
|
// Test processReadModified and processReadModifiedAll
|
||||||
|
output.reset();
|
||||||
|
processor.processReadModified();
|
||||||
|
assertTrue(output.size() >= 3);
|
||||||
|
|
||||||
|
output.reset();
|
||||||
|
processor.processReadModifiedAll();
|
||||||
|
assertTrue(output.size() >= 3);
|
||||||
|
|
||||||
|
// Test processReadBuffer
|
||||||
|
output.reset();
|
||||||
|
processor.processReadBuffer();
|
||||||
|
assertTrue(output.size() >= 3);
|
||||||
|
|
||||||
|
// Test processEraseWrite and processEraseWriteAlternate
|
||||||
|
byte[] ewData = new byte[] { (byte) CMD_EW, 0x00, (byte) ORDER_SBA, 0x40, 0x40, (byte) 0xC1 };
|
||||||
|
processor.processEraseWrite(ewData);
|
||||||
|
assertEquals('A', screen.getCell(0).ucs4);
|
||||||
|
|
||||||
|
byte[] ewaData = new byte[] { (byte) CMD_EWA, 0x00, (byte) ORDER_SBA, 0x40, 0x40, (byte) 0xC2 };
|
||||||
|
processor.processEraseWriteAlternate(ewaData);
|
||||||
|
assertEquals('B', screen.getCell(0).ucs4);
|
||||||
|
|
||||||
|
// Test processSetReplyMode, processCreatePartition, processEraseReset byte[] overloads
|
||||||
|
byte[] srm = new byte[] { (byte) CMD_WSF, 0x00, 0x05, (byte) SF_SET_REPLY_MODE, 0x00, (byte) SF_SRM_CHAR };
|
||||||
|
processor.processSetReplyMode(srm);
|
||||||
|
assertEquals((byte) SF_SRM_CHAR, screen.getReplyMode());
|
||||||
|
|
||||||
|
byte[] cp = new byte[] { (byte) CMD_WSF, 0x00, 0x08, (byte) SF_CREATE_PART, 0x01, 0x00, 80, 0x00, 24 };
|
||||||
|
processor.processCreatePartition(cp);
|
||||||
|
assertEquals(1, screen.getActivePartition());
|
||||||
|
|
||||||
|
byte[] er = new byte[] { (byte) CMD_WSF, 0x00, 0x04, (byte) SF_ERASE_RESET, (byte) SF_ER_DEFAULT };
|
||||||
|
processor.processEraseReset(er);
|
||||||
|
assertEquals(0, screen.getActivePartition());
|
||||||
|
|
||||||
|
// Test query partition overloads
|
||||||
|
output.reset();
|
||||||
|
processor.processSFReadPartitionQuery(new byte[] { (byte) CMD_WSF, 0x00, 0x05, (byte) SF_READ_PART, 0x00, (byte) SF_RP_QUERY });
|
||||||
|
assertTrue(output.size() > 0);
|
||||||
|
|
||||||
|
output.reset();
|
||||||
|
processor.processSFReadPartitionQueryList(new byte[] {
|
||||||
|
(byte) CMD_WSF, 0x00, 0x07, (byte) SF_READ_PART, 0x00, (byte) SF_RP_QLIST, (byte) SF_RPQ_LIST, (byte) QR_COLOR
|
||||||
|
});
|
||||||
|
assertTrue(output.size() > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
public class ECLOIAPhase3Test {
|
||||||
|
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private InputProcessor input;
|
||||||
|
private ECLOIA oia;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||||
|
input = new InputProcessor(screen, translator, null);
|
||||||
|
oia = new ECLOIA(screen, input, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlphanumericTypeDetection() {
|
||||||
|
screen.erase(false);
|
||||||
|
// Unformatted screen
|
||||||
|
assertEquals(TYPE_ALPHANUMERIC, oia.getAlphanumericType());
|
||||||
|
assertEquals("A", oia.getAlphanumericTypeString());
|
||||||
|
|
||||||
|
// Numeric field
|
||||||
|
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_NUMERIC));
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
assertEquals(TYPE_NUMERIC, oia.getAlphanumericType());
|
||||||
|
assertEquals("N", oia.getAlphanumericTypeString());
|
||||||
|
|
||||||
|
// Alphanumeric unprotected field
|
||||||
|
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
|
||||||
|
screen.setCursorAddress(11);
|
||||||
|
assertEquals(TYPE_ALPHANUMERIC, oia.getAlphanumericType());
|
||||||
|
assertEquals("A", oia.getAlphanumericTypeString());
|
||||||
|
|
||||||
|
// DBCS cell
|
||||||
|
ExtendedAttribute ea = screen.getCell(11);
|
||||||
|
ea.cs = ExtendedAttribute.CS_DBCS;
|
||||||
|
assertEquals(TYPE_DBCS, oia.getAlphanumericType());
|
||||||
|
assertEquals("D", oia.getAlphanumericTypeString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOIAStatusFlagsAndStrings() {
|
||||||
|
screen.erase(false);
|
||||||
|
assertEquals("READY", oia.getStatusString());
|
||||||
|
assertFalse(oia.isXSystem());
|
||||||
|
assertFalse(oia.isXProt());
|
||||||
|
assertFalse(oia.isXNum());
|
||||||
|
assertFalse(oia.isXComm());
|
||||||
|
assertFalse(oia.isXOverflow());
|
||||||
|
assertFalse(oia.isXOperatorDue());
|
||||||
|
|
||||||
|
// Keyboard lock -> X-SYSTEM
|
||||||
|
input.setKeyboardLocked(true);
|
||||||
|
assertTrue(oia.isXSystem());
|
||||||
|
assertTrue(oia.isXWait());
|
||||||
|
assertEquals("X-SYSTEM", oia.getStatusString());
|
||||||
|
input.setKeyboardLocked(false);
|
||||||
|
|
||||||
|
// Protected field inhibit
|
||||||
|
oia.setInputInhibited(INHIBIT_PROTECTED_FIELD);
|
||||||
|
assertTrue(oia.isXProt());
|
||||||
|
assertEquals("X-PROT", oia.getStatusString());
|
||||||
|
|
||||||
|
// Numeric only inhibit
|
||||||
|
oia.setInputInhibited(INHIBIT_NUMERIC_ONLY);
|
||||||
|
assertTrue(oia.isXNum());
|
||||||
|
assertEquals("X-NUM", oia.getStatusString());
|
||||||
|
|
||||||
|
// Overflow inhibit
|
||||||
|
oia.setInputInhibited(INHIBIT_OVERFLOW);
|
||||||
|
assertTrue(oia.isXOverflow());
|
||||||
|
assertEquals("X-OVERFLOW", oia.getStatusString());
|
||||||
|
|
||||||
|
// Comm check inhibit
|
||||||
|
oia.setInputInhibited(INHIBIT_COMM_CHECK);
|
||||||
|
assertTrue(oia.isXComm());
|
||||||
|
assertEquals("X-COMM", oia.getStatusString());
|
||||||
|
|
||||||
|
// Operator due inhibit
|
||||||
|
oia.setInputInhibited(INHIBIT_OPERATOR_DUE);
|
||||||
|
assertTrue(oia.isXOperatorDue());
|
||||||
|
assertEquals("X-OP", oia.getStatusString());
|
||||||
|
|
||||||
|
// Insert mode
|
||||||
|
oia.setInputInhibited(INHIBIT_NOT_INHIBITED);
|
||||||
|
input.setInsertMode(true);
|
||||||
|
assertTrue(oia.isXInsert());
|
||||||
|
assertEquals("X-INSERT", oia.getStatusString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testECLFieldOperations() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setChar(0, 1, 'X');
|
||||||
|
screen.setChar(0, 2, 'Y');
|
||||||
|
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
|
||||||
|
ECLPS ps = new ECLPS(screen, input, translator);
|
||||||
|
ECLField f = ps.getFieldList().getFirstField();
|
||||||
|
assertNotNull(f);
|
||||||
|
|
||||||
|
assertEquals("XY ", f.getText());
|
||||||
|
assertFalse(f.isModified());
|
||||||
|
|
||||||
|
f.setModified(true);
|
||||||
|
assertTrue(f.isModified());
|
||||||
|
|
||||||
|
f.erase();
|
||||||
|
assertEquals(" ", f.getText());
|
||||||
|
assertFalse(f.isModified());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
public class ECLPSPhase3Test implements ECLConstants {
|
||||||
|
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private InputProcessor input;
|
||||||
|
private ECLPS ps;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||||
|
input = new InputProcessor(screen, translator, null);
|
||||||
|
ps = new ECLPS(screen, input, translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchPSOverloadsAndCaseOptions() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setText("Host On-Demand ECL Automation");
|
||||||
|
|
||||||
|
// 1-based SearchPS
|
||||||
|
assertEquals(6, ps.SearchPS("On-Demand"));
|
||||||
|
assertEquals(6, ps.SearchPS("on-demand", 1, 1, SEARCH_FORWARD, true));
|
||||||
|
assertEquals(0, ps.SearchPS("on-demand", 1, 1, SEARCH_FORWARD, false));
|
||||||
|
assertEquals(0, ps.SearchPS("NONEXISTENT"));
|
||||||
|
|
||||||
|
// SearchPSExt
|
||||||
|
int pos = ps.SearchPSExt("ECL", 1, 50, SEARCH_FORWARD, false, false);
|
||||||
|
assertEquals(16, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchStringBackwardsAndWrap() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setChar(0, 10, 'A');
|
||||||
|
screen.setChar(0, 11, 'B');
|
||||||
|
screen.setChar(0, 12, 'C');
|
||||||
|
|
||||||
|
screen.setChar(1, 20, 'A');
|
||||||
|
screen.setChar(1, 21, 'B');
|
||||||
|
screen.setChar(1, 22, 'C');
|
||||||
|
|
||||||
|
// Backward search starting from row 1 col 0 (addr 80) should find "ABC" at row 0 col 10 (addr 10)
|
||||||
|
int found = ps.searchString("ABC", 1, 0, SEARCH_BACKWARD, false);
|
||||||
|
assertEquals(10, found);
|
||||||
|
|
||||||
|
// Backward search starting from row 0 col 5 (addr 5) wraps around and finds "ABC" at row 1 col 20 (addr 100)
|
||||||
|
int wrapped = ps.searchString("ABC", 0, 5, SEARCH_BACKWARD, false);
|
||||||
|
assertEquals(100, wrapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCopyStringRectangularExtraction() {
|
||||||
|
screen.erase(false);
|
||||||
|
// Row 0: "0123456789"
|
||||||
|
for (int c = 0; c < 10; c++) {
|
||||||
|
screen.setChar(0, c, (char) ('0' + c));
|
||||||
|
}
|
||||||
|
// Row 1: "ABCDEFGHIJ"
|
||||||
|
for (int c = 0; c < 10; c++) {
|
||||||
|
screen.setChar(1, c, (char) ('A' + c));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy columns 2..5 of rows 0..1
|
||||||
|
String block = ps.copyString(0, 2, 1, 5);
|
||||||
|
assertEquals("2345\nCDEF", block);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPasteStringRectangular() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
|
||||||
|
|
||||||
|
int pasted = ps.pasteString("HELLO\nWORLD", 0, 1);
|
||||||
|
assertEquals(10, pasted);
|
||||||
|
|
||||||
|
assertEquals("HELLO", ps.getString(1, 5));
|
||||||
|
assertEquals("WORLD", ps.getString(81, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSendCharactersWithDelay() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
ps.sendCharacters("ABC[tab]DEF", 2);
|
||||||
|
assertEquals('A', screen.getChar(0, 1));
|
||||||
|
assertEquals('B', screen.getChar(0, 2));
|
||||||
|
assertEquals('C', screen.getChar(0, 3));
|
||||||
|
assertEquals('D', screen.getChar(0, 11));
|
||||||
|
assertEquals('E', screen.getChar(0, 12));
|
||||||
|
assertEquals('F', screen.getChar(0, 13));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWaitForScreenAndCursor() {
|
||||||
|
screen.erase(false);
|
||||||
|
assertFalse(ps.waitForScreen("READY", 50));
|
||||||
|
assertFalse(ps.waitForCursor(5, 10, 50));
|
||||||
|
|
||||||
|
screen.setText("READY");
|
||||||
|
assertTrue(ps.waitForScreen("READY", 50));
|
||||||
|
assertTrue(ps.waitForScreen("READY", 0, 0, 50));
|
||||||
|
assertFalse(ps.waitForScreen("READY", 1, 0, 50));
|
||||||
|
|
||||||
|
ps.setCursorPos(5, 10);
|
||||||
|
assertTrue(ps.waitForCursor(5, 10, 50));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -288,6 +288,199 @@ public class GocaDecoderPhase5Test {
|
|||||||
assertEquals(56, sf2.length);
|
assertEquals(56, sf2.length);
|
||||||
assertEquals(0x00, sf2[0]);
|
assertEquals(0x00, sf2[0]);
|
||||||
assertEquals(0x34, sf2[1]);
|
assertEquals(0x34, sf2[1]);
|
||||||
|
|
||||||
|
byte[] sf3 = GraphicInputBuilder.buildPickCorrelation(120, 240, 5);
|
||||||
|
assertEquals(56, sf3.length);
|
||||||
|
assertEquals(120, ((sf3[24] & 0xFF) << 8) | (sf3[25] & 0xFF));
|
||||||
|
assertEquals(240, ((sf3[26] & 0xFF) << 8) | (sf3[27] & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testViewingWindowClipping() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// Define viewing window covering center of presentation space: [-100..100, -100..100]
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(GocaConstants.G_GSVW_DEF);
|
||||||
|
out.write(0x08);
|
||||||
|
out.write((byte) 0xFF); out.write((byte) 0x9C); // xMin = -100
|
||||||
|
out.write((byte) 0xFF); out.write((byte) 0x9C); // yMin = -100
|
||||||
|
out.write(0x00); out.write(100); // xMax = 100
|
||||||
|
out.write(0x00); out.write(100); // yMax = 100
|
||||||
|
|
||||||
|
// Draw a line inside presentation space but outside the viewing window: (-300, -150) -> (-200, -150)
|
||||||
|
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red
|
||||||
|
out.write(GocaConstants.G_GLINE); out.write(0x08);
|
||||||
|
out.write((byte) 0xFE); out.write((byte) 0xD4); out.write((byte) 0xFF); out.write((byte) 0x6A); // (-300, -150)
|
||||||
|
out.write((byte) 0xFF); out.write((byte) 0x38); out.write((byte) 0xFF); out.write((byte) 0x6A); // (-200, -150)
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
// Pixels outside viewing window should have been clipped out
|
||||||
|
int pxClipped = plane.mapX(-300);
|
||||||
|
int pyClipped = plane.mapY(-150);
|
||||||
|
int pixelOutside = plane.getRgbBuffer()[pyClipped * plane.getCanvasWidth() + pxClipped];
|
||||||
|
assertEquals(0, pixelOutside, "Pixel outside viewing window must be 0 (clipped)");
|
||||||
|
|
||||||
|
// Now draw a line crossing the center (0, 0) -> (50, 50)
|
||||||
|
ByteArrayOutputStream insideStream = new ByteArrayOutputStream();
|
||||||
|
insideStream.write(GocaConstants.G_GLINE); insideStream.write(0x08);
|
||||||
|
insideStream.write(0x00); insideStream.write(0); insideStream.write(0x00); insideStream.write(0);
|
||||||
|
insideStream.write(0x00); insideStream.write(50); insideStream.write(0x00); insideStream.write(50);
|
||||||
|
|
||||||
|
byte[] inBytes = insideStream.toByteArray();
|
||||||
|
decoder.decodeGoca(inBytes, 0, inBytes.length);
|
||||||
|
|
||||||
|
int pxInside = plane.mapX(0);
|
||||||
|
int pyInside = plane.mapY(0);
|
||||||
|
int pixelInside = plane.getRgbBuffer()[pyInside * plane.getCanvasWidth() + pxInside];
|
||||||
|
assertNotEquals(0, pixelInside, "Pixel inside viewing window must be drawn");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFractionalLineWidth() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
// Set Fractional Line Width: 2.5 (0x02 0x80)
|
||||||
|
out.write(GocaConstants.G_GSFLW);
|
||||||
|
out.write(0x02);
|
||||||
|
out.write(0x80);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertEquals(2.5, decoder.getFractionalLineWidth(), 0.01);
|
||||||
|
assertEquals(2.5, plane.getFractionalLineWidth(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSegmentCharacteristics() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
// Flags = 0xC0: Chained (0x80), Dynamic (0x40), Visible (0x00)
|
||||||
|
decoder.processSegmentCharacteristics(new byte[]{(byte) 0xC0});
|
||||||
|
assertTrue(decoder.isSegChained());
|
||||||
|
assertTrue(decoder.isSegDynamic());
|
||||||
|
assertTrue(decoder.isSegVisible());
|
||||||
|
|
||||||
|
// Flags = 0x20: Invisible
|
||||||
|
decoder.processSegmentCharacteristics(new byte[]{(byte) 0x20});
|
||||||
|
assertFalse(decoder.isSegChained());
|
||||||
|
assertFalse(decoder.isSegDynamic());
|
||||||
|
assertFalse(decoder.isSegVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCharacterAngleAndShear() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
GocaDecoder decoder = new GocaDecoder(plane);
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
// Set Character Angle: 45 degrees vector (ax=10, ay=10)
|
||||||
|
out.write(GocaConstants.G_GSCA);
|
||||||
|
out.write(0x04);
|
||||||
|
out.write(0x00); out.write(10);
|
||||||
|
out.write(0x00); out.write(10);
|
||||||
|
|
||||||
|
// Set Character Shear: vector (sx=10, sy=0)
|
||||||
|
out.write(GocaConstants.G_GSCR);
|
||||||
|
out.write(0x04);
|
||||||
|
out.write(0x00); out.write(10);
|
||||||
|
out.write(0x00); out.write(0);
|
||||||
|
|
||||||
|
byte[] stream = out.toByteArray();
|
||||||
|
decoder.decodeGoca(stream, 0, stream.length);
|
||||||
|
|
||||||
|
assertEquals(45.0, decoder.getCharAngle(), 0.01);
|
||||||
|
assertEquals(90.0, decoder.getCharShear(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWindingRuleAreaFill() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
FillArea fillArea = new FillArea();
|
||||||
|
fillArea.setFillRule(GocaConstants.FILL_RULE_WINDING);
|
||||||
|
assertEquals(GocaConstants.FILL_RULE_WINDING, fillArea.getFillRule());
|
||||||
|
|
||||||
|
// Fill self-intersecting bow-tie polygon
|
||||||
|
int[] px = new int[]{50, 150, 50, 150};
|
||||||
|
int[] py = new int[]{50, 150, 150, 50};
|
||||||
|
fillArea.fill(plane, px, py, 4, 0xFF00FF00, GocaConstants.PT_SOLID, false, 0, 0, 0, 2, 0);
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiBitAndCompressedImages() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||||
|
|
||||||
|
// Test 2-bit per pixel image
|
||||||
|
byte[] twoBppData = new byte[]{(byte) 0b00011011}; // 4 pixels: 0, 1 (Blue), 2 (Red), 3 (Green)
|
||||||
|
plane.drawImage(10, 10, 4, 1, twoBppData, 0xFFFFFFFF, GocaConstants.BPP_2, GocaConstants.IMG_UNCOMPRESSED);
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
|
||||||
|
// Test 4-bit per pixel image
|
||||||
|
byte[] fourBppData = new byte[]{(byte) 0x12, (byte) 0x34}; // 4 pixels: 1, 2, 3, 4
|
||||||
|
plane.clear();
|
||||||
|
plane.drawImage(10, 10, 4, 1, fourBppData, 0xFFFFFFFF, GocaConstants.BPP_4, GocaConstants.IMG_UNCOMPRESSED);
|
||||||
|
assertTrue(plane.hasContent());
|
||||||
|
|
||||||
|
// Test RLE decompressed image
|
||||||
|
byte[] rleData = new byte[]{
|
||||||
|
0x04, (byte) 0xFF // 4 repeats of 0xFF
|
||||||
|
};
|
||||||
|
byte[] decompressed = GraphicsPlane.decompressGocaRle(rleData, 32, 1, GocaConstants.BPP_1);
|
||||||
|
assertEquals(4, decompressed.length);
|
||||||
|
assertEquals((byte) 0xFF, decompressed[0]);
|
||||||
|
assertEquals((byte) 0xFF, decompressed[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiColorProgrammedSymbolsOverlay() {
|
||||||
|
ProgramSymbolManager psm = new ProgramSymbolManager(9, 16);
|
||||||
|
|
||||||
|
// Load Red plane (colorPlane = 1) into Slot 4 (LCID 0x41, Start 0x40, RWS 0x04)
|
||||||
|
byte[] loadRed = new byte[4 + 6 + 18];
|
||||||
|
loadRed[0] = (byte) 0x81; // Extended header present (0x80) | Format 1
|
||||||
|
loadRed[1] = 0x41; // LCID
|
||||||
|
loadRed[2] = 0x40; // Start codepoint
|
||||||
|
loadRed[3] = 0x04; // RWS Slot 4 (Triple Plane)
|
||||||
|
loadRed[4] = 0x06; // Ext header length = 6
|
||||||
|
loadRed[5] = 0x00;
|
||||||
|
loadRed[6] = 9; // Cell width = 9
|
||||||
|
loadRed[7] = 16; // Cell height = 16
|
||||||
|
loadRed[8] = 0x00;
|
||||||
|
loadRed[9] = 0x01; // Plane = Red (1)
|
||||||
|
for (int i = 0; i < 18; i++) loadRed[10 + i] = (byte) 0xFF;
|
||||||
|
|
||||||
|
psm.loadps(loadRed);
|
||||||
|
|
||||||
|
// Load Green plane (colorPlane = 2) into the same slot without clearing (clearAll = false)
|
||||||
|
byte[] loadGreen = new byte[4 + 6 + 18];
|
||||||
|
loadGreen[0] = (byte) 0x81;
|
||||||
|
loadGreen[1] = 0x41;
|
||||||
|
loadGreen[2] = 0x40;
|
||||||
|
loadGreen[3] = 0x04;
|
||||||
|
loadGreen[4] = 0x06;
|
||||||
|
loadGreen[5] = 0x00;
|
||||||
|
loadGreen[6] = 9;
|
||||||
|
loadGreen[7] = 16;
|
||||||
|
loadGreen[8] = 0x00;
|
||||||
|
loadGreen[9] = 0x02; // Plane = Green (2)
|
||||||
|
for (int i = 0; i < 18; i++) loadGreen[10 + i] = (byte) 0xFF;
|
||||||
|
|
||||||
|
psm.loadps(loadGreen);
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x41, 0x40);
|
||||||
|
assertNotNull(slot);
|
||||||
|
byte[] pixels = slot.getPixelData();
|
||||||
|
assertNotNull(pixels);
|
||||||
|
// Pixels should be composite of Red (1) | Green (2) = Yellow (3)
|
||||||
|
assertEquals(3, pixels[0] & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte outCoord(int val) {
|
private static byte outCoord(int val) {
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package haus.nightmare.lib3270j.input;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class InputProcessorPhase4Test {
|
||||||
|
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private InputProcessor inputProcessor;
|
||||||
|
private ECLOIA oia;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
|
||||||
|
inputProcessor = new InputProcessor(screen, translator, null);
|
||||||
|
oia = new ECLOIA(screen, inputProcessor, null);
|
||||||
|
inputProcessor.setOIA(oia);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNumericOnlyValidation() {
|
||||||
|
// Setup numeric field at pos 0 (FA), data at 1..9
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_NUMERIC);
|
||||||
|
screen.setFieldAttribute(10, (byte) FA_PROTECT); // terminate field
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
// Typing valid digits and punctuation
|
||||||
|
inputProcessor.typeCharacter('1');
|
||||||
|
assertEquals(2, screen.getCursorAddress());
|
||||||
|
assertEquals('1', (char) screen.getCell(1).ucs4);
|
||||||
|
assertFalse(inputProcessor.isKeyboardLocked());
|
||||||
|
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||||
|
|
||||||
|
inputProcessor.typeCharacter('-');
|
||||||
|
assertEquals(3, screen.getCursorAddress());
|
||||||
|
assertEquals('-', (char) screen.getCell(2).ucs4);
|
||||||
|
|
||||||
|
inputProcessor.typeCharacter('.');
|
||||||
|
assertEquals(4, screen.getCursorAddress());
|
||||||
|
assertEquals('.', (char) screen.getCell(3).ucs4);
|
||||||
|
|
||||||
|
inputProcessor.typeCharacter(' ');
|
||||||
|
assertEquals(5, screen.getCursorAddress());
|
||||||
|
|
||||||
|
// Typing invalid non-numeric character 'A' in numeric field
|
||||||
|
inputProcessor.typeCharacter('A');
|
||||||
|
assertTrue(inputProcessor.isKeyboardLocked());
|
||||||
|
assertEquals(ECLConstants.INHIBIT_NUMERIC_ONLY, oia.getInputInhibited());
|
||||||
|
assertEquals("X-NUM", oia.getStatusString());
|
||||||
|
// Buffer position 5 should NOT have changed
|
||||||
|
assertEquals(5, screen.getCursorAddress());
|
||||||
|
assertEquals(0, screen.getCell(5).ec);
|
||||||
|
|
||||||
|
// Reset clears error and unlocks keyboard
|
||||||
|
inputProcessor.reset();
|
||||||
|
assertFalse(inputProcessor.isKeyboardLocked());
|
||||||
|
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProtectedFieldInhibition() {
|
||||||
|
// Setup protected field at pos 0, length 10
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PROTECT);
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
// Attempting to type into protected field
|
||||||
|
inputProcessor.typeCharacter('Z');
|
||||||
|
assertTrue(inputProcessor.isKeyboardLocked());
|
||||||
|
assertEquals(ECLConstants.INHIBIT_PROTECTED_FIELD, oia.getInputInhibited());
|
||||||
|
assertEquals("X-PROT", oia.getStatusString());
|
||||||
|
assertEquals(0, screen.getCell(1).ec);
|
||||||
|
|
||||||
|
// Reset unlocks
|
||||||
|
inputProcessor.reset();
|
||||||
|
assertFalse(inputProcessor.isKeyboardLocked());
|
||||||
|
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAutoSkipFieldBoundary() {
|
||||||
|
// Field 1: Unprotected at pos 0 (chars at 1, 2)
|
||||||
|
screen.setFieldAttribute(0, (byte) 0x00);
|
||||||
|
// Field 2: Auto-skip at pos 3 (FA_PROTECT | FA_NUMERIC) (chars at 4, 5)
|
||||||
|
screen.setFieldAttribute(3, (byte) (FA_PROTECT | FA_NUMERIC));
|
||||||
|
// Field 3: Unprotected at pos 6 (chars at 7, 8)
|
||||||
|
screen.setFieldAttribute(6, (byte) 0x00);
|
||||||
|
// Delimiter at pos 9
|
||||||
|
screen.setFieldAttribute(9, (byte) FA_PROTECT);
|
||||||
|
|
||||||
|
// Cursor at pos 1, type 'A' -> moves to pos 2
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
inputProcessor.typeCharacter('A');
|
||||||
|
assertEquals(2, screen.getCursorAddress());
|
||||||
|
|
||||||
|
// Type 'B' at pos 2 (the last char in Field 1) -> auto-skips over Field 2 (pos 3..5) to pos 7!
|
||||||
|
inputProcessor.typeCharacter('B');
|
||||||
|
assertEquals(7, screen.getCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInsertModeFieldOverflowInhibition() {
|
||||||
|
// Unprotected field at pos 0, size 3 (positions 1, 2, 3)
|
||||||
|
screen.setFieldAttribute(0, (byte) 0x00);
|
||||||
|
screen.setFieldAttribute(4, (byte) FA_PROTECT);
|
||||||
|
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
inputProcessor.typeCharacter('A');
|
||||||
|
inputProcessor.typeCharacter('B');
|
||||||
|
inputProcessor.typeCharacter('C');
|
||||||
|
|
||||||
|
// Field is full: pos 1='A', pos 2='B', pos 3='C'
|
||||||
|
assertEquals('A', (char) screen.getCell(1).ucs4);
|
||||||
|
assertEquals('B', (char) screen.getCell(2).ucs4);
|
||||||
|
assertEquals('C', (char) screen.getCell(3).ucs4);
|
||||||
|
|
||||||
|
// Enable insert mode and attempt to type at pos 1
|
||||||
|
inputProcessor.setInsertMode(true);
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
inputProcessor.typeCharacter('X');
|
||||||
|
|
||||||
|
assertTrue(inputProcessor.isKeyboardLocked());
|
||||||
|
assertEquals(ECLConstants.INHIBIT_OVERFLOW, oia.getInputInhibited());
|
||||||
|
assertEquals("X-OVERFLOW", oia.getStatusString());
|
||||||
|
assertEquals('A', (char) screen.getCell(1).ucs4); // unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadModifiedInboundDataFraming() {
|
||||||
|
// Create field at pos 0, type "TEST"
|
||||||
|
screen.setFieldAttribute(0, (byte) 0x00);
|
||||||
|
screen.setFieldAttribute(10, (byte) FA_PROTECT);
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
inputProcessor.typeCharacter('T');
|
||||||
|
inputProcessor.typeCharacter('E');
|
||||||
|
inputProcessor.typeCharacter('S');
|
||||||
|
inputProcessor.typeCharacter('T');
|
||||||
|
|
||||||
|
byte[] inbound = inputProcessor.buildReadModifiedInboundData(AID_ENTER, false);
|
||||||
|
assertNotNull(inbound);
|
||||||
|
assertTrue(inbound.length >= 7);
|
||||||
|
assertEquals((byte) AID_ENTER, inbound[0]); // AID
|
||||||
|
// Next 2 bytes: cursor address
|
||||||
|
// Next byte: SBA (0x11)
|
||||||
|
assertEquals((byte) ORDER_SBA, inbound[3]);
|
||||||
|
// Followed by field address (pos 1), then EBCDIC bytes for "TEST"
|
||||||
|
assertEquals((byte) translator.unicodeToEbcdic('T'), inbound[6]);
|
||||||
|
assertEquals((byte) translator.unicodeToEbcdic('E'), inbound[7]);
|
||||||
|
assertEquals((byte) translator.unicodeToEbcdic('S'), inbound[8]);
|
||||||
|
assertEquals((byte) translator.unicodeToEbcdic('T'), inbound[9]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAidSelectDoesNotTransmitFieldCharacters() {
|
||||||
|
// Create selectable field with designator ' ' at pos 1
|
||||||
|
screen.setFieldAttribute(0, (byte) (FA_INT_HIGH_SEL | FA_NUMERIC)); // selectable
|
||||||
|
screen.getCell(0).fa |= FA_MODIFY;
|
||||||
|
screen.getCell(1).ec = 0x40; // space designator
|
||||||
|
screen.getCell(2).ec = (byte) translator.unicodeToEbcdic('X');
|
||||||
|
screen.setFieldAttribute(10, (byte) FA_PROTECT);
|
||||||
|
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
byte[] inbound = inputProcessor.buildReadModifiedInboundData(AID_SELECT, false);
|
||||||
|
assertNotNull(inbound);
|
||||||
|
assertEquals((byte) AID_SELECT, inbound[0]);
|
||||||
|
// Must contain SBA and designator address, but NO field text bytes
|
||||||
|
assertEquals(6, inbound.length); // AID (1) + Cursor (2) + SBA (1) + Addr (2) = 6 bytes
|
||||||
|
assertEquals((byte) ORDER_SBA, inbound[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPAKeysAndClearInboundData() {
|
||||||
|
screen.setCursorAddress(80);
|
||||||
|
byte[] pa1 = inputProcessor.buildReadModifiedInboundData(AID_PA1, false);
|
||||||
|
assertEquals(3, pa1.length); // AID + 2 cursor bytes
|
||||||
|
assertEquals((byte) AID_PA1, pa1[0]);
|
||||||
|
|
||||||
|
byte[] clear = inputProcessor.buildReadModifiedInboundData(AID_CLEAR, false);
|
||||||
|
assertEquals(3, clear.length);
|
||||||
|
assertEquals((byte) AID_CLEAR, clear[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadBufferInboundDataStandardAndExtended() {
|
||||||
|
screen.setFieldAttribute(0, (byte) 0x00);
|
||||||
|
screen.getCell(1).ec = (byte) translator.unicodeToEbcdic('A');
|
||||||
|
screen.setFieldAttribute(10, (byte) FA_PROTECT);
|
||||||
|
|
||||||
|
// Standard Field Mode
|
||||||
|
screen.setReplyMode((byte) SF_SRM_FIELD);
|
||||||
|
byte[] standardBuf = inputProcessor.buildReadBufferInboundData(AID_ENTER);
|
||||||
|
assertNotNull(standardBuf);
|
||||||
|
assertEquals((byte) AID_ENTER, standardBuf[0]);
|
||||||
|
assertEquals((byte) ORDER_SF, standardBuf[3]);
|
||||||
|
|
||||||
|
// Extended Field Mode
|
||||||
|
screen.setReplyMode((byte) SF_SRM_XFIELD);
|
||||||
|
byte[] extendedBuf = inputProcessor.buildReadBufferInboundData(AID_ENTER);
|
||||||
|
assertNotNull(extendedBuf);
|
||||||
|
assertEquals((byte) AID_ENTER, extendedBuf[0]);
|
||||||
|
assertEquals((byte) ORDER_SFE, extendedBuf[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWordLeftWordRightAndFieldEndNavigation() {
|
||||||
|
// Field: "HELLO WORLD "
|
||||||
|
screen.setFieldAttribute(0, (byte) 0x00);
|
||||||
|
screen.setFieldAttribute(25, (byte) FA_PROTECT);
|
||||||
|
|
||||||
|
String text = "HELLO WORLD ";
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
screen.getCell(1 + i).ec = (byte) translator.unicodeToEbcdic(text.charAt(i));
|
||||||
|
screen.getCell(1 + i).ucs4 = text.charAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Field End
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
inputProcessor.processFieldEnd();
|
||||||
|
// Immediately following 'D' in WORLD (pos 1 + 13 = 14)
|
||||||
|
assertEquals(14, screen.getCursorAddress());
|
||||||
|
|
||||||
|
// Test Word Left from pos 14 -> jumps to start of "WORLD" (pos 9)
|
||||||
|
inputProcessor.processWordLeft();
|
||||||
|
assertEquals(9, screen.getCursorAddress());
|
||||||
|
|
||||||
|
// Test Word Left from pos 9 -> jumps to start of "HELLO" (pos 1)
|
||||||
|
inputProcessor.processWordLeft();
|
||||||
|
assertEquals(1, screen.getCursorAddress());
|
||||||
|
|
||||||
|
// Test Word Right from pos 1 -> jumps to start of "WORLD" (pos 9)
|
||||||
|
inputProcessor.processWordRight();
|
||||||
|
assertEquals(9, screen.getCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAll22CompatibleMethodsAndOverloads() {
|
||||||
|
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_2, false);
|
||||||
|
Telnet3270Client client = new Telnet3270Client(config);
|
||||||
|
|
||||||
|
// 1. processChar
|
||||||
|
client.processChar('A');
|
||||||
|
client.processChar('B', 5, false);
|
||||||
|
|
||||||
|
// 2. processEnter
|
||||||
|
client.processEnter();
|
||||||
|
|
||||||
|
// 3. processPF
|
||||||
|
client.processPF(3);
|
||||||
|
|
||||||
|
// 4. processPA
|
||||||
|
client.processPA(1);
|
||||||
|
|
||||||
|
// 5. processClear
|
||||||
|
client.processClear();
|
||||||
|
|
||||||
|
// 6-9. Cursor navigation
|
||||||
|
client.processCursorUp();
|
||||||
|
client.processCursorDown();
|
||||||
|
client.processCursorLeft();
|
||||||
|
client.processCursorRight();
|
||||||
|
|
||||||
|
// 10-13. Field navigation
|
||||||
|
client.processTab();
|
||||||
|
client.processBackTab();
|
||||||
|
client.processHome();
|
||||||
|
client.processNewline();
|
||||||
|
|
||||||
|
// 14-16. Editing
|
||||||
|
client.processDelete();
|
||||||
|
client.processBackspace();
|
||||||
|
client.processEraseEOF();
|
||||||
|
client.processEraseInput();
|
||||||
|
|
||||||
|
// 17-19. Special orders & toggles
|
||||||
|
client.processDup();
|
||||||
|
client.processFieldMark();
|
||||||
|
client.processToggleInsert();
|
||||||
|
|
||||||
|
// 20-22. Control / Selection
|
||||||
|
client.processReset();
|
||||||
|
client.processWordLeft();
|
||||||
|
client.processWordRight();
|
||||||
|
client.processFieldEnd();
|
||||||
|
client.processAttn();
|
||||||
|
client.processSysReq();
|
||||||
|
client.processCurSel();
|
||||||
|
client.processLightPen();
|
||||||
|
client.processLightPen(10);
|
||||||
|
|
||||||
|
assertNotNull(client.getInputProcessor().buildReadModifiedInboundData());
|
||||||
|
assertNotNull(client.getInputProcessor().buildReadBufferInboundData());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSendKeysMnemonicTokens() {
|
||||||
|
screen.setFieldAttribute(0, (byte) 0x00);
|
||||||
|
screen.setFieldAttribute(15, (byte) 0x00);
|
||||||
|
screen.setFieldAttribute(30, (byte) FA_PROTECT);
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
inputProcessor.sendKeys("123[tab]456[wordleft][fieldend][reset]");
|
||||||
|
assertEquals('1', (char) screen.getCell(1).ucs4);
|
||||||
|
assertEquals('2', (char) screen.getCell(2).ucs4);
|
||||||
|
assertEquals('3', (char) screen.getCell(3).ucs4);
|
||||||
|
assertEquals('4', (char) screen.getCell(16).ucs4);
|
||||||
|
assertEquals('5', (char) screen.getCell(17).ucs4);
|
||||||
|
assertEquals('6', (char) screen.getCell(18).ucs4);
|
||||||
|
assertFalse(inputProcessor.isKeyboardLocked());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package haus.nightmare.lib3270j.screen;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.AbstractDBCSCodePage;
|
||||||
|
import haus.nightmare.lib3270j.charset.CodePageRegistry;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
public class ScreenBufferPhase3Test {
|
||||||
|
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWrappedFieldListFirstFaNotAtZero() {
|
||||||
|
screen.erase(false);
|
||||||
|
// Place FA at pos 100 (unprotected)
|
||||||
|
screen.setFieldAttribute(100, (byte) FA_PRINTABLE);
|
||||||
|
// Place FA at pos 150 (protected) - wraps around 1919 back to 99
|
||||||
|
screen.setFieldAttribute(150, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
|
||||||
|
ECLFieldList list = screen.buildFieldList();
|
||||||
|
assertEquals(2, list.getFieldCount());
|
||||||
|
|
||||||
|
ECLField f1 = list.getFirstField();
|
||||||
|
assertNotNull(f1);
|
||||||
|
assertEquals(100, f1.getStart());
|
||||||
|
assertEquals(101, f1.getDataStart());
|
||||||
|
assertEquals(149, f1.getEnd());
|
||||||
|
assertEquals(49, f1.getLength());
|
||||||
|
assertFalse(f1.isWrapped());
|
||||||
|
assertFalse(f1.isProtected());
|
||||||
|
|
||||||
|
ECLField f2 = list.getNextField(f1);
|
||||||
|
assertNotNull(f2);
|
||||||
|
assertEquals(150, f2.getStart());
|
||||||
|
assertEquals(151, f2.getDataStart());
|
||||||
|
assertEquals(99, f2.getEnd());
|
||||||
|
assertEquals(1920 - 150 - 1 + 100, f2.getLength());
|
||||||
|
assertTrue(f2.isWrapped());
|
||||||
|
assertTrue(f2.isProtected());
|
||||||
|
|
||||||
|
// Test field containment on wrapped field
|
||||||
|
assertTrue(f2.contains(150));
|
||||||
|
assertTrue(f2.contains(151));
|
||||||
|
assertTrue(f2.contains(1919));
|
||||||
|
assertTrue(f2.contains(0));
|
||||||
|
assertTrue(f2.contains(99));
|
||||||
|
assertFalse(f2.contains(100));
|
||||||
|
assertFalse(f2.contains(120));
|
||||||
|
|
||||||
|
// Test field lookup at position
|
||||||
|
assertEquals(150, screen.findFieldAt(50).getStart());
|
||||||
|
assertEquals(100, screen.findFieldAt(120).getStart());
|
||||||
|
|
||||||
|
// Test navigation
|
||||||
|
assertEquals(150, screen.findPrevField(120).getStart());
|
||||||
|
assertEquals(150, screen.findNextField(120).getStart());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSingleFieldScreenWrap() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setFieldAttribute(50, (byte) FA_PRINTABLE);
|
||||||
|
|
||||||
|
ECLFieldList list = screen.buildFieldList();
|
||||||
|
assertEquals(1, list.getFieldCount());
|
||||||
|
|
||||||
|
ECLField f = list.getFirstField();
|
||||||
|
assertNotNull(f);
|
||||||
|
assertEquals(50, f.getStart());
|
||||||
|
assertEquals(51, f.getDataStart());
|
||||||
|
assertEquals(49, f.getEnd());
|
||||||
|
assertEquals(1919, f.getLength());
|
||||||
|
assertTrue(f.isWrapped());
|
||||||
|
assertTrue(f.contains(0));
|
||||||
|
assertTrue(f.contains(50));
|
||||||
|
assertTrue(f.contains(1919));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInsertCharSBCSAndOverflow() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setFieldAttribute(6, (byte) (FA_PRINTABLE | FA_PROTECT)); // field of 5 chars (pos 1..5)
|
||||||
|
|
||||||
|
// Write 'A', 'B', 'C', 'D' at pos 1..4 (pos 5 is null)
|
||||||
|
screen.setChar(0, 1, 'A');
|
||||||
|
screen.setChar(0, 2, 'B');
|
||||||
|
screen.setChar(0, 3, 'C');
|
||||||
|
screen.setChar(0, 4, 'D');
|
||||||
|
|
||||||
|
// Insert 'Z' at pos 1
|
||||||
|
boolean inserted = screen.insertChar(1, 'Z');
|
||||||
|
assertTrue(inserted);
|
||||||
|
assertEquals('Z', screen.getChar(0, 1));
|
||||||
|
assertEquals('A', screen.getChar(0, 2));
|
||||||
|
assertEquals('B', screen.getChar(0, 3));
|
||||||
|
assertEquals('C', screen.getChar(0, 4));
|
||||||
|
assertEquals('D', screen.getChar(0, 5));
|
||||||
|
|
||||||
|
// Field is now full (pos 5 is 'D'). Inserting another char should overflow and return false
|
||||||
|
boolean overflow = screen.insertChar(1, 'X');
|
||||||
|
assertFalse(overflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInsertAndShiftDBCSChar() {
|
||||||
|
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
|
||||||
|
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
|
||||||
|
cp.registerDbcsPair(0x4341, '\u6771'); // '東'
|
||||||
|
|
||||||
|
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
|
||||||
|
dbcsScreen.erase(false);
|
||||||
|
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
dbcsScreen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
|
||||||
|
// Insert DBCS char at pos 1
|
||||||
|
boolean ok = dbcsScreen.insertChar(1, '\u6771');
|
||||||
|
assertTrue(ok);
|
||||||
|
assertEquals(0x43, dbcsScreen.getCell(1).ec & 0xFF);
|
||||||
|
assertEquals(0x41, dbcsScreen.getCell(2).ec & 0xFF);
|
||||||
|
assertEquals(ExtendedAttribute.CS_DBCS, dbcsScreen.getCell(1).cs);
|
||||||
|
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
|
||||||
|
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
|
||||||
|
assertEquals(3, dbcsScreen.getCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeleteCharDBCSAndCleanAdjacentSISO() {
|
||||||
|
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
|
||||||
|
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
|
||||||
|
cp.registerDbcsPair(0x4341, '\u6771');
|
||||||
|
|
||||||
|
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
|
||||||
|
dbcsScreen.erase(false);
|
||||||
|
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
dbcsScreen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
|
||||||
|
// Insert DBCS character at pos 1
|
||||||
|
dbcsScreen.insertChar(1, '\u6771');
|
||||||
|
// Delete DBCS character at pos 1
|
||||||
|
boolean deleted = dbcsScreen.deleteChar(1);
|
||||||
|
assertTrue(deleted);
|
||||||
|
assertEquals(0, dbcsScreen.getCell(1).ec);
|
||||||
|
assertEquals(0, dbcsScreen.getCell(2).ec);
|
||||||
|
|
||||||
|
// Test orphaned SO/SI cleanup on delete
|
||||||
|
dbcsScreen.setCell(3, 0x0E); // SO
|
||||||
|
dbcsScreen.setCell(4, 0x43); // DBCS byte 1
|
||||||
|
dbcsScreen.setCell(5, 0x41); // DBCS byte 2
|
||||||
|
dbcsScreen.setCell(6, 0x0F); // SI
|
||||||
|
dbcsScreen.getCell(4).cs = ExtendedAttribute.CS_DBCS;
|
||||||
|
dbcsScreen.getCell(4).db = ExtendedAttribute.DB_LEFT;
|
||||||
|
dbcsScreen.getCell(5).cs = ExtendedAttribute.CS_DBCS;
|
||||||
|
dbcsScreen.getCell(5).db = ExtendedAttribute.DB_RIGHT;
|
||||||
|
|
||||||
|
// Deleting the DBCS character at pos 4 pulls SI (pos 6) adjacent to SO (pos 3), triggering SISO cleanup
|
||||||
|
dbcsScreen.deleteChar(4);
|
||||||
|
assertEquals(0, dbcsScreen.getCell(3).ec);
|
||||||
|
assertEquals(0, dbcsScreen.getCell(4).ec);
|
||||||
|
assertEquals(0, dbcsScreen.getCell(5).ec);
|
||||||
|
assertEquals(0, dbcsScreen.getCell(6).ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEntryAssistDOCModeAndTabStops() {
|
||||||
|
screen.erase(false);
|
||||||
|
assertFalse(screen.isEntryAssistDOCmode());
|
||||||
|
assertFalse(screen.isEntryAssistWordWrap());
|
||||||
|
|
||||||
|
screen.setEntryAssistDOCmode(true);
|
||||||
|
screen.setEntryAssistWordWrap(true);
|
||||||
|
screen.setEntryAssistStartColumn(5);
|
||||||
|
screen.setEntryAssistEndColumn(75);
|
||||||
|
screen.setEntryAssistTabStops(new int[]{ 10, 20, 30, 40 });
|
||||||
|
|
||||||
|
assertTrue(screen.isEntryAssistDOCmode());
|
||||||
|
assertTrue(screen.isEntryAssistWordWrap());
|
||||||
|
assertEquals(5, screen.getEntryAssistStartColumn());
|
||||||
|
assertEquals(75, screen.getEntryAssistEndColumn());
|
||||||
|
assertArrayEquals(new int[]{ 10, 20, 30, 40 }, screen.getEntryAssistTabStops());
|
||||||
|
|
||||||
|
// Test word tab forward with tab stops
|
||||||
|
screen.setCursorPosition(0, 0);
|
||||||
|
screen.processWordTab(true);
|
||||||
|
assertEquals(10, screen.getCursorCol());
|
||||||
|
|
||||||
|
screen.processWordTab(true);
|
||||||
|
assertEquals(20, screen.getCursorCol());
|
||||||
|
|
||||||
|
// Test word tab backward
|
||||||
|
screen.processWordTab(false);
|
||||||
|
assertEquals(10, screen.getCursorCol());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessDeleteWord() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
|
||||||
|
// Write "HELLO WORLD" starting at pos 1
|
||||||
|
String text = "HELLO WORLD";
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
screen.setChar(0, 1 + i, text.charAt(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
screen.processDeleteWord();
|
||||||
|
|
||||||
|
// "HELLO " is deleted, "WORLD" is shifted left to pos 1
|
||||||
|
assertEquals('W', screen.getChar(0, 1));
|
||||||
|
assertEquals('O', screen.getChar(0, 2));
|
||||||
|
assertEquals('R', screen.getChar(0, 3));
|
||||||
|
assertEquals('L', screen.getChar(0, 4));
|
||||||
|
assertEquals('D', screen.getChar(0, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessSOSIDisplayTransformation() {
|
||||||
|
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
|
||||||
|
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
|
||||||
|
cp.registerDbcsPair(0x4341, '\u6771');
|
||||||
|
|
||||||
|
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
|
||||||
|
dbcsScreen.erase(false);
|
||||||
|
|
||||||
|
// Put SO (0x0E), 0x43, 0x41, SI (0x0F) at pos 0..3
|
||||||
|
dbcsScreen.setCell(0, 0x0E);
|
||||||
|
dbcsScreen.setCell(1, 0x43);
|
||||||
|
dbcsScreen.setCell(2, 0x41);
|
||||||
|
dbcsScreen.setCell(3, 0x0F);
|
||||||
|
|
||||||
|
dbcsScreen.processSOSI();
|
||||||
|
|
||||||
|
assertEquals(ExtendedAttribute.DB_SO, dbcsScreen.getCell(0).db);
|
||||||
|
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
|
||||||
|
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
|
||||||
|
assertEquals(ExtendedAttribute.DB_SI, dbcsScreen.getCell(3).db);
|
||||||
|
assertEquals('\u6771', dbcsScreen.getCell(1).ucs4);
|
||||||
|
assertEquals('\u6771', dbcsScreen.getCell(2).ucs4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAccessorsAndConvenienceMethods() {
|
||||||
|
screen.erase(false);
|
||||||
|
assertEquals(24 * 80, screen.getSize());
|
||||||
|
|
||||||
|
screen.writeChar(10, (byte) 0xC1); // 'A' in CP037
|
||||||
|
assertEquals('A', screen.getChar(0, 10));
|
||||||
|
|
||||||
|
screen.setChar(1, 5, 'Z');
|
||||||
|
assertEquals('Z', screen.getChar(1, 5));
|
||||||
|
|
||||||
|
ExtendedAttribute ea = new ExtendedAttribute();
|
||||||
|
ea.fg = 2; // RED
|
||||||
|
screen.setExtAttr(1, 5, ea);
|
||||||
|
assertEquals(2, screen.getExtAttr(1, 5).fg);
|
||||||
|
|
||||||
|
screen.setText("TESTING 123");
|
||||||
|
assertEquals("TESTING 123", screen.getString(0, 11));
|
||||||
|
assertEquals(0, screen.searchString("TESTING"));
|
||||||
|
assertEquals(-1, screen.searchString("NOTFOUND"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user