Cleanup datastream and Graphics
This commit is contained in:
@@ -363,8 +363,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
boolean isEnterField = (desEbcdic == 0x50 || desAscii == '&' || desAscii == '\uFF06');
|
boolean isEnterField = (desEbcdic == 0x50 || desAscii == '&' || desAscii == '\uFF06');
|
||||||
boolean isProtected = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa);
|
boolean isProtected = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa);
|
||||||
|
|
||||||
// Only auto-select toggle fields (?/>), enter fields (&), or unprotected space fields
|
boolean isDoubleClick = (e.getClickCount() >= 2);
|
||||||
if (lightPenMode || isToggleField || isEnterField || !isProtected) {
|
// For detectable fields (FA bits 4-5 = 01 or 11):
|
||||||
|
// Double-clicking or invoking mnemonic [cursel] toggles the designator character (? -> >).
|
||||||
|
// If field has immediate select attribute, immediately transmit Cursor Select AID (0x7E).
|
||||||
|
if (lightPenMode || isDoubleClick || isToggleField || isEnterField || !isProtected) {
|
||||||
if (client.lightPenSelect(clickAddr)) {
|
if (client.lightPenSelect(clickAddr)) {
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
return;
|
return;
|
||||||
@@ -1343,7 +1346,41 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
blinkTimer = new Timer(500, e -> {
|
blinkTimer = new Timer(500, e -> {
|
||||||
cursorVisible = !cursorVisible;
|
cursorVisible = !cursorVisible;
|
||||||
textBlinkVisible = !textBlinkVisible;
|
textBlinkVisible = !textBlinkVisible;
|
||||||
|
|
||||||
|
// Check if there are any blinking cells or fields on the screen
|
||||||
|
boolean hasBlinkingCells = false;
|
||||||
|
ScreenBuffer sb = (client != null) ? client.getScreenBuffer() : null;
|
||||||
|
if (sb != null) {
|
||||||
|
int total = sb.getDisplayRows() * sb.getDisplayCols();
|
||||||
|
byte curFaGr = 0;
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
|
ExtendedAttribute c = sb.getDisplayCell(i);
|
||||||
|
if (c.isFieldAttribute()) {
|
||||||
|
curFaGr = c.gr;
|
||||||
|
}
|
||||||
|
if ((c.gr & ExtendedAttribute.GR_BLINK) != 0 || (curFaGr & ExtendedAttribute.GR_BLINK) != 0) {
|
||||||
|
hasBlinkingCells = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBlinkingCells) {
|
||||||
repaint();
|
repaint();
|
||||||
|
} else if (sb != null && client != null && client.getConnectionState() != null && client.getConnectionState().isFullSession()) {
|
||||||
|
// Only repaint the cursor cell bounding rect
|
||||||
|
int cols = sb.getDisplayCols();
|
||||||
|
int ox = getRenderOffsetX();
|
||||||
|
int oy = getRenderOffsetY();
|
||||||
|
int curAddr = sb.getDisplayCursorAddress();
|
||||||
|
int curRow = curAddr / cols;
|
||||||
|
int curCol = curAddr % cols;
|
||||||
|
int cx = ox + curCol * cellWidth;
|
||||||
|
int cy = oy + curRow * cellHeight;
|
||||||
|
repaint(cx, cy, cellWidth, cellHeight);
|
||||||
|
} else {
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
blinkTimer.start();
|
blinkTimer.start();
|
||||||
}
|
}
|
||||||
@@ -1609,6 +1646,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field Outlining (0xC2 / TY_FOUTLINE)
|
||||||
|
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
|
||||||
|
|
||||||
if (isCellSelected(row, col)) {
|
if (isCellSelected(row, col)) {
|
||||||
g2.setColor(SELECTION_COLOR);
|
g2.setColor(SELECTION_COLOR);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
@@ -1628,7 +1668,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check text blink (500ms cycle)
|
// Check text blink (500ms cycle)
|
||||||
boolean blink = (gr & GR_BLINK) != 0;
|
boolean blink = (gr & GR_BLINK) != 0 || (currentFieldEa != null && (currentFieldEa.gr & GR_BLINK) != 0);
|
||||||
boolean suppressGlyph = blink && !textBlinkVisible;
|
boolean suppressGlyph = blink && !textBlinkVisible;
|
||||||
|
|
||||||
// Draw character or Programmed Symbol
|
// Draw character or Programmed Symbol
|
||||||
@@ -1662,12 +1702,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (underline) {
|
if (underline && !suppressGlyph) {
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
|
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
|
||||||
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
|
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field Outlining (0xC2 / TY_FOUTLINE)
|
||||||
|
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
|
||||||
|
|
||||||
// Draw search highlight
|
// Draw search highlight
|
||||||
if (isCellSearchHighlighted(row, col)) {
|
if (isCellSearchHighlighted(row, col)) {
|
||||||
g2.setColor(SEARCH_HIGHLIGHT_COLOR);
|
g2.setColor(SEARCH_HIGHLIGHT_COLOR);
|
||||||
@@ -1835,6 +1878,57 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
|
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render field outline lines around cells based on ea.ol or field attribute outline.
|
||||||
|
* Conforms to IBM 3270 Extended Data Stream / Host On-Demand DS3270 TY_FOUTLINE (0xC2).
|
||||||
|
*
|
||||||
|
* Bit 0 (0x01): Underline -> draw line at y + cellHeight - 1
|
||||||
|
* Bit 1 (0x02): Right vertical line -> draw line at x + cellWidth - 1
|
||||||
|
* Bit 2 (0x04): Overline -> draw line at y
|
||||||
|
* Bit 3 (0x08): Left vertical line -> draw line at x
|
||||||
|
*/
|
||||||
|
private void drawOutlining(Graphics2D g2, ExtendedAttribute ea, ExtendedAttribute currentFieldEa,
|
||||||
|
int baddr, int row, int col, int x, int y,
|
||||||
|
int cellWidth, int cellHeight, int rows, int cols,
|
||||||
|
ScreenBuffer sb, Color fgColor) {
|
||||||
|
int outline = ea.ol & 0xFF;
|
||||||
|
if (outline == 0 && currentFieldEa != null && currentFieldEa.ol != 0) {
|
||||||
|
int fol = currentFieldEa.ol & 0xFF;
|
||||||
|
if ((fol & ExtendedAttribute.OL_UNDERLINE) != 0) outline |= ExtendedAttribute.OL_UNDERLINE;
|
||||||
|
if ((fol & ExtendedAttribute.OL_OVERLINE) != 0) outline |= ExtendedAttribute.OL_OVERLINE;
|
||||||
|
|
||||||
|
int size = rows * cols;
|
||||||
|
int prevAddr = (baddr - 1 + size) % size;
|
||||||
|
if (sb.getDisplayCell(prevAddr).isFieldAttribute() || col == 0) {
|
||||||
|
if ((fol & ExtendedAttribute.OL_LEFT) != 0) outline |= ExtendedAttribute.OL_LEFT;
|
||||||
|
}
|
||||||
|
int nextAddr = (baddr + 1) % size;
|
||||||
|
if (sb.getDisplayCell(nextAddr).isFieldAttribute() || col == cols - 1) {
|
||||||
|
if ((fol & ExtendedAttribute.OL_RIGHT) != 0) outline |= ExtendedAttribute.OL_RIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outline != 0) {
|
||||||
|
g2.setColor(fgColor);
|
||||||
|
// Bit 0 (0x01): Underline -> draw line at y + cellHeight - 1
|
||||||
|
if ((outline & ExtendedAttribute.OL_UNDERLINE) != 0) {
|
||||||
|
g2.drawLine(x, y + cellHeight - 1, x + cellWidth - 1, y + cellHeight - 1);
|
||||||
|
}
|
||||||
|
// Bit 1 (0x02): Right vertical line -> draw line at x + cellWidth - 1
|
||||||
|
if ((outline & ExtendedAttribute.OL_RIGHT) != 0) {
|
||||||
|
g2.drawLine(x + cellWidth - 1, y, x + cellWidth - 1, y + cellHeight - 1);
|
||||||
|
}
|
||||||
|
// Bit 2 (0x04): Overline -> draw line at y
|
||||||
|
if ((outline & ExtendedAttribute.OL_OVERLINE) != 0) {
|
||||||
|
g2.drawLine(x, y, x + cellWidth - 1, y);
|
||||||
|
}
|
||||||
|
// Bit 3 (0x08): Left vertical line -> draw line at x
|
||||||
|
if ((outline & ExtendedAttribute.OL_LEFT) != 0) {
|
||||||
|
g2.drawLine(x, y, x, y + cellHeight - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (blinkTimer != null)
|
if (blinkTimer != null)
|
||||||
blinkTimer.stop();
|
blinkTimer.stop();
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ public class DataStreamProcessor {
|
|||||||
private byte currentBg = 0;
|
private byte currentBg = 0;
|
||||||
private byte currentGr = 0;
|
private byte currentGr = 0;
|
||||||
private byte currentCs = 0;
|
private byte currentCs = 0;
|
||||||
|
private byte currentOl = 0;
|
||||||
|
|
||||||
/** Functional interface for sending output back through the telnet stack. */
|
/** Functional interface for sending output back through the telnet stack. */
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
@@ -250,6 +251,7 @@ public class DataStreamProcessor {
|
|||||||
currentBg = 0;
|
currentBg = 0;
|
||||||
currentGr = 0;
|
currentGr = 0;
|
||||||
currentCs = 0;
|
currentCs = 0;
|
||||||
|
currentOl = 0;
|
||||||
break;
|
break;
|
||||||
case CMD_WSF:
|
case CMD_WSF:
|
||||||
case SNA_CMD_WSF:
|
case SNA_CMD_WSF:
|
||||||
@@ -392,6 +394,7 @@ public class DataStreamProcessor {
|
|||||||
currentBg = 0;
|
currentBg = 0;
|
||||||
currentGr = 0;
|
currentGr = 0;
|
||||||
currentCs = 0;
|
currentCs = 0;
|
||||||
|
currentOl = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eraseFirst) {
|
if (eraseFirst) {
|
||||||
@@ -401,6 +404,7 @@ public class DataStreamProcessor {
|
|||||||
currentBg = 0;
|
currentBg = 0;
|
||||||
currentGr = 0;
|
currentGr = 0;
|
||||||
currentCs = 0;
|
currentCs = 0;
|
||||||
|
currentOl = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process orders and data starting at byte 2
|
// Process orders and data starting at byte 2
|
||||||
@@ -497,6 +501,7 @@ public class DataStreamProcessor {
|
|||||||
currentBg = 0;
|
currentBg = 0;
|
||||||
currentGr = 0;
|
currentGr = 0;
|
||||||
currentCs = 0;
|
currentCs = 0;
|
||||||
|
currentOl = 0;
|
||||||
break;
|
break;
|
||||||
case XA_FOREGROUND:
|
case XA_FOREGROUND:
|
||||||
currentFg = (byte) attrValue;
|
currentFg = (byte) attrValue;
|
||||||
@@ -521,6 +526,10 @@ public class DataStreamProcessor {
|
|||||||
case XA_CHARSET:
|
case XA_CHARSET:
|
||||||
currentCs = (byte) attrValue;
|
currentCs = (byte) attrValue;
|
||||||
break;
|
break;
|
||||||
|
case XA_OUTLINING:
|
||||||
|
case 0x84:
|
||||||
|
currentOl = (byte) attrValue;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
pos += 3;
|
pos += 3;
|
||||||
lastWasOrder = true;
|
lastWasOrder = true;
|
||||||
@@ -605,6 +614,7 @@ public class DataStreamProcessor {
|
|||||||
ea.bg = currentBg;
|
ea.bg = currentBg;
|
||||||
ea.gr = (byte) currentGr;
|
ea.gr = (byte) currentGr;
|
||||||
ea.cs = fillCs;
|
ea.cs = fillCs;
|
||||||
|
ea.ol = currentOl;
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
} while (baddr != toAddr);
|
} while (baddr != toAddr);
|
||||||
screen.setBufferAddress(baddr);
|
screen.setBufferAddress(baddr);
|
||||||
@@ -633,6 +643,7 @@ public class DataStreamProcessor {
|
|||||||
ea.bg = 0;
|
ea.bg = 0;
|
||||||
ea.gr = 0;
|
ea.gr = 0;
|
||||||
ea.cs = 0;
|
ea.cs = 0;
|
||||||
|
ea.ol = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
@@ -657,6 +668,7 @@ public class DataStreamProcessor {
|
|||||||
ea.fg = currentFg;
|
ea.fg = currentFg;
|
||||||
ea.bg = currentBg;
|
ea.bg = currentBg;
|
||||||
ea.gr = (byte) currentGr;
|
ea.gr = (byte) currentGr;
|
||||||
|
ea.ol = currentOl;
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
screen.setBufferAddress(baddr);
|
screen.setBufferAddress(baddr);
|
||||||
pos += 2;
|
pos += 2;
|
||||||
@@ -683,6 +695,7 @@ public class DataStreamProcessor {
|
|||||||
ea.bg = currentBg;
|
ea.bg = currentBg;
|
||||||
ea.gr = (byte) currentGr;
|
ea.gr = (byte) currentGr;
|
||||||
ea.cs = currentCs;
|
ea.cs = currentCs;
|
||||||
|
ea.ol = currentOl;
|
||||||
|
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
screen.setBufferAddress(baddr);
|
screen.setBufferAddress(baddr);
|
||||||
@@ -995,6 +1008,7 @@ public class DataStreamProcessor {
|
|||||||
byte[] psData = new byte[fieldLen - 3];
|
byte[] psData = new byte[fieldLen - 3];
|
||||||
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
||||||
programSymbolManager.loadps(psData);
|
programSymbolManager.loadps(psData);
|
||||||
|
notifyScreenUpdated();
|
||||||
}
|
}
|
||||||
break;
|
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
|
||||||
@@ -1305,6 +1319,7 @@ public class DataStreamProcessor {
|
|||||||
byte[] psData = new byte[fieldLen - 4];
|
byte[] psData = new byte[fieldLen - 4];
|
||||||
System.arraycopy(data, offset + 4, psData, 0, psData.length);
|
System.arraycopy(data, offset + 4, psData, 0, psData.length);
|
||||||
programSymbolManager.loadps(psData);
|
programSymbolManager.loadps(psData);
|
||||||
|
notifyScreenUpdated();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
||||||
|
|||||||
@@ -482,8 +482,8 @@ public class QueryReplyBuilder {
|
|||||||
cpgid = screen.getTranslator().getCpgid();
|
cpgid = screen.getTranslator().getCpgid();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (graphicsMode == GraphicsMode.PROGRAMMED_SYMBOLS) {
|
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||||
// Programmed Symbols only mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
// Programmed Symbols mode (3279 PS / 3179G with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||||
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||||
out.write(0x00); // more flags
|
out.write(0x00); // more flags
|
||||||
@@ -681,16 +681,18 @@ public class QueryReplyBuilder {
|
|||||||
public byte[] buildGrSymbolSet() {
|
public byte[] buildGrSymbolSet() {
|
||||||
int charW = getCharWidth();
|
int charW = getCharWidth();
|
||||||
int charH = getCharHeight();
|
int charH = getCharHeight();
|
||||||
int cgcsgid = 0x02B9;
|
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||||
int cpgid = 0x0025;
|
return new byte[]{
|
||||||
if (screen != null && screen.getTranslator() != null) {
|
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||||
cgcsgid = screen.getTranslator().getCgcsgid();
|
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00, (byte) charW, (byte) charH,
|
||||||
cpgid = screen.getTranslator().getCpgid();
|
0x00, 0x01, 0x4C, 0x01, 0x22, 0x00, 0x00, 0x12, 0x01, 0x00, 0x20, (byte) 0xF8, (byte) 0xC1, (byte) 0xC1,
|
||||||
|
0x00, 0x00, 0x18, 0x18, 0x00, 0x01, 0x72, 0x01, 0x2C, 0x00, (byte) 0xFF
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||||
0x01, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00,
|
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
|
||||||
(byte) charW, (byte) charH, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00
|
(byte) charW, (byte) charH, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1159,7 +1159,9 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
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) {
|
||||||
setPixelInFill(x, y, fill, isMixOr);
|
int pixColor = (psSlot.isTriplePlane() && psSlot.getRgbPixels(fill, bg) != null)
|
||||||
|
? psSlot.getRgbPixels(fill, bg)[pIdx] : fill;
|
||||||
|
setPixelInFill(x, y, pixColor, isMixOr);
|
||||||
} else if (isOpaqueBg) {
|
} else if (isOpaqueBg) {
|
||||||
setPixelInFill(x, y, bg, isMixOr);
|
setPixelInFill(x, y, bg, isMixOr);
|
||||||
}
|
}
|
||||||
@@ -1192,7 +1194,9 @@ public class GraphicsPlane implements PixelBuffer {
|
|||||||
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) {
|
||||||
setPixelInFill(x, y, fill, isMixOr);
|
int pixColor = (psSlot.isTriplePlane() && psSlot.getRgbPixels(fill, bg) != null)
|
||||||
|
? psSlot.getRgbPixels(fill, bg)[pIdx] : fill;
|
||||||
|
setPixelInFill(x, y, pixColor, isMixOr);
|
||||||
} else if (isOpaqueBg) {
|
} else if (isOpaqueBg) {
|
||||||
setPixelInFill(x, y, bg, isMixOr);
|
setPixelInFill(x, y, bg, isMixOr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,34 +175,46 @@ public class ProgramSymbolManager {
|
|||||||
int startCodePoint = data[2] & 0xFF;
|
int startCodePoint = data[2] & 0xFF;
|
||||||
int rws = data[3] & 0xFF;
|
int rws = data[3] & 0xFF;
|
||||||
|
|
||||||
|
int normRws = rws;
|
||||||
|
if (normRws >= 0x40 && normRws <= 0x49) {
|
||||||
|
normRws = normRws - 0x40 + 2; // 0x40 -> 2 (PSA), 0x41 -> 3 (PSB), ..., 0x45 -> 7 (PSF)
|
||||||
|
} else if ((normRws < 2 || normRws > 11) && (lcid >= 0x40 && lcid <= 0x49)) {
|
||||||
|
normRws = lcid - 0x40 + 2;
|
||||||
|
}
|
||||||
|
|
||||||
int setIndex;
|
int setIndex;
|
||||||
switch (rws) {
|
switch (normRws) {
|
||||||
case 2: setIndex = 0; break;
|
case 2: setIndex = 0; break; // PSA (0x40)
|
||||||
case 3: setIndex = 1; break;
|
case 3: setIndex = 1; break; // PSB (0x41)
|
||||||
case 4: setIndex = 2; break;
|
case 4: setIndex = 2; break; // PSC (0x42)
|
||||||
case 5: setIndex = 3; break;
|
case 5: setIndex = 3; break; // PSD (0x43)
|
||||||
case 6: setIndex = 4; break;
|
case 6: setIndex = 4; break; // PSE (0x44)
|
||||||
case 7: setIndex = 5; break;
|
case 7: setIndex = 5; break; // PSF (0x45)
|
||||||
case 8: setIndex = 6; break;
|
case 8: setIndex = 6; break;
|
||||||
case 9: setIndex = 7; break;
|
case 9: setIndex = 7; break;
|
||||||
case 10: setIndex = 8; break;
|
case 10: setIndex = 8; break;
|
||||||
case 11: setIndex = 9; break;
|
case 11: setIndex = 9; break;
|
||||||
default:
|
default:
|
||||||
|
if (lcid >= 0x40 && lcid < 256) {
|
||||||
|
setIndex = (lcid - 0x40) % NUMBER_SYMBOL_SETS;
|
||||||
|
} else {
|
||||||
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
|
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
|
||||||
ProgramSymbolSet set = sets[setIndex];
|
ProgramSymbolSet set = sets[setIndex];
|
||||||
|
boolean isTriplePlane = set.isTriplePlane() || (normRws >= 4 && normRws <= 7);
|
||||||
|
|
||||||
if (clearAll) {
|
if (clearAll) {
|
||||||
set.clear();
|
set.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
int extHeaderLen = 0;
|
int extHeaderLen = 0;
|
||||||
int cellWidth = (loadFormat == 1 || rws < 8) ? 9 : defaultCellWidth;
|
int cellWidth = (loadFormat == 1) ? 9 : (defaultCellWidth > 0 ? defaultCellWidth : 9);
|
||||||
int cellHeight = (loadFormat == 1 || rws < 8) ? 16 : defaultCellHeight;
|
int cellHeight = (loadFormat == 1) ? 16 : (defaultCellHeight > 0 ? defaultCellHeight : 16);
|
||||||
int colorPlane = 0; // 0 = all planes, 1 = Blue, 2 = Red, 4 = Green
|
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
||||||
|
|
||||||
if (hasExtHeader && data.length > 4) {
|
if (hasExtHeader && data.length > 4) {
|
||||||
extHeaderLen = data[4] & 0xFF;
|
extHeaderLen = data[4] & 0xFF;
|
||||||
|
|||||||
@@ -334,11 +334,17 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set MDT on field attribute
|
// Set MDT on field attribute
|
||||||
|
boolean isLastFieldChar = false;
|
||||||
|
ExtendedAttribute currentFaEa = null;
|
||||||
if (screen.isFormatted()) {
|
if (screen.isFormatted()) {
|
||||||
int faAddr = screen.findFieldAttribute(baddr);
|
int faAddr = screen.findFieldAttribute(baddr);
|
||||||
if (faAddr >= 0) {
|
if (faAddr >= 0) {
|
||||||
ExtendedAttribute faEa = screen.getCell(faAddr);
|
currentFaEa = screen.getCell(faAddr);
|
||||||
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
|
currentFaEa.fa = (byte) (currentFaEa.fa | FA_MODIFY);
|
||||||
|
}
|
||||||
|
int nextAddr = screen.incrementAddress(baddr);
|
||||||
|
if (screen.getCell(nextAddr).isFieldAttribute()) {
|
||||||
|
isLastFieldChar = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +397,11 @@ public class InputProcessor {
|
|||||||
|
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
screen.updateDisplaySnapshot();
|
screen.updateDisplaySnapshot();
|
||||||
|
|
||||||
|
// Field Validation: Trigger field (0x04) — automatically send AID upon filling the last character position
|
||||||
|
if (isLastFieldChar && currentFaEa != null && (currentFaEa.vl & ExtendedAttribute.VL_TRIGGER) != 0) {
|
||||||
|
sendAid(AID_ENTER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -858,6 +869,13 @@ public class InputProcessor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (aidCode != AID_CLEAR && aidCode != AID_PA1 && aidCode != AID_PA2 && aidCode != AID_PA3 && aidCode != AID_SYSREQ && aidCode != AID_SELECT) {
|
||||||
|
if (!validateInput()) {
|
||||||
|
log.fine("Input validation failed for AID 0x" + Integer.toHexString(aidCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (insertOffOnAid && insertMode) {
|
if (insertOffOnAid && insertMode) {
|
||||||
setInsertMode(false);
|
setInsertMode(false);
|
||||||
}
|
}
|
||||||
@@ -1017,6 +1035,141 @@ public class InputProcessor {
|
|||||||
sendAidResponse(out.toByteArray());
|
sendAidResponse(out.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 3270 EDS Field Validation (0xC1 / TY_FVALIDATE) ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforce 3270 Extended Data Stream field validation attributes (0xC1 / TY_FVALIDATE)
|
||||||
|
* prior to transmitting AID keys (Mandatory Fill: 0x01, Mandatory Entry: 0x02).
|
||||||
|
*
|
||||||
|
* @return true if all fields pass validation, false if validation failed and input inhibited
|
||||||
|
*/
|
||||||
|
public boolean validateInput() {
|
||||||
|
if (screen == null || !screen.isFormatted()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return true;
|
||||||
|
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ExtendedAttribute faCell = screen.getCell(i);
|
||||||
|
if (!faCell.isFieldAttribute()) continue;
|
||||||
|
|
||||||
|
int vl = faCell.vl & 0xFF;
|
||||||
|
if (vl == 0) continue;
|
||||||
|
|
||||||
|
// Only validate unprotected input fields
|
||||||
|
if (faIsProtected(faCell.fa & 0xFF)) continue;
|
||||||
|
|
||||||
|
int fieldStart = (i + 1) % size;
|
||||||
|
int totalChars = 0;
|
||||||
|
int filledChars = 0;
|
||||||
|
int firstEmptyPos = -1;
|
||||||
|
|
||||||
|
int p = fieldStart;
|
||||||
|
while (p != i && !screen.getCell(p).isFieldAttribute()) {
|
||||||
|
ExtendedAttribute cell = screen.getCell(p);
|
||||||
|
totalChars++;
|
||||||
|
int ec = cell.ec & 0xFF;
|
||||||
|
char ucs = cell.ucs4;
|
||||||
|
boolean isFilled = (ec != 0x00 && ec != 0x40 && ucs != 0 && ucs != ' ' && ucs != '\u3000');
|
||||||
|
if (isFilled) {
|
||||||
|
filledChars++;
|
||||||
|
} else if (firstEmptyPos < 0) {
|
||||||
|
firstEmptyPos = p;
|
||||||
|
}
|
||||||
|
p = (p + 1) % size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalChars == 0) continue;
|
||||||
|
|
||||||
|
boolean isModified = faIsModified(faCell.fa & 0xFF);
|
||||||
|
boolean mandatoryFill = (vl & ExtendedAttribute.VL_MANDATORY_FILL) != 0;
|
||||||
|
boolean mandatoryEntry = (vl & ExtendedAttribute.VL_MANDATORY_ENTRY) != 0;
|
||||||
|
|
||||||
|
// Mandatory Entry (0x02): Field must contain at least one user-entered character
|
||||||
|
if (mandatoryEntry && filledChars == 0) {
|
||||||
|
failValidation(fieldStart);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mandatory Fill (0x01): Field must be completely filled with characters (no nulls/blanks).
|
||||||
|
// If the field is modified, has characters entered, or has Mandatory Entry also set:
|
||||||
|
if (mandatoryFill) {
|
||||||
|
if (mandatoryEntry || isModified || filledChars > 0) {
|
||||||
|
if (filledChars < totalChars) {
|
||||||
|
failValidation(firstEmptyPos >= 0 ? firstEmptyPos : fieldStart);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the field at the current cursor position when the user attempts to tab away.
|
||||||
|
* If the field has Mandatory Fill (0x01) and is partially filled, inhibits keyboard with input error.
|
||||||
|
*/
|
||||||
|
public boolean validateCurrentFieldOnExit() {
|
||||||
|
if (screen == null || !screen.isFormatted()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int curAddr = screen.getCursorAddress();
|
||||||
|
int faPos = screen.findFieldAttribute(curAddr);
|
||||||
|
if (faPos < 0) return true;
|
||||||
|
|
||||||
|
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||||
|
int vl = faCell.vl & 0xFF;
|
||||||
|
if ((vl & ExtendedAttribute.VL_MANDATORY_FILL) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (faIsProtected(faCell.fa & 0xFF)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
int fieldStart = (faPos + 1) % size;
|
||||||
|
int totalChars = 0;
|
||||||
|
int filledChars = 0;
|
||||||
|
int firstEmptyPos = -1;
|
||||||
|
|
||||||
|
int p = fieldStart;
|
||||||
|
while (p != faPos && !screen.getCell(p).isFieldAttribute()) {
|
||||||
|
ExtendedAttribute cell = screen.getCell(p);
|
||||||
|
totalChars++;
|
||||||
|
int ec = cell.ec & 0xFF;
|
||||||
|
char ucs = cell.ucs4;
|
||||||
|
boolean isFilled = (ec != 0x00 && ec != 0x40 && ucs != 0 && ucs != ' ' && ucs != '\u3000');
|
||||||
|
if (isFilled) {
|
||||||
|
filledChars++;
|
||||||
|
} else if (firstEmptyPos < 0) {
|
||||||
|
firstEmptyPos = p;
|
||||||
|
}
|
||||||
|
p = (p + 1) % size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If partially filled (incomplete):
|
||||||
|
if (filledChars > 0 && filledChars < totalChars) {
|
||||||
|
failValidation(firstEmptyPos >= 0 ? firstEmptyPos : curAddr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void failValidation(int errorCursorPos) {
|
||||||
|
if (errorCursorPos >= 0 && screen != null) {
|
||||||
|
screen.setCursorAddress(errorCursorPos);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
if (oia != null) {
|
||||||
|
oia.setInputInhibited(ECLConstants.INHIBIT_OPERATOR_DUE);
|
||||||
|
}
|
||||||
|
setKeyboardLocked(true);
|
||||||
|
if (bellListener != null) {
|
||||||
|
bellListener.onBell();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Cursor movement ==========
|
// ========== Cursor movement ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1144,6 +1297,9 @@ public class InputProcessor {
|
|||||||
screen.processWordTab(true);
|
screen.processWordTab(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!validateCurrentFieldOnExit()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
screen.updateDisplaySnapshot();
|
screen.updateDisplaySnapshot();
|
||||||
@@ -1163,6 +1319,9 @@ public class InputProcessor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!screen.isFormatted()) return;
|
if (!screen.isFormatted()) return;
|
||||||
|
if (!validateCurrentFieldOnExit()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
int size = screen.getRows() * screen.getCols();
|
int size = screen.getRows() * screen.getCols();
|
||||||
if (size <= 0) return;
|
if (size <= 0) return;
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
|
|||||||
@@ -62,6 +62,20 @@ public class ExtendedAttribute {
|
|||||||
public static final byte GR_UNDERLINE = 0x04;
|
public static final byte GR_UNDERLINE = 0x04;
|
||||||
public static final byte GR_INTENSIFY = 0x08;
|
public static final byte GR_INTENSIFY = 0x08;
|
||||||
|
|
||||||
|
// Outlining attribute bits (0xC2 / TY_FOUTLINE)
|
||||||
|
public static final byte OL_NONE = 0x00;
|
||||||
|
public static final byte OL_UNDERLINE = 0x01; // Bit 0: Underline (bottom border)
|
||||||
|
public static final byte OL_RIGHT = 0x02; // Bit 1: Right vertical border
|
||||||
|
public static final byte OL_OVERLINE = 0x04; // Bit 2: Overline (top border)
|
||||||
|
public static final byte OL_LEFT = 0x08; // Bit 3: Left vertical border
|
||||||
|
public static final byte OL_BOX = 0x0F; // All four borders
|
||||||
|
|
||||||
|
// Field Validation attribute bits (0xC1 / TY_FVALIDATE)
|
||||||
|
public static final byte VL_NONE = 0x00;
|
||||||
|
public static final byte VL_MANDATORY_FILL = 0x01; // Bit 0: Mandatory fill
|
||||||
|
public static final byte VL_MANDATORY_ENTRY = 0x02; // Bit 1: Mandatory entry
|
||||||
|
public static final byte VL_TRIGGER = 0x04; // Bit 2: Trigger field
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
|||||||
+258
@@ -0,0 +1,258 @@
|
|||||||
|
package haus.nightmare.lib3270j.datastream;
|
||||||
|
|
||||||
|
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.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 19 verification test suite:
|
||||||
|
* - Field Outlining Extended Attribute (0xC2 / TY_FOUTLINE)
|
||||||
|
* - Field Validation Extended Attribute (0xC1 / TY_FVALIDATE)
|
||||||
|
* - Blinking attributes and suppression
|
||||||
|
* - Light Pen & Cursor Select Field Interaction
|
||||||
|
*/
|
||||||
|
public class Phase19ExtendedAttributesTest {
|
||||||
|
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private DataStreamProcessor processor;
|
||||||
|
private InputProcessor inputProcessor;
|
||||||
|
private ECLOIA oia;
|
||||||
|
private ByteArrayOutputStream output;
|
||||||
|
private AtomicInteger sentAid;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
EbcdicTranslator translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator);
|
||||||
|
processor = new DataStreamProcessor(screen, translator);
|
||||||
|
output = new ByteArrayOutputStream();
|
||||||
|
processor.setOutputSender(bytes -> output.write(bytes, 0, bytes.length));
|
||||||
|
|
||||||
|
inputProcessor = new InputProcessor(screen, translator, null);
|
||||||
|
oia = new ECLOIA(screen, inputProcessor, null);
|
||||||
|
inputProcessor.setOIA(oia);
|
||||||
|
|
||||||
|
sentAid = new AtomicInteger(-1);
|
||||||
|
inputProcessor.setOutputSender(bytes -> {
|
||||||
|
if (bytes.length > 0) {
|
||||||
|
sentAid.set(bytes[0] & 0xFF);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOutliningConstantsAndParsing() {
|
||||||
|
// Verify bitmask constants in ExtendedAttribute
|
||||||
|
assertEquals((byte) 0x01, ExtendedAttribute.OL_UNDERLINE);
|
||||||
|
assertEquals((byte) 0x02, ExtendedAttribute.OL_RIGHT);
|
||||||
|
assertEquals((byte) 0x04, ExtendedAttribute.OL_OVERLINE);
|
||||||
|
assertEquals((byte) 0x08, ExtendedAttribute.OL_LEFT);
|
||||||
|
assertEquals((byte) 0x0F, ExtendedAttribute.OL_BOX);
|
||||||
|
|
||||||
|
// Parse SFE record with Outlining (0xC2) = 0x0F (Full box)
|
||||||
|
byte[] sfeRecord = new byte[] {
|
||||||
|
(byte) CMD_EW, 0x00,
|
||||||
|
(byte) ORDER_SFE, 0x02,
|
||||||
|
(byte) XA_3270, (byte) FA_PRINTABLE,
|
||||||
|
(byte) XA_OUTLINING, (byte) ExtendedAttribute.OL_BOX
|
||||||
|
};
|
||||||
|
processor.processRecord(sfeRecord, 0, sfeRecord.length, true);
|
||||||
|
|
||||||
|
ExtendedAttribute cell = screen.getCell(0);
|
||||||
|
assertTrue(cell.isFieldAttribute());
|
||||||
|
assertEquals(ExtendedAttribute.OL_BOX, cell.ol);
|
||||||
|
|
||||||
|
// Parse SA order with Outlining (0xC2) = 0x05 (Underline + Overline)
|
||||||
|
byte[] saRecord = new byte[] {
|
||||||
|
(byte) CMD_W, 0x00,
|
||||||
|
(byte) ORDER_SBA, 0x00, 0x01,
|
||||||
|
(byte) ORDER_SA, (byte) XA_OUTLINING, (byte) (ExtendedAttribute.OL_UNDERLINE | ExtendedAttribute.OL_OVERLINE),
|
||||||
|
(byte) 0xC1 // 'A'
|
||||||
|
};
|
||||||
|
processor.processRecord(saRecord, 0, saRecord.length, true);
|
||||||
|
|
||||||
|
ExtendedAttribute charCell = screen.getCell(1);
|
||||||
|
assertFalse(charCell.isFieldAttribute());
|
||||||
|
assertEquals((byte) (ExtendedAttribute.OL_UNDERLINE | ExtendedAttribute.OL_OVERLINE), charCell.ol);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMandatoryEntryValidation() {
|
||||||
|
// Set up field 1 at pos 0 (unprotected, length 5, Mandatory Entry: 0x02)
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.getCell(0).vl = ExtendedAttribute.VL_MANDATORY_ENTRY;
|
||||||
|
|
||||||
|
// Set up field 2 at pos 6 (terminator / protected)
|
||||||
|
screen.setFieldAttribute(6, (byte) (FA_PROTECT | FA_NUMERIC));
|
||||||
|
|
||||||
|
// Screen cursor at start of field 1
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
// Attempt to send AID_ENTER while field is empty -> Must fail validation
|
||||||
|
inputProcessor.sendAid(AID_ENTER);
|
||||||
|
|
||||||
|
assertEquals(-1, sentAid.get(), "AID_ENTER must NOT be transmitted when Mandatory Entry is violated");
|
||||||
|
assertTrue(inputProcessor.isKeyboardLocked(), "Keyboard must be locked on validation error");
|
||||||
|
assertEquals(ECLConstants.INHIBIT_OPERATOR_DUE, oia.getInputInhibited(), "OIA must indicate Operator Due inhibit");
|
||||||
|
|
||||||
|
// Unlock / reset
|
||||||
|
inputProcessor.reset();
|
||||||
|
assertEquals(ECLConstants.INHIBIT_NOT_INHIBITED, oia.getInputInhibited());
|
||||||
|
assertFalse(inputProcessor.isKeyboardLocked());
|
||||||
|
|
||||||
|
// Now enter a character in field 1
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
inputProcessor.processChar('X');
|
||||||
|
|
||||||
|
// Attempt to send AID_ENTER again -> Should succeed
|
||||||
|
inputProcessor.sendAid(AID_ENTER);
|
||||||
|
assertEquals(AID_ENTER, sentAid.get(), "AID_ENTER must be transmitted after entering required character");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMandatoryFillValidation() {
|
||||||
|
// Set up field 1 at pos 0 (unprotected, length 4: pos 1, 2, 3, 4, Mandatory Fill: 0x01)
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.getCell(0).vl = ExtendedAttribute.VL_MANDATORY_FILL;
|
||||||
|
|
||||||
|
// Set up field 2 at pos 5 (unprotected, length 4: pos 6, 7, 8, 9)
|
||||||
|
screen.setFieldAttribute(5, (byte) FA_PRINTABLE);
|
||||||
|
|
||||||
|
// Terminator field at pos 10
|
||||||
|
screen.setFieldAttribute(10, (byte) (FA_PROTECT | FA_NUMERIC));
|
||||||
|
|
||||||
|
// Type only 2 characters out of 4 into field 1 (partial fill)
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
inputProcessor.processChar('A');
|
||||||
|
inputProcessor.processChar('B');
|
||||||
|
|
||||||
|
// 1. Attempt to tab away from incomplete field 1 -> Must inhibit
|
||||||
|
inputProcessor.tab();
|
||||||
|
assertEquals(ECLConstants.INHIBIT_OPERATOR_DUE, oia.getInputInhibited(), "Tab away from incomplete Mandatory Fill must inhibit");
|
||||||
|
assertTrue(inputProcessor.isKeyboardLocked());
|
||||||
|
|
||||||
|
// Cursor should remain in field 1
|
||||||
|
int curAddr = screen.getCursorAddress();
|
||||||
|
assertTrue(curAddr >= 1 && curAddr <= 4, "Cursor must remain within incomplete field");
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
inputProcessor.reset();
|
||||||
|
|
||||||
|
// 2. Attempt to send AID_ENTER with incomplete field 1 -> Must inhibit
|
||||||
|
inputProcessor.sendAid(AID_ENTER);
|
||||||
|
assertEquals(-1, sentAid.get(), "AID_ENTER must NOT transmit when Mandatory Fill field is incomplete");
|
||||||
|
assertEquals(ECLConstants.INHIBIT_OPERATOR_DUE, oia.getInputInhibited());
|
||||||
|
assertTrue(inputProcessor.isKeyboardLocked());
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
inputProcessor.reset();
|
||||||
|
|
||||||
|
// 3. Complete field 1 (type remaining 2 characters)
|
||||||
|
screen.setCursorAddress(3);
|
||||||
|
inputProcessor.processChar('C');
|
||||||
|
inputProcessor.processChar('D');
|
||||||
|
|
||||||
|
// Set cursor back to field 1 (pos 1) to test tabbing to field 2 (pos 6)
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
inputProcessor.tab();
|
||||||
|
assertEquals(6, screen.getCursorAddress(), "Tab must succeed to next field once Mandatory Fill is complete");
|
||||||
|
|
||||||
|
// Sending AID_ENTER should now succeed
|
||||||
|
inputProcessor.sendAid(AID_ENTER);
|
||||||
|
assertEquals(AID_ENTER, sentAid.get(), "AID_ENTER must transmit once all validation requirements are met");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTriggerFieldAutoAid() {
|
||||||
|
// Set up field at pos 0 (unprotected, length 3: pos 1, 2, 3, Trigger: 0x04)
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.getCell(0).vl = ExtendedAttribute.VL_TRIGGER;
|
||||||
|
|
||||||
|
// Terminator at pos 4
|
||||||
|
screen.setFieldAttribute(4, (byte) (FA_PROTECT | FA_NUMERIC));
|
||||||
|
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
// Type 1st char: no AID
|
||||||
|
inputProcessor.processChar('1');
|
||||||
|
assertEquals(-1, sentAid.get());
|
||||||
|
|
||||||
|
// Type 2nd char: no AID
|
||||||
|
inputProcessor.processChar('2');
|
||||||
|
assertEquals(-1, sentAid.get());
|
||||||
|
|
||||||
|
// Type 3rd (last) char: should automatically trigger AID_ENTER!
|
||||||
|
inputProcessor.processChar('3');
|
||||||
|
assertEquals(AID_ENTER, sentAid.get(), "Typing into the last position of a Trigger field must automatically send AID_ENTER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShortReadAidBypassesValidation() {
|
||||||
|
// Field with Mandatory Entry at pos 0
|
||||||
|
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.getCell(0).vl = ExtendedAttribute.VL_MANDATORY_ENTRY;
|
||||||
|
screen.setFieldAttribute(5, (byte) (FA_PROTECT | FA_NUMERIC));
|
||||||
|
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
// CLEAR AID must bypass validation
|
||||||
|
inputProcessor.sendAid(AID_CLEAR);
|
||||||
|
assertEquals(AID_CLEAR, sentAid.get());
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
inputProcessor.reset();
|
||||||
|
sentAid.set(-1);
|
||||||
|
|
||||||
|
// PA1 AID must bypass validation
|
||||||
|
inputProcessor.sendAid(AID_PA1);
|
||||||
|
assertEquals(AID_PA1, sentAid.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCursorSelectAndDeferredToggle() {
|
||||||
|
// Detectable selectable field at pos 0 (FA_SELECTABLE: bits 4-5 = 01 or 11)
|
||||||
|
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | 0x08)); // FA_PRINTABLE (0xC0 | 0x00) with detectable bits
|
||||||
|
|
||||||
|
// Designator at pos 1: '?' (0x6F)
|
||||||
|
ExtendedAttribute desCell = screen.getCell(1);
|
||||||
|
desCell.ec = 0x6F;
|
||||||
|
desCell.ucs4 = '?';
|
||||||
|
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
// Toggle via cursorSelect()
|
||||||
|
boolean selected = inputProcessor.cursorSelect();
|
||||||
|
assertTrue(selected);
|
||||||
|
assertEquals('>', desCell.ucs4, "Cursor select on '?' must toggle to '>'");
|
||||||
|
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0, "Toggling to '>' must set MDT");
|
||||||
|
|
||||||
|
// Toggle back to '?'
|
||||||
|
selected = inputProcessor.cursorSelect();
|
||||||
|
assertTrue(selected);
|
||||||
|
assertEquals('?', desCell.ucs4, "Cursor select on '>' must toggle back to '?'");
|
||||||
|
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0, "Toggling to '?' must clear MDT");
|
||||||
|
|
||||||
|
// Immediate select space designator at pos 5
|
||||||
|
screen.setFieldAttribute(5, (byte) (FA_PRINTABLE | 0x08));
|
||||||
|
ExtendedAttribute spaceDesCell = screen.getCell(6);
|
||||||
|
spaceDesCell.ec = 0x40;
|
||||||
|
spaceDesCell.ucs4 = ' ';
|
||||||
|
|
||||||
|
screen.setCursorAddress(6);
|
||||||
|
selected = inputProcessor.cursorSelect();
|
||||||
|
assertTrue(selected);
|
||||||
|
assertEquals(AID_SELECT, sentAid.get(), "Immediate space designator selection must transmit AID_SELECT (0x7E)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
package haus.nightmare.lib3270j.datastream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.graphics.*;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
|
public class QueryReplyPhase18Test {
|
||||||
|
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private QueryReplyBuilder qrBuilder;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||||
|
qrBuilder = new QueryReplyBuilder(screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildCharsetsInBothAndProgrammedSymbolsModes() {
|
||||||
|
// 1. In GraphicsMode.BOTH, buildCharsets must advertise LoadPS 0x0A (Format 1 & 3) and PSA..PSF
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
|
byte[] bothCharsets = qrBuilder.buildCharsets();
|
||||||
|
assertNotNull(bothCharsets);
|
||||||
|
assertEquals(65, bothCharsets.length, "buildCharsets in BOTH mode must be 65 bytes");
|
||||||
|
assertEquals((byte) 0xA2, bothCharsets[0], "Flags1 must be 0xA2 (GE | PS | CGCSGID)");
|
||||||
|
assertEquals(0x0A, bothCharsets[4] & 0xFF, "LoadPS format must be 0x0A (Format 1 and 3)");
|
||||||
|
assertEquals(7, bothCharsets[8] & 0xFF, "Descriptor length must be 7");
|
||||||
|
|
||||||
|
// Verify all 6 loadable sets PSA..PSF
|
||||||
|
// Descriptor offset starts at index 9:
|
||||||
|
// Set 0 (Base): idx 9
|
||||||
|
assertEquals(0x00, bothCharsets[9] & 0xFF);
|
||||||
|
assertEquals(0x10, bothCharsets[10] & 0xFF);
|
||||||
|
// Set 1 (APL): idx 16
|
||||||
|
assertEquals(0x01, bothCharsets[16] & 0xFF);
|
||||||
|
assertEquals(0x00, bothCharsets[17] & 0xFF);
|
||||||
|
// Slot 2: PSA (LCID 0x40, single plane 0x80)
|
||||||
|
assertEquals(0x02, bothCharsets[23] & 0xFF);
|
||||||
|
assertEquals((byte) 0x80, bothCharsets[24]);
|
||||||
|
assertEquals(0x40, bothCharsets[25] & 0xFF);
|
||||||
|
// Slot 3: PSB (LCID 0x41, single plane 0x80)
|
||||||
|
assertEquals(0x03, bothCharsets[30] & 0xFF);
|
||||||
|
assertEquals((byte) 0x80, bothCharsets[31]);
|
||||||
|
assertEquals(0x41, bothCharsets[32] & 0xFF);
|
||||||
|
// Slot 4: PSC (LCID 0x42, triple plane 0xC0)
|
||||||
|
assertEquals(0x04, bothCharsets[37] & 0xFF);
|
||||||
|
assertEquals((byte) 0xC0, bothCharsets[38]);
|
||||||
|
assertEquals(0x42, bothCharsets[39] & 0xFF);
|
||||||
|
// Slot 5: PSD (LCID 0x43, triple plane 0xC0)
|
||||||
|
assertEquals(0x05, bothCharsets[44] & 0xFF);
|
||||||
|
assertEquals((byte) 0xC0, bothCharsets[45]);
|
||||||
|
assertEquals(0x43, bothCharsets[46] & 0xFF);
|
||||||
|
// Slot 6: PSE (LCID 0x44, triple plane 0xC0)
|
||||||
|
assertEquals(0x06, bothCharsets[51] & 0xFF);
|
||||||
|
assertEquals((byte) 0xC0, bothCharsets[52]);
|
||||||
|
assertEquals(0x44, bothCharsets[53] & 0xFF);
|
||||||
|
// Slot 7: PSF (LCID 0x45, triple plane 0xC0)
|
||||||
|
assertEquals(0x07, bothCharsets[58] & 0xFF);
|
||||||
|
assertEquals((byte) 0xC0, bothCharsets[59]);
|
||||||
|
assertEquals(0x45, bothCharsets[60] & 0xFF);
|
||||||
|
|
||||||
|
// 2. In GraphicsMode.PROGRAMMED_SYMBOLS, buildCharsets must also be 65 bytes with LoadPS 0x0A,
|
||||||
|
// but default charHeight is 12 (3279-2) instead of 16 (3179G)
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
|
||||||
|
byte[] psCharsets = qrBuilder.buildCharsets();
|
||||||
|
assertEquals(65, psCharsets.length);
|
||||||
|
assertEquals((byte) 0xA2, psCharsets[0]);
|
||||||
|
assertEquals(12, psCharsets[3] & 0xFF, "Programmed Symbols only charHeight is 12 (3279-2)");
|
||||||
|
assertEquals(0x0A, psCharsets[4] & 0xFF);
|
||||||
|
// Descriptors (bytes 8..64) should be identical
|
||||||
|
for (int i = 8; i < 65; i++) {
|
||||||
|
assertEquals(bothCharsets[i], psCharsets[i], "Descriptor byte at index " + i + " must match");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildCharsetsInNoneAndVectorOnlyModes() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
|
||||||
|
byte[] noneCharsets = qrBuilder.buildCharsets();
|
||||||
|
assertEquals(23, noneCharsets.length);
|
||||||
|
assertEquals((byte) 0x82, noneCharsets[0]);
|
||||||
|
assertEquals(0x00, noneCharsets[4] & 0xFF, "LoadPS format must be 0x00 in NONE mode");
|
||||||
|
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||||
|
byte[] vecCharsets = qrBuilder.buildCharsets();
|
||||||
|
assertEquals(23, vecCharsets.length);
|
||||||
|
assertEquals((byte) 0x82, vecCharsets[0]);
|
||||||
|
assertEquals(0x00, vecCharsets[4] & 0xFF, "LoadPS format must be 0x00 in VECTOR_GRAPHICS mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLoadpsWithRwsAsLcidOrSlotIds() {
|
||||||
|
ProgramSymbolManager psm = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// 1. Load PSA (LCID 0x40) with RWS = 0x40 (using LCID as slot ID)
|
||||||
|
byte[] payload1 = new byte[4 + 18];
|
||||||
|
payload1[0] = 0x01; // Format 1
|
||||||
|
payload1[1] = 0x40; // LCID 0x40
|
||||||
|
payload1[2] = 0x41; // Codepoint 0x41
|
||||||
|
payload1[3] = 0x40; // RWS passed as 0x40 (PSA)
|
||||||
|
payload1[4] = (byte) 0x80; // Pixel (0,0) = 1
|
||||||
|
psm.loadps(payload1);
|
||||||
|
|
||||||
|
ProgramSymbolSet setA = psm.getSymbolSet(0x40);
|
||||||
|
assertNotNull(setA, "PSA symbol set must be allocated");
|
||||||
|
assertFalse(setA.isTriplePlane());
|
||||||
|
ProgramSymbolSet.SymbolSlot slotA = psm.getSymbol(0x40, 0x41);
|
||||||
|
assertNotNull(slotA);
|
||||||
|
assertEquals(1, slotA.getPixelData()[0] & 0xFF);
|
||||||
|
|
||||||
|
// 2. Load PSC (LCID 0x42) with RWS = 0x42 (Triple Plane)
|
||||||
|
byte[] payload2 = new byte[4 + 18];
|
||||||
|
payload2[0] = 0x01; // Format 1
|
||||||
|
payload2[1] = 0x42; // LCID 0x42
|
||||||
|
payload2[2] = 0x41; // Codepoint 0x41
|
||||||
|
payload2[3] = 0x42; // RWS passed as 0x42 (PSC)
|
||||||
|
payload2[4] = (byte) 0x80; // Pixel (0,0) = 1
|
||||||
|
psm.loadps(payload2);
|
||||||
|
|
||||||
|
ProgramSymbolSet setC = psm.getSymbolSet(0x42);
|
||||||
|
assertNotNull(setC, "PSC symbol set must be allocated");
|
||||||
|
assertTrue(setC.isTriplePlane(), "PSC must be recognized as triple-plane");
|
||||||
|
ProgramSymbolSet.SymbolSlot slotC = psm.getSymbol(0x42, 0x41);
|
||||||
|
assertNotNull(slotC);
|
||||||
|
assertTrue(slotC.isTriplePlane());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLoadpsFormat3VariableDimensions() {
|
||||||
|
ProgramSymbolManager psm = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// Load symbol set PSE (LCID 0x44) with Format 3 and custom cell dimensions 12x24
|
||||||
|
int w = 12;
|
||||||
|
int h = 24;
|
||||||
|
int bytesPerSymbol = (w * h + 7) / 8; // (288 + 7) / 8 = 36 bytes
|
||||||
|
byte[] payload = new byte[4 + 6 + bytesPerSymbol];
|
||||||
|
payload[0] = (byte) 0x83; // Format 3, extHeader = true
|
||||||
|
payload[1] = 0x44; // LCID 0x44 (PSE)
|
||||||
|
payload[2] = 0x41; // Codepoint 0x41
|
||||||
|
payload[3] = 0x06; // RWS 6 (PSE)
|
||||||
|
// Ext header (6 bytes):
|
||||||
|
payload[4] = 0x06; // Ext header length = 6
|
||||||
|
payload[5] = 0x00; // reserved
|
||||||
|
payload[6] = (byte) w; // LW = 12
|
||||||
|
payload[7] = (byte) h; // LH = 24
|
||||||
|
payload[8] = 0x00; // reserved
|
||||||
|
payload[9] = 0x00; // colorPlane = 0 (all planes)
|
||||||
|
|
||||||
|
// Set pixel at (0, 0)
|
||||||
|
payload[10] = (byte) 0x80;
|
||||||
|
// Set pixel at (11, 23) -> index 12 * 24 - 1 = 287 -> byte 287 / 8 = 35, bit 7 - (287 % 8) = 0
|
||||||
|
payload[10 + 35] = (byte) 0x01;
|
||||||
|
|
||||||
|
psm.loadps(payload);
|
||||||
|
|
||||||
|
ProgramSymbolSet set = psm.getSymbolSet(0x44);
|
||||||
|
assertNotNull(set);
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x44, 0x41);
|
||||||
|
assertNotNull(slot);
|
||||||
|
assertEquals(12, slot.getWidth());
|
||||||
|
assertEquals(24, slot.getHeight());
|
||||||
|
assertTrue(slot.isTriplePlane());
|
||||||
|
|
||||||
|
byte[] pix = slot.getPixelData();
|
||||||
|
assertEquals(12 * 24, pix.length);
|
||||||
|
assertEquals(7, pix[0] & 0xFF, "Pixel (0,0) should be 7 (all planes)");
|
||||||
|
assertEquals(7, pix[12 * 24 - 1] & 0xFF, "Pixel (11,23) should be 7 (all planes)");
|
||||||
|
assertEquals(0, pix[1] & 0xFF, "Unset pixel (1,0) should be 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTriplePlaneMultiColorCompositeMerging() {
|
||||||
|
ProgramSymbolManager psm = new ProgramSymbolManager();
|
||||||
|
|
||||||
|
// Load 9x16 Format 1 glyph for PSC (LCID 0x42) using 3 planes:
|
||||||
|
// Plane 1: Red at pixel (0,0) and (1,0)
|
||||||
|
byte[] p1 = new byte[4 + 6 + 18];
|
||||||
|
p1[0] = (byte) 0x81; p1[1] = 0x42; p1[2] = 0x41; p1[3] = 0x04; p1[4] = 0x06;
|
||||||
|
p1[9] = 0x01; // Plane 1 = Red
|
||||||
|
p1[10] = (byte) 0x80; // Row 0 Col 0
|
||||||
|
p1[12] = (byte) 0x80; // Row 0 Col 1
|
||||||
|
psm.loadps(p1);
|
||||||
|
|
||||||
|
// Plane 2: Green at pixel (1,0) and (2,0)
|
||||||
|
byte[] p2 = new byte[4 + 6 + 18];
|
||||||
|
p2[0] = (byte) 0x81; p2[1] = 0x42; p2[2] = 0x41; p2[3] = 0x04; p2[4] = 0x06;
|
||||||
|
p2[9] = 0x02; // Plane 2 = Green
|
||||||
|
p2[12] = (byte) 0xC0; // Row 0 Col 1, 2
|
||||||
|
psm.loadps(p2);
|
||||||
|
|
||||||
|
// Plane 4: Blue at pixel (0,0), (1,0), (3,0)
|
||||||
|
byte[] p3 = new byte[4 + 6 + 18];
|
||||||
|
p3[0] = (byte) 0x81; p3[1] = 0x42; p3[2] = 0x41; p3[3] = 0x04; p3[4] = 0x06;
|
||||||
|
p3[9] = 0x04; // Plane 4 = Blue
|
||||||
|
p3[10] = (byte) 0x80; // Row 0 Col 0
|
||||||
|
p3[12] = (byte) 0xA0; // Row 0 Col 1, 3
|
||||||
|
psm.loadps(p3);
|
||||||
|
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x42, 0x41);
|
||||||
|
assertNotNull(slot);
|
||||||
|
byte[] pix = slot.getPixelData();
|
||||||
|
|
||||||
|
// Pixel (0,0): Red (1) | Blue (4) = 5 (Pink)
|
||||||
|
assertEquals(5, pix[0] & 0xFF);
|
||||||
|
// Pixel (1,0): Red (1) | Green (2) | Blue (4) = 7 (White)
|
||||||
|
assertEquals(7, pix[1] & 0xFF);
|
||||||
|
// Pixel (2,0): Green (2) alone
|
||||||
|
assertEquals(2, pix[2] & 0xFF);
|
||||||
|
// Pixel (3,0): Blue (4) alone
|
||||||
|
assertEquals(4, pix[3] & 0xFF);
|
||||||
|
|
||||||
|
// Verify composite ARGB colors:
|
||||||
|
int[] rgb = slot.getRgbPixels(0, 0);
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[3], rgb[0], "Color at (0,0) should be Pink (GOCA index 3)");
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[7], rgb[1], "Color at (1,0) should be White (GOCA index 7)");
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[4], rgb[2], "Color at (2,0) should be Green (GOCA index 4)");
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[1], rgb[3], "Color at (3,0) should be Blue (GOCA index 1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGocaQueryRepliesBinaryLayout() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
|
|
||||||
|
// 1. QR_SEGMENT (0xB0)
|
||||||
|
byte[] seg = qrBuilder.buildSegment();
|
||||||
|
assertEquals(7, seg.length);
|
||||||
|
assertEquals((byte) 0x80, seg[0]);
|
||||||
|
assertEquals(0x02, seg[1]);
|
||||||
|
assertEquals(0x00, seg[2]);
|
||||||
|
assertEquals(0x00, seg[3]);
|
||||||
|
assertEquals(0x00, seg[4]);
|
||||||
|
assertEquals((byte) 0xFC, seg[5]);
|
||||||
|
assertEquals(0x00, seg[6]);
|
||||||
|
|
||||||
|
// 2. QR_PROCEDURE (0xB1)
|
||||||
|
byte[] proc = qrBuilder.buildProcedure();
|
||||||
|
assertEquals(17, proc.length);
|
||||||
|
assertEquals(0x00, proc[0]);
|
||||||
|
assertEquals(0x01, proc[1]);
|
||||||
|
assertEquals(0x00, proc[2]);
|
||||||
|
assertEquals(0x00, proc[3]);
|
||||||
|
assertEquals(0x00, proc[4]);
|
||||||
|
assertEquals((byte) 0xFC, proc[5]);
|
||||||
|
assertEquals(0x00, proc[6]);
|
||||||
|
|
||||||
|
// 3. QR_LINETYPE (0xB2)
|
||||||
|
byte[] lt = qrBuilder.buildLineType();
|
||||||
|
assertEquals(20, lt.length);
|
||||||
|
assertEquals(0x00, lt[0]);
|
||||||
|
assertEquals(0x09, lt[1]);
|
||||||
|
assertEquals(0x00, lt[2]);
|
||||||
|
assertEquals(0x07, lt[3]);
|
||||||
|
for (int i = 1; i <= 8; i++) {
|
||||||
|
assertEquals(i, lt[4 + (i - 1) * 2] & 0xFF);
|
||||||
|
assertEquals(i, lt[4 + (i - 1) * 2 + 1] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. QR_PORT (0xB3)
|
||||||
|
byte[] port = qrBuilder.buildPort();
|
||||||
|
assertEquals(64, port.length);
|
||||||
|
|
||||||
|
// 5. QR_GRCOLOR (0xB4)
|
||||||
|
byte[] grColor = qrBuilder.buildGrColor();
|
||||||
|
assertEquals(105, grColor.length); // 9 header + 16 * 6 = 105 bytes payload
|
||||||
|
|
||||||
|
// 6. QR_GRSYMBOLSET (0xB6)
|
||||||
|
byte[] grSym = qrBuilder.buildGrSymbolSet();
|
||||||
|
assertEquals(29, grSym.length);
|
||||||
|
// Trailing 7 bytes must be zero matching HoD QR_GRSYMBOLSET_S_STRING
|
||||||
|
for (int i = 22; i < 29; i++) {
|
||||||
|
assertEquals(0x00, grSym[i], "Trailing byte at index " + i + " must be 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDataStreamProcessorStructuredField0x06Dispatch() {
|
||||||
|
ScreenBuffer s = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||||
|
DataStreamProcessor dsp = new DataStreamProcessor(s, translator);
|
||||||
|
AtomicBoolean updated = new AtomicBoolean(false);
|
||||||
|
dsp.addScreenUpdateListener(() -> updated.set(true));
|
||||||
|
|
||||||
|
// Construct WSF with SF 0x06 (LoadPS direct)
|
||||||
|
// Length = 2 bytes (3 + 4 + 18 = 25 = 0x0019), SFID = 0x06, followed by 4+18 byte LoadPS payload
|
||||||
|
byte[] wsf = new byte[1 + 25];
|
||||||
|
wsf[0] = (byte) CMD_WSF; // WSF command (0x11)
|
||||||
|
wsf[1] = 0x00;
|
||||||
|
wsf[2] = 0x19; // length = 25
|
||||||
|
wsf[3] = 0x06; // SF_LOADPS_DIRECT (0x06)
|
||||||
|
// Payload:
|
||||||
|
wsf[4] = 0x01; // Format 1
|
||||||
|
wsf[5] = 0x40; // LCID 0x40 (PSA)
|
||||||
|
wsf[6] = 0x41; // Codepoint 0x41
|
||||||
|
wsf[7] = 0x02; // RWS 2
|
||||||
|
wsf[8] = (byte) 0x80; // Row 0 Col 0 bit set
|
||||||
|
|
||||||
|
dsp.processRecord(wsf, 0, wsf.length, true);
|
||||||
|
|
||||||
|
assertTrue(updated.get(), "Screen update listener must be triggered after SF 0x06");
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = dsp.getProgramSymbolManager().getSymbol(0x40, 0x41);
|
||||||
|
assertNotNull(slot, "Symbol 0x41 must be loaded in PSA");
|
||||||
|
assertEquals(1, slot.getPixelData()[0] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGraphicsPlanePatternFillWithTriplePlanePss() {
|
||||||
|
GraphicsPlane plane = new GraphicsPlane(80, 24);
|
||||||
|
ProgramSymbolManager psm = new ProgramSymbolManager();
|
||||||
|
plane.setProgramSymbolManager(psm);
|
||||||
|
|
||||||
|
// Load 9x16 Format 1 glyph for PSC (LCID 0x42) with Red (1) and Blue (4) = Pink (5)
|
||||||
|
byte[] p = new byte[4 + 6 + 18];
|
||||||
|
p[0] = (byte) 0x81; p[1] = 0x42; p[2] = 0x41; p[3] = 0x04; p[4] = 0x06;
|
||||||
|
p[9] = 0x01; // Plane 1 = Red
|
||||||
|
p[10] = (byte) 0xC0; // Row 0 and Row 1, Col 0
|
||||||
|
psm.loadps(p);
|
||||||
|
|
||||||
|
byte[] pBlue = new byte[4 + 6 + 18];
|
||||||
|
pBlue[0] = (byte) 0x81; pBlue[1] = 0x42; pBlue[2] = 0x41; pBlue[3] = 0x04; pBlue[4] = 0x06;
|
||||||
|
pBlue[9] = 0x04; // Plane 4 = Blue
|
||||||
|
pBlue[10] = (byte) 0xC0; // Row 0 and Row 1, Col 0
|
||||||
|
psm.loadps(pBlue);
|
||||||
|
|
||||||
|
// Fill a small 9x16 rectangle using patternSet = 0x42, pattern = 0x41
|
||||||
|
int[] px = new int[]{0, 9, 9, 0};
|
||||||
|
int[] py = new int[]{0, 0, 16, 16};
|
||||||
|
plane.fillArea(px, py, 4, new int[]{4}, 1, 0xFFFFFFFF, 0x42, 0x41, false, 0, 0, 1, 0, 0xFF000000, GocaConstants.FILL_RULE_EVEN_ODD);
|
||||||
|
|
||||||
|
// Pixel (0,1) must have been rendered with Pink composite color (GOCA_COLORS[3]), not flat foreground 0xFFFFFFFF!
|
||||||
|
int renderedPixel = plane.getPixel(0, 1);
|
||||||
|
assertEquals(GocaConstants.GOCA_COLORS[3], renderedPixel, "Triple plane pattern fill must render with composite color (Pink)");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user