This commit is contained in:
+4
-2
@@ -10,8 +10,10 @@ allprojects {
|
|||||||
subprojects {
|
subprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
java {
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
|||||||
@@ -163,10 +163,20 @@ public class TerminalPanel extends JPanel {
|
|||||||
// IMPORTANT ARCHITECTURE NOTE:
|
// IMPORTANT ARCHITECTURE NOTE:
|
||||||
// 1. In GDDM Graphic Cursor Mode (3179G / GOCA), mouse events represent graphic light-pen touches.
|
// 1. In GDDM Graphic Cursor Mode (3179G / GOCA), mouse events represent graphic light-pen touches.
|
||||||
// Text drag-selection is explicitly suppressed so blue selection boxes do not artifact over graphics.
|
// Text drag-selection is explicitly suppressed so blue selection boxes do not artifact over graphics.
|
||||||
// 2. In Text Light Pen mode (Alt+L on 3270 formatted screens), clicks toggle selectable fields.
|
// 2. In Text Light Pen / Selectable Field mode (per IBM Host On-Demand MouseMgr.java / PS3270.java),
|
||||||
|
// clicks on detectable fields (faIsSelectable) trigger field selection or cursor select.
|
||||||
// 3. In standard alphanumeric mode, click-drag selects text for copy-paste.
|
// 3. In standard alphanumeric mode, click-drag selects text for copy-paste.
|
||||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
||||||
if (isGraphic || lightPenMode) {
|
boolean isSelectableField = false;
|
||||||
|
if (sb.isFormatted()) {
|
||||||
|
int faPos = sb.findFieldAttribute(row * displayCols + col);
|
||||||
|
if (faPos >= 0) {
|
||||||
|
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||||
|
isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||||
|
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isGraphic || lightPenMode || isSelectableField) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
} else {
|
} else {
|
||||||
selectionStartRow = row;
|
selectionStartRow = row;
|
||||||
@@ -277,6 +287,25 @@ public class TerminalPanel extends JPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatic Light-Pen / Selectable Field detection on mouse click:
|
||||||
|
// Per IBM Host On-Demand (MouseMgr.java / PS3270.java) and 3270 specifications:
|
||||||
|
// If the clicked screen location belongs to a detectable field (faIsSelectable and not faIsZero),
|
||||||
|
// automatically process light-pen / cursor selection (e.g. immediate selection, Enter, or toggling ? -> >).
|
||||||
|
if (sb.isFormatted()) {
|
||||||
|
int faPos = sb.findFieldAttribute(clickAddr);
|
||||||
|
if (faPos >= 0) {
|
||||||
|
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||||
|
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||||
|
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
||||||
|
clearSelection();
|
||||||
|
if (client.lightPenSelect(clickAddr)) {
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (lightPenMode) {
|
if (lightPenMode) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
sb.setCursorAddress(clickAddr);
|
sb.setCursorAddress(clickAddr);
|
||||||
@@ -947,8 +976,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||||
|
|
||||||
int fontSize = (int) Math.round(ch * 0.95);
|
int fontSize = Math.min((int) Math.round(ch * 0.82), (int) Math.round(cw * 1.45));
|
||||||
if (fontSize < 10) fontSize = 10;
|
if (fontSize < 9) fontSize = 9;
|
||||||
Font f = (boldTerminalFont != null ? boldTerminalFont : terminalFont).deriveFont((float) fontSize);
|
Font f = (boldTerminalFont != null ? boldTerminalFont : terminalFont).deriveFont((float) fontSize);
|
||||||
g2.setFont(f);
|
g2.setFont(f);
|
||||||
FontMetrics fm = g2.getFontMetrics();
|
FontMetrics fm = g2.getFontMetrics();
|
||||||
@@ -1154,10 +1183,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
Font f = bold ? boldTerminalFont : terminalFont;
|
Font f = bold ? boldTerminalFont : terminalFont;
|
||||||
g2.setFont(f);
|
g2.setFont(f);
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
|
int textY = y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2);
|
||||||
if (ch < 128) {
|
if (ch < 128) {
|
||||||
g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent);
|
g2.drawString(CHAR_STRINGS[ch], x, textY);
|
||||||
} else {
|
} else {
|
||||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
g2.drawString(String.valueOf(ch), x, textY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1165,8 +1195,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Draw underline
|
// Draw underline
|
||||||
if (underline) {
|
if (underline) {
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
g2.drawLine(x, y + cellHeight - fontDescent,
|
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
|
||||||
x + cellWidth - 1, y + cellHeight - fontDescent);
|
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw selection highlight
|
// Draw selection highlight
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
// Graphics & Programmed Symbols
|
// Graphics & Programmed Symbols
|
||||||
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
|
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
|
||||||
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(720, 384);
|
||||||
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
||||||
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||||
private int currentGocaSubtype = 0;
|
private int currentGocaSubtype = 0;
|
||||||
@@ -853,6 +853,11 @@ public class DataStreamProcessor {
|
|||||||
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
|
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
|
||||||
|
|
||||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
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
|
if (flags == 0x80) { // SPAN_FIRST
|
||||||
gocaAccumulator.reset();
|
gocaAccumulator.reset();
|
||||||
|
|||||||
@@ -293,14 +293,36 @@ public class QueryReplyBuilder {
|
|||||||
out.write((Yr_3279_2 >> 16) & 0xFF);
|
out.write((Yr_3279_2 >> 16) & 0xFF);
|
||||||
out.write((Yr_3279_2 >> 8) & 0xFF);
|
out.write((Yr_3279_2 >> 8) & 0xFF);
|
||||||
out.write(Yr_3279_2 & 0xFF);
|
out.write(Yr_3279_2 & 0xFF);
|
||||||
out.write(SW_3279_2); // AW
|
int charW = getCharWidth();
|
||||||
out.write(SH_3279_2); // AH
|
int charH = getCharHeight();
|
||||||
|
out.write(charW); // AW
|
||||||
|
out.write(charH); // AH
|
||||||
int buf = maxCols * maxRows;
|
int buf = maxCols * maxRows;
|
||||||
out.write((buf >> 8) & 0xFF); // buffer size high
|
out.write((buf >> 8) & 0xFF); // buffer size high
|
||||||
out.write(buf & 0xFF); // buffer size low
|
out.write(buf & 0xFF); // buffer size low
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCharWidth() {
|
||||||
|
return SW_3279_2; // 9
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCharHeight() {
|
||||||
|
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES:
|
||||||
|
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations:
|
||||||
|
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes
|
||||||
|
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
|
||||||
|
// GDDM then places the top menu bar at Row 1 (gy = 187..200).
|
||||||
|
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
|
||||||
|
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
|
||||||
|
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
|
||||||
|
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
|
||||||
|
//
|
||||||
|
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
|
||||||
|
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
|
||||||
|
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] buildAlphaPartitions(int maxRows) {
|
private byte[] buildAlphaPartitions(int maxRows) {
|
||||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||||
@@ -312,13 +334,16 @@ public class QueryReplyBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildCharsets() {
|
private byte[] buildCharsets() {
|
||||||
|
int charW = getCharWidth();
|
||||||
|
int charH = getCharHeight();
|
||||||
|
|
||||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||||
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
// Programmed Symbols mode (3279 PS 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
|
||||||
out.write(SW_3279_2); // SDW (9)
|
out.write(charW); // SDW (9)
|
||||||
out.write(SH_3279_2); // SDH (12)
|
out.write(charH); // SDH (16 for 3179G, 12 for 3279-2)
|
||||||
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
||||||
out.write(0x00); // Load PS device type (high)
|
out.write(0x00); // Load PS device type (high)
|
||||||
out.write(0x00); // Load PS device type (low)
|
out.write(0x00); // Load PS device type (low)
|
||||||
@@ -343,8 +368,8 @@ public class QueryReplyBuilder {
|
|||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
||||||
out.write(0x82); // flags: GE, CGCSGID present
|
out.write(0x82); // flags: GE, CGCSGID present
|
||||||
out.write(0x00); // more flags
|
out.write(0x00); // more flags
|
||||||
out.write(SW_3279_2); // SDW - default char width (9)
|
out.write(charW); // SDW - default char width (9)
|
||||||
out.write(SH_3279_2); // SDH - default char height (12)
|
out.write(charH); // SDH - default char height (16 for 3179G, 12 for 3279-2)
|
||||||
out.write(0x00); // LoadPS format (0x00)
|
out.write(0x00); // LoadPS format (0x00)
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ package haus.nightmare.lib3270j.graphics;
|
|||||||
*/
|
*/
|
||||||
public class GraphicInputBuilder {
|
public class GraphicInputBuilder {
|
||||||
|
|
||||||
// 56-byte template mask from IBM Host On-Demand (HODInput.java)
|
// 56-byte template mask from IBM Host On-Demand (HODInput.java).
|
||||||
|
// Note on Structured Field Length (bytes 0-1):
|
||||||
|
// IBM HOD sets bytes 0-1 to 0x00 0x34 (52 decimal). In 3270 GOCA architecture,
|
||||||
|
// the Data Unit Object Control payload is 52 bytes. Overwriting this with 0x00 0x38 (56)
|
||||||
|
// causes the host's 3270 inbound structured field decoder to misalign the trailing AID byte
|
||||||
|
// (0x7D) and cursor address by 4 bytes, causing host GDDM to reject the click with an alarm beep.
|
||||||
private static final byte[] MASK = new byte[] {
|
private static final byte[] MASK = new byte[] {
|
||||||
0x00, 0x38, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
|
0x00, 0x34, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
|
||||||
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01,
|
0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
|
||||||
@@ -60,9 +65,8 @@ public class GraphicInputBuilder {
|
|||||||
byte[] sf = new byte[MASK.length];
|
byte[] sf = new byte[MASK.length];
|
||||||
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
||||||
|
|
||||||
// Byte 0-1: Structured Field Length (56 bytes)
|
// Bytes 0-1: Structured Field Length (0x0034 = 52 decimal per IBM HODInput.java / GOCA architecture).
|
||||||
sf[0] = (byte) ((MASK.length >> 8) & 0xFF);
|
// Preserved from MASK; do not overwrite with MASK.length (56).
|
||||||
sf[1] = (byte) (MASK.length & 0xFF);
|
|
||||||
|
|
||||||
// Bytes 16-19: Device correlation class descriptor (0x23, 0x00, 0x23, 0x00)
|
// Bytes 16-19: Device correlation class descriptor (0x23, 0x00, 0x23, 0x00)
|
||||||
// Note: Preserved from MASK per IBM Host On-Demand (HODInput.java); do not overwrite with row/col.
|
// Note: Preserved from MASK per IBM Host On-Demand (HODInput.java); do not overwrite with row/col.
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ public class GraphicsPlane {
|
|||||||
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
||||||
};
|
};
|
||||||
|
|
||||||
private int canvasWidth = 800;
|
// 3179G Presentation Space metrics: 9x16 cell pitch (720x384 for Model 2, 720x688 for Model 4)
|
||||||
private int canvasHeight = 600;
|
private int screenCols = 80;
|
||||||
|
private int screenRows = 24;
|
||||||
|
private int canvasWidth = 720;
|
||||||
|
private int canvasHeight = 384;
|
||||||
private int[] rgbBuffer;
|
private int[] rgbBuffer;
|
||||||
private boolean hasContent = false;
|
private boolean hasContent = false;
|
||||||
private long updateCount = 0;
|
private long updateCount = 0;
|
||||||
|
|
||||||
private int screenCols = 80;
|
|
||||||
private int screenRows = 24;
|
|
||||||
private ProgramSymbolManager programSymbolManager;
|
private ProgramSymbolManager programSymbolManager;
|
||||||
|
|
||||||
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||||
@@ -595,20 +595,19 @@ public class GraphicsPlane {
|
|||||||
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||||
int bg = bgColorArgb;
|
int bg = bgColorArgb;
|
||||||
|
|
||||||
// Note / Logic for Future Iterations:
|
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
|
||||||
// In IBM 3179G / GDDM architecture, solid black fills (fillColor = GOCA_COLORS[8] / 0xFF000000,
|
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
|
||||||
// pattern = PT_SOLID / 16) are the standard mechanism used by host applications to ERASE
|
// Background Mix (GSBMX / bgMix):
|
||||||
// dropdown menus, dialog boxes, and dynamic regions from the screen.
|
// - 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.
|
||||||
// A prior failed attempt added an 'isTransparentBlack' guard:
|
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
|
||||||
// boolean isTransparentBlack = (fill == GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != MIX_OVER);
|
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
|
||||||
// if (... && !isTransparentBlack)
|
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
|
||||||
// This caused GDDM's menu erasure rectangles (which use fillColor = Black and bgMix = 5) to be
|
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
|
||||||
// silently skipped and discarded. As a result, closed dropdown menus were never erased, causing
|
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
||||||
// multiple menus (FILE, DRAW, TRANSFORM) to linger and stack on top of each other permanently.
|
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
|
||||||
//
|
|
||||||
// Solid black fills (0xFF000000) must always be rasterized to overwrite and erase previously drawn pixels.
|
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
||||||
if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
|
||||||
// Find polygon vertical bounds across all points
|
// Find polygon vertical bounds across all points
|
||||||
int minY = py[0];
|
int minY = py[0];
|
||||||
int maxY = py[0];
|
int maxY = py[0];
|
||||||
|
|||||||
@@ -244,7 +244,43 @@ public class InputProcessor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
|
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();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|
||||||
out.write(aidCode);
|
out.write(aidCode);
|
||||||
|
|||||||
+5
-1
@@ -132,11 +132,15 @@ public class QueryReplyBuilderTest {
|
|||||||
assertEquals(0x81, replies[3] & 0xFF);
|
assertEquals(0x81, replies[3] & 0xFF);
|
||||||
assertEquals(QR_DDM, replies[4] & 0xFF);
|
assertEquals(QR_DDM, replies[4] & 0xFF);
|
||||||
|
|
||||||
// Second SF should be Usable Area
|
// Second SF should be Usable Area: 23 bytes (4 bytes header + 19 bytes payload)
|
||||||
int pos2 = 1 + len1;
|
int pos2 = 1 + len1;
|
||||||
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||||
|
assertEquals(23, len2);
|
||||||
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
||||||
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
|
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
|
||||||
|
// Offset 0x15 (21 in SF payload = pos2 + 21): Buffer size high
|
||||||
|
int bufSize = ((replies[pos2 + 21] & 0xFF) << 8) | (replies[pos2 + 22] & 0xFF);
|
||||||
|
assertEquals(80 * 43, bufSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ public class InputProcessorTest {
|
|||||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
|
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
|
||||||
assertEquals(56, sf.length);
|
assertEquals(56, sf.length);
|
||||||
assertEquals(0x00, sf[0]);
|
assertEquals(0x00, sf[0]);
|
||||||
assertEquals(0x38, sf[1]); // Length = 56 bytes (0x0038)
|
assertEquals(0x34, sf[1]); // Length = 52 bytes (0x0034) matching IBM HODInput.java
|
||||||
assertEquals(0x0F, sf[2]); // SF ID = 0x0F
|
assertEquals(0x0F, sf[2]); // SF ID = 0x0F
|
||||||
assertEquals(0x0F, sf[3]); // SF ID = 0x0F
|
assertEquals(0x0F, sf[3]); // SF ID = 0x0F
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ public class InputProcessorTest {
|
|||||||
);
|
);
|
||||||
assertEquals(56, sfMouse.length);
|
assertEquals(56, sfMouse.length);
|
||||||
assertEquals(0x00, sfMouse[0]);
|
assertEquals(0x00, sfMouse[0]);
|
||||||
assertEquals(0x38, sfMouse[1]);
|
assertEquals(0x34, sfMouse[1]); // Length = 52 bytes (0x0034) matching IBM HODInput.java
|
||||||
assertEquals(0x23, sfMouse[16]);
|
assertEquals(0x23, sfMouse[16]);
|
||||||
assertEquals(0x00, sfMouse[17]);
|
assertEquals(0x00, sfMouse[17]);
|
||||||
assertEquals(0x23, sfMouse[18]);
|
assertEquals(0x23, sfMouse[18]);
|
||||||
@@ -394,9 +394,9 @@ public class InputProcessorTest {
|
|||||||
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes
|
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes
|
||||||
assertEquals(64, result.length);
|
assertEquals(64, result.length);
|
||||||
assertEquals((byte) AID_SF, result[0]);
|
assertEquals((byte) AID_SF, result[0]);
|
||||||
// SF length = 56 (0x00 0x38)
|
// SF length = 52 (0x00 0x34) per IBM HOD / GOCA specification
|
||||||
assertEquals(0x00, result[1]);
|
assertEquals(0x00, result[1]);
|
||||||
assertEquals(0x38, result[2]);
|
assertEquals(0x34, result[2]);
|
||||||
// SF ID = 0x0F0F
|
// SF ID = 0x0F0F
|
||||||
assertEquals(0x0F, result[3]);
|
assertEquals(0x0F, result[3]);
|
||||||
assertEquals(0x0F, result[4]);
|
assertEquals(0x0F, result[4]);
|
||||||
@@ -418,4 +418,37 @@ public class InputProcessorTest {
|
|||||||
// Trailing field content 'A' at 63
|
// Trailing field content 'A' at 63
|
||||||
assertEquals((byte) 0xC1, result[63]);
|
assertEquals((byte) 0xC1, result[63]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAidSelectFramingOmitsFieldData() {
|
||||||
|
// Formatted screen with an unprotected field at 0, modified, containing 'A' and 'B'
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||||
|
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
screen.getCell(1).ec = (byte) 0xC1;
|
||||||
|
screen.getCell(1).ucs4 = 'A';
|
||||||
|
screen.getCell(2).ec = (byte) 0xC2;
|
||||||
|
screen.getCell(2).ucs4 = 'B';
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
protected void sendAidResponse(byte[] data) {
|
||||||
|
sent.set(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.sendAid(AID_SELECT);
|
||||||
|
|
||||||
|
byte[] result = sent.get();
|
||||||
|
assertNotNull(result);
|
||||||
|
// For AID_SELECT (0x7E), the stream consists ONLY of:
|
||||||
|
// 1 byte AID (0x7E) + 2 bytes cursor address + 1 byte SBA (0x11) + 2 bytes designator address = 6 bytes total.
|
||||||
|
// Field character contents ('A', 'B') MUST NOT be sent per IBM 3270 DS architecture!
|
||||||
|
assertEquals(6, result.length);
|
||||||
|
assertEquals((byte) AID_SELECT, result[0]);
|
||||||
|
// SBA order at byte 3
|
||||||
|
assertEquals((byte) ORDER_SBA, result[3]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user