Compare commits
2 Commits
ebc0b5ae6f
...
2e9b6d325a
| Author | SHA1 | Date | |
|---|---|---|---|
|
2e9b6d325a
|
|||
|
540a8dfd1d
|
+4
-2
@@ -10,8 +10,10 @@ allprojects {
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
|
||||
@@ -453,7 +453,8 @@ public class SettingsDialog extends JDialog {
|
||||
JPanel main = new JPanel(new BorderLayout());
|
||||
|
||||
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"};
|
||||
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR",
|
||||
"ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL"};
|
||||
|
||||
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
|
||||
@Override
|
||||
@@ -465,7 +466,14 @@ public class SettingsDialog extends JDialog {
|
||||
// Populate table from Settings or Defaults
|
||||
for(String act : actions) {
|
||||
String def = act;
|
||||
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example
|
||||
if (def.equals("ERASE_INPUT")) def = "alt E";
|
||||
else if (def.equals("NEWLINE")) def = "shift ENTER";
|
||||
else if (def.equals("DUP")) def = "alt D";
|
||||
else if (def.equals("FIELD_MARK")) def = "alt M";
|
||||
else if (def.equals("ATTN")) def = "alt A";
|
||||
else if (def.equals("SYSREQ")) def = "alt S";
|
||||
else if (def.equals("CURSEL")) def = "alt Q";
|
||||
else if (def.equals("CLEAR")) def = "alt C";
|
||||
String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def);
|
||||
tempKeyBindings.put(act, current);
|
||||
keymapModel.addRow(new Object[]{act, current});
|
||||
|
||||
@@ -163,10 +163,20 @@ public class TerminalPanel extends JPanel {
|
||||
// IMPORTANT ARCHITECTURE NOTE:
|
||||
// 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.
|
||||
// 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.
|
||||
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();
|
||||
} else {
|
||||
selectionStartRow = row;
|
||||
@@ -259,10 +269,12 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
int gx = client.getGocaDecoder().getGraphicCursorX();
|
||||
int gy = client.getGocaDecoder().getGraphicCursorY();
|
||||
int hitSeg = client.getGocaDecoder().findPickedSegment(gx, gy);
|
||||
int hitTag = client.getGocaDecoder().getSegmentTag(hitSeg);
|
||||
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
|
||||
System.err.println(String.format(
|
||||
"TerminalPanel.mouseReleased: mouse=(%d, %d) offset=(%d, %d) grid=(%dx%d) px=(%d, %d) goca=(%d, %d) btn=%d",
|
||||
e.getX(), e.getY(), ox, oy, gridW, gridH, px, py, gx, gy, button
|
||||
"TerminalPanel.mouseReleased: mouse=(%d, %d) offset=(%d, %d) grid=(%dx%d) px=(%d, %d) goca=(%d, %d) hitSeg=%d hitTag=%d btn=%d",
|
||||
e.getX(), e.getY(), ox, oy, gridW, gridH, px, py, gx, gy, hitSeg, hitTag, button
|
||||
));
|
||||
|
||||
client.getInputProcessor().sendGraphicMouseAid(
|
||||
@@ -275,13 +287,34 @@ public class TerminalPanel extends JPanel {
|
||||
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) {
|
||||
clearSelection();
|
||||
sb.setCursorAddress(clickAddr);
|
||||
boolean result = client.getInputProcessor().lightPenSelect(clickAddr);
|
||||
boolean result = client.lightPenSelect(clickAddr);
|
||||
if (result) {
|
||||
refreshScreen();
|
||||
return;
|
||||
} else {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,6 +641,7 @@ public class TerminalPanel extends JPanel {
|
||||
bindKeyToMap(im, "FIELD_MARK", haus.nightmare.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
|
||||
bindKeyToMap(im, "ATTN", haus.nightmare.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
|
||||
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
||||
bindKeyToMap(im, "CURSEL", haus.nightmare.j3270.config.Settings.getKeyBinding("CURSEL", "alt Q"));
|
||||
|
||||
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
|
||||
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
@@ -638,6 +672,7 @@ public class TerminalPanel extends JPanel {
|
||||
am.put("j3270-FIELD_MARK", createAction(this::handleFieldMark));
|
||||
am.put("j3270-ATTN", createAction(this::handleAttn));
|
||||
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
|
||||
am.put("j3270-CURSEL", createAction(this::handleCursorSelect));
|
||||
|
||||
// Copy/Paste actions
|
||||
am.put("j3270-COPY", createAction(this::copySelection));
|
||||
@@ -824,6 +859,17 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCursorSelect() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
boolean selected = client.cursorSelect();
|
||||
if (selected) {
|
||||
refreshScreen();
|
||||
} else {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshScreen() {
|
||||
if (client != null) {
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
@@ -930,8 +976,8 @@ public class TerminalPanel extends JPanel {
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||
|
||||
int fontSize = (int) Math.round(ch * 0.95);
|
||||
if (fontSize < 10) fontSize = 10;
|
||||
int fontSize = Math.min((int) Math.round(ch * 0.82), (int) Math.round(cw * 1.45));
|
||||
if (fontSize < 9) fontSize = 9;
|
||||
Font f = (boldTerminalFont != null ? boldTerminalFont : terminalFont).deriveFont((float) fontSize);
|
||||
g2.setFont(f);
|
||||
FontMetrics fm = g2.getFontMetrics();
|
||||
@@ -1137,10 +1183,11 @@ public class TerminalPanel extends JPanel {
|
||||
Font f = bold ? boldTerminalFont : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
int textY = y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2);
|
||||
if (ch < 128) {
|
||||
g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent);
|
||||
g2.drawString(CHAR_STRINGS[ch], x, textY);
|
||||
} else {
|
||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
||||
g2.drawString(String.valueOf(ch), x, textY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1148,8 +1195,8 @@ public class TerminalPanel extends JPanel {
|
||||
// Draw underline
|
||||
if (underline) {
|
||||
g2.setColor(fgColor);
|
||||
g2.drawLine(x, y + cellHeight - fontDescent,
|
||||
x + cellWidth - 1, y + cellHeight - fontDescent);
|
||||
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);
|
||||
}
|
||||
|
||||
// Draw selection highlight
|
||||
|
||||
@@ -223,6 +223,10 @@ public class Telnet3270Client {
|
||||
public void sysReq() { inputProcessor.sysReq(); }
|
||||
/** Reset (unlock keyboard). */
|
||||
public void reset() { inputProcessor.reset(); }
|
||||
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
|
||||
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
|
||||
/** Trigger Light Pen selection at the specified screen address. */
|
||||
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||
return dsProcessor.getProgramSymbolManager();
|
||||
|
||||
@@ -41,7 +41,7 @@ public class DataStreamProcessor {
|
||||
|
||||
// Graphics & Programmed Symbols
|
||||
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 java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||
private int currentGocaSubtype = 0;
|
||||
@@ -832,9 +832,15 @@ public class DataStreamProcessor {
|
||||
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)
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Data (GOCA draw 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));
|
||||
@@ -847,6 +853,11 @@ public class DataStreamProcessor {
|
||||
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();
|
||||
|
||||
@@ -293,14 +293,36 @@ public class QueryReplyBuilder {
|
||||
out.write((Yr_3279_2 >> 16) & 0xFF);
|
||||
out.write((Yr_3279_2 >> 8) & 0xFF);
|
||||
out.write(Yr_3279_2 & 0xFF);
|
||||
out.write(SW_3279_2); // AW
|
||||
out.write(SH_3279_2); // AH
|
||||
int charW = getCharWidth();
|
||||
int charH = getCharHeight();
|
||||
out.write(charW); // AW
|
||||
out.write(charH); // AH
|
||||
int buf = maxCols * maxRows;
|
||||
out.write((buf >> 8) & 0xFF); // buffer size high
|
||||
out.write(buf & 0xFF); // buffer size low
|
||||
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) {
|
||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||
@@ -312,13 +334,16 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
private byte[] buildCharsets() {
|
||||
int charW = getCharWidth();
|
||||
int charH = getCharHeight();
|
||||
|
||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW (9)
|
||||
out.write(SH_3279_2); // SDH (12)
|
||||
out.write(charW); // SDW (9)
|
||||
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(0x00); // Load PS device type (high)
|
||||
out.write(0x00); // Load PS device type (low)
|
||||
@@ -343,8 +368,8 @@ public class QueryReplyBuilder {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
||||
out.write(0x82); // flags: GE, CGCSGID present
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW - default char width (9)
|
||||
out.write(SH_3279_2); // SDH - default char height (12)
|
||||
out.write(charW); // SDW - default char width (9)
|
||||
out.write(charH); // SDH - default char height (16 for 3179G, 12 for 3279-2)
|
||||
out.write(0x00); // LoadPS format (0x00)
|
||||
out.write(0x00);
|
||||
out.write(0x00);
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Point;
|
||||
|
||||
/**
|
||||
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
|
||||
* Matches IBM Host On-Demand (HODTransform.java, PS3179G.java, HODInput.java).
|
||||
*
|
||||
* <p>Coordinate System Architecture:
|
||||
* <ul>
|
||||
* <li>GOCA Presentation Space: Centered at (0, 0).
|
||||
* X ranges [-xMax .. +xMax] where width = cols * defaultCharWidth (defsx = 9).
|
||||
* Y ranges [-yMax .. +yMax] where height = rows * defaultCharHeight (defsy = 16 or 12).
|
||||
* (+Y is UP, -Y is DOWN, +X is RIGHT, -X is LEFT).</li>
|
||||
* <li>Base Presentation Space (px, py): Unsigned top-left origin (0, 0).
|
||||
* px = gx + xMax (ranges 0 .. totalWidth).
|
||||
* py = yMax - gy (ranges 0 .. totalHeight).</li>
|
||||
* <li>Display Screen / Window Space (sx, sy): Physical Swing pixel coordinates relative
|
||||
* to terminal character grid rendering offset (ox, oy).
|
||||
* sx = ox + (int) Math.round(px * transformX).
|
||||
* sy = oy + (int) Math.round(py * transformY).
|
||||
* where transformX = cellWidth / defaultCharWidth, transformY = cellHeight / defaultCharHeight.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Why simple fixed pixel scaling or hardcoded multipliers failed in past iterations:
|
||||
* <ul>
|
||||
* <li><b>Failure Mode 1</b>: Hardcoding totalHeight = rows * 12 caused coordinate truncation in 43-row mode (Model 4),
|
||||
* capping yMax at 257 instead of 343 and making the top menu / EXIT button unreachable.</li>
|
||||
* <li><b>Failure Mode 2</b>: Hardcoding totalHeight = rows * 16 without viewport alignment resulted in yMax = 343.
|
||||
* In ADMDRAW, GDDM defines the entire active drawing area between y = -169 and y = +200 (~370 units total height).
|
||||
* Mapping this into a 688-high canvas placed the top menu at Row 9.2 (middle of the screen) with a large blank void above,
|
||||
* and clicking the visual menu emitted gy = 224 (missing the [187..200] menu bounding box).</li>
|
||||
* <li><b>Failure Mode 3</b>: Intermediate raster buffering (e.g. fixed 800x600 canvas stretched to gridW x gridH)
|
||||
* introduced rounding artifacts in bidirectional coordinate conversion (unmapX/unmapY).</li>
|
||||
* <li><b>Failure Mode 4</b>: Overlapping dropdown menus (FILE, DRAW, TRANSFORM) lingering simultaneously when
|
||||
* segment retention/clearing was decoupled from GDDM's segment lifecycle.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class GddmCoordinateTransform {
|
||||
|
||||
public static final int DEFAULT_CHAR_WIDTH = 9; // defsx: 3179G standard character cell width
|
||||
public static final int DEFAULT_CHAR_HEIGHT_16 = 16; // defsy: 3179G standard character cell height
|
||||
public static final int DEFAULT_CHAR_HEIGHT_12 = 12; // defsy: 3279-2 alternate character cell height
|
||||
|
||||
private int defaultCharWidth = DEFAULT_CHAR_WIDTH;
|
||||
private int defaultCharHeight = DEFAULT_CHAR_HEIGHT_16;
|
||||
|
||||
private int screenCols = 80;
|
||||
private int screenRows = 24;
|
||||
|
||||
private double transformX = 1.0;
|
||||
private double transformY = 1.0;
|
||||
|
||||
public GddmCoordinateTransform() {
|
||||
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
||||
}
|
||||
|
||||
public GddmCoordinateTransform(int screenCols, int screenRows) {
|
||||
this(screenCols, screenRows, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
||||
}
|
||||
|
||||
public GddmCoordinateTransform(int screenCols, int screenRows, int defCharWidth, int defCharHeight) {
|
||||
this.screenCols = screenCols > 0 ? screenCols : 80;
|
||||
this.screenRows = screenRows > 0 ? screenRows : 24;
|
||||
this.defaultCharWidth = defCharWidth > 0 ? defCharWidth : DEFAULT_CHAR_WIDTH;
|
||||
this.defaultCharHeight = defCharHeight > 0 ? defCharHeight : DEFAULT_CHAR_HEIGHT_16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates screen grid and cell dimensions from UI.
|
||||
*
|
||||
* @param cellWidth Width of a character cell in Swing pixels
|
||||
* @param cellHeight Height of a character cell in Swing pixels
|
||||
*/
|
||||
public void updateDisplayMetrics(int cellWidth, int cellHeight) {
|
||||
if (cellWidth > 0 && defaultCharWidth > 0) {
|
||||
this.transformX = (double) cellWidth / (double) defaultCharWidth;
|
||||
}
|
||||
if (cellHeight > 0 && defaultCharHeight > 0) {
|
||||
this.transformY = (double) cellHeight / (double) defaultCharHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public void setScreenDimensions(int cols, int rows) {
|
||||
if (cols > 0) this.screenCols = cols;
|
||||
if (rows > 0) this.screenRows = rows;
|
||||
}
|
||||
|
||||
public void setDefaultCharMetrics(int defWidth, int defHeight) {
|
||||
if (defWidth > 0) this.defaultCharWidth = defWidth;
|
||||
if (defHeight > 0) this.defaultCharHeight = defHeight;
|
||||
}
|
||||
|
||||
public int getTotalWidth() {
|
||||
return screenCols * defaultCharWidth;
|
||||
}
|
||||
|
||||
public int getTotalHeight() {
|
||||
return screenRows * defaultCharHeight;
|
||||
}
|
||||
|
||||
public int getXMax() {
|
||||
int totalW = getTotalWidth();
|
||||
return (totalW - 1) / 2 + (totalW - 1) % 2;
|
||||
}
|
||||
|
||||
public int getYMax() {
|
||||
int totalH = getTotalHeight();
|
||||
return (totalH - 1) / 2;
|
||||
}
|
||||
|
||||
public double getTransformX() {
|
||||
return transformX;
|
||||
}
|
||||
|
||||
public double getTransformY() {
|
||||
return transformY;
|
||||
}
|
||||
|
||||
public int getDefaultCharWidth() {
|
||||
return defaultCharWidth;
|
||||
}
|
||||
|
||||
public int getDefaultCharHeight() {
|
||||
return defaultCharHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GOCA signed coordinate (gx, gy) to base presentation space coordinate (px, py).
|
||||
*/
|
||||
public Point gocaToBase(int gx, int gy) {
|
||||
int px = gx + getXMax();
|
||||
int py = getYMax() - gy;
|
||||
return new Point(px, py);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a base presentation space coordinate (px, py) to GOCA signed coordinate (gx, gy).
|
||||
*/
|
||||
public Point baseToGoca(int px, int py) {
|
||||
int gx = px - getXMax();
|
||||
int gy = getYMax() - py;
|
||||
return new Point(gx, gy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GOCA signed coordinate (gx, gy) directly to Swing screen pixel coordinate (sx, sy).
|
||||
*/
|
||||
public Point gocaToScreenPixel(int gx, int gy, int ox, int oy, int cellWidth, int cellHeight) {
|
||||
updateDisplayMetrics(cellWidth, cellHeight);
|
||||
int px = gx + getXMax();
|
||||
int py = getYMax() - gy;
|
||||
int sx = ox + (int) Math.round(px * transformX);
|
||||
int sy = oy + (int) Math.round(py * transformY);
|
||||
return new Point(sx, sy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a mouse click in Swing screen coordinates (mouseX, mouseY) directly to GOCA signed coordinate (gx, gy).
|
||||
* Matches HODInput.getHODInput:
|
||||
* x = mouseX / transformX
|
||||
* y = mouseY / transformY
|
||||
* gx = x - xMax
|
||||
* gy = yMax - y
|
||||
*/
|
||||
public Point screenPixelToGoca(int mouseX, int mouseY, int ox, int oy, int cellWidth, int cellHeight) {
|
||||
updateDisplayMetrics(cellWidth, cellHeight);
|
||||
double relX = mouseX - ox;
|
||||
double relY = mouseY - oy;
|
||||
|
||||
int px = (transformX > 0) ? (int) Math.round(relX / transformX) : (int) relX;
|
||||
int py = (transformY > 0) ? (int) Math.round(relY / transformY) : (int) relY;
|
||||
|
||||
int gx = px - getXMax();
|
||||
int gy = getYMax() - py;
|
||||
return new Point(gx, gy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps GOCA (gx, gy) to fixed canvas buffer dimensions (canvasWidth, canvasHeight).
|
||||
*/
|
||||
public Point gocaToCanvasPixel(int gx, int gy, int canvasWidth, int canvasHeight) {
|
||||
int totalW = getTotalWidth();
|
||||
int totalH = getTotalHeight();
|
||||
int xMax = getXMax();
|
||||
int yMax = getYMax();
|
||||
|
||||
int px = (int) Math.round((double) (gx + xMax) * canvasWidth / (totalW > 0 ? totalW : 1));
|
||||
int py = (int) Math.round((double) (yMax - gy) * canvasHeight / (totalH > 0 ? totalH : 1));
|
||||
return new Point(px, py);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps fixed canvas buffer pixel (canvasX, canvasY) to GOCA (gx, gy).
|
||||
*/
|
||||
public Point canvasPixelToGoca(int canvasX, int canvasY, int canvasWidth, int canvasHeight) {
|
||||
int totalW = getTotalWidth();
|
||||
int totalH = getTotalHeight();
|
||||
int xMax = getXMax();
|
||||
int yMax = getYMax();
|
||||
|
||||
int nx = (int) Math.round((double) canvasX * totalW / (canvasWidth > 0 ? canvasWidth : 1));
|
||||
int ny = (int) Math.round((double) canvasY * totalH / (canvasHeight > 0 ? canvasHeight : 1));
|
||||
return new Point(nx - xMax, yMax - ny);
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ public class GocaDecoder {
|
||||
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
|
||||
SegmentBounds sb = activeSegmentsInOrder.get(i);
|
||||
if (sb.contains(gx, gy, 25)) {
|
||||
System.err.println(String.format(
|
||||
logger.info(String.format(
|
||||
"findPickedSegment: goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
||||
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
||||
));
|
||||
@@ -173,14 +173,14 @@ public class GocaDecoder {
|
||||
}
|
||||
for (SegmentBounds sb : segmentBoundsMap.values()) {
|
||||
if (sb.contains(gx, gy, 25)) {
|
||||
System.err.println(String.format(
|
||||
logger.info(String.format(
|
||||
"findPickedSegment (fallback): goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
||||
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
||||
));
|
||||
return sb.segId;
|
||||
}
|
||||
}
|
||||
System.err.println(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
|
||||
logger.info(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@ public class GocaDecoder {
|
||||
if (currentSegId != 0) {
|
||||
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
|
||||
if (sb != null) {
|
||||
System.err.println(String.format(
|
||||
logger.info(String.format(
|
||||
"GocaDecoder: ENDSEGM segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
||||
sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
||||
));
|
||||
@@ -805,12 +805,17 @@ public class GocaDecoder {
|
||||
idx += 2;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
|
||||
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space (HODEraseGraphicsPlane)
|
||||
plane.clear();
|
||||
resetAttributes();
|
||||
curX = 0;
|
||||
curY = 0;
|
||||
activeSegmentsInOrder.clear();
|
||||
segmentBoundsMap.clear();
|
||||
segmentStore.clear();
|
||||
segmentOrderList.clear();
|
||||
chainedTargets.clear();
|
||||
logger.info("GOCA P_ERASE: erased graphics presentation space and cleared segment stores");
|
||||
idx += 2;
|
||||
break;
|
||||
}
|
||||
@@ -818,55 +823,15 @@ public class GocaDecoder {
|
||||
idx += 12;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.P_SCUDEF: { // 0x21: Drawing Process Control / Segment Execute
|
||||
if (idx + 5 < end) {
|
||||
int pLen = data[idx + 1] & 0xFF;
|
||||
if (pLen >= 6) {
|
||||
int flags0 = data[idx + 2] & 0xFF;
|
||||
int flags1 = data[idx + 3] & 0xFF;
|
||||
int startSeg = ((data[idx + 4] & 0xFF) << 8) | (data[idx + 5] & 0xFF);
|
||||
int endSeg = (idx + 7 < end) ? (((data[idx + 6] & 0xFF) << 8) | (data[idx + 7] & 0xFF)) : startSeg;
|
||||
logger.info(String.format("GOCA P_SCUDEF: flags0=0x%02x flags1=0x%02x startSeg=%d endSeg=%d",
|
||||
flags0, flags1, startSeg, endSeg));
|
||||
// Redraw all stored base segments not in dynamic range [startSeg..endSeg]
|
||||
for (int segId : segmentOrderList) {
|
||||
if (segId < startSeg || segId > endSeg) {
|
||||
if (!chainedTargets.contains(segId)) {
|
||||
byte[] segBytes = segmentStore.get(segId);
|
||||
if (segBytes != null) {
|
||||
int savedSegId = currentSegId;
|
||||
currentSegId = segId;
|
||||
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
|
||||
activeSegmentsInOrder.remove(sb);
|
||||
activeSegmentsInOrder.add(sb);
|
||||
callDepth++;
|
||||
decodeStreamDirect(segBytes, 0, segBytes.length);
|
||||
callDepth--;
|
||||
currentSegId = savedSegId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int s = startSeg; s <= endSeg; s++) {
|
||||
byte[] segBytes = segmentStore.get(s);
|
||||
if (segBytes != null) {
|
||||
int savedSegId = currentSegId;
|
||||
currentSegId = s;
|
||||
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(s, SegmentBounds::new);
|
||||
activeSegmentsInOrder.remove(sb);
|
||||
activeSegmentsInOrder.add(sb);
|
||||
callDepth++;
|
||||
decodeStreamDirect(segBytes, 0, segBytes.length);
|
||||
callDepth--;
|
||||
currentSegId = savedSegId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults (HODCurrentDefaults)
|
||||
// Note: Per IBM 3179G / HOD architecture, 0x21 sets default drawing attributes (color, line, pattern).
|
||||
// It is NOT an executive segment redraw order. A prior attempt treated 0x21 as an invented
|
||||
// P_SCUDEF segment redraw loop, which caused old dropdown menus and segments to be repeatedly
|
||||
// repainted on top of the screen, creating ghost artifacts and stale bounding boxes.
|
||||
if (idx + 1 < end) {
|
||||
int len = (data[idx + 1] & 0xFF) + 2;
|
||||
idx += len;
|
||||
int pLen = data[idx + 1] & 0xFF;
|
||||
logger.fine(String.format("GOCA Set Current Defaults (0x21): len=%d", pLen));
|
||||
idx += pLen + 2;
|
||||
} else {
|
||||
idx++;
|
||||
}
|
||||
@@ -1302,9 +1267,9 @@ public class GocaDecoder {
|
||||
int textLen = end - pos;
|
||||
if (textLen <= 0) return;
|
||||
|
||||
// IBM 3279 vector graphics base cell is 9x12
|
||||
// IBM 3179G vector graphics base cell is 9x16
|
||||
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
|
||||
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 12.0)) : 14.0;
|
||||
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
|
||||
|
||||
int totalW = textLen * (charWidth > 0 ? charWidth : 9);
|
||||
int totalH = (charHeight > 0 ? charHeight : 14);
|
||||
|
||||
@@ -6,13 +6,18 @@ package haus.nightmare.lib3270j.graphics;
|
||||
*/
|
||||
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[] {
|
||||
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,
|
||||
0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
|
||||
0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00
|
||||
};
|
||||
@@ -30,16 +35,17 @@ public class GraphicInputBuilder {
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
||||
return buildGraphicInput(gocaX, gocaY, buttonOrAidCode, isMouseAction, isShift, isCtrl, 0, 0);
|
||||
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
|
||||
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag (legacy overload).
|
||||
* Note: Per IBM HOD / 3179G architecture, pick correlation is evaluated host-side by GDDM using (gx, gy).
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
||||
int pickedSegId, int pickTag) {
|
||||
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl, pickedSegId, pickTag);
|
||||
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,20 +54,22 @@ public class GraphicInputBuilder {
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
||||
int pickedSegId, int pickTag) {
|
||||
return buildGraphicInput(gocaX, gocaY, row, col, buttonOrAidCode, isMouseAction, isShift, isCtrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field matching IBM Host On-Demand (HODInput.java).
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
||||
byte[] sf = new byte[MASK.length];
|
||||
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
||||
|
||||
// Byte 0-1: Structured Field Length (56 bytes)
|
||||
sf[0] = (byte) ((MASK.length >> 8) & 0xFF);
|
||||
sf[1] = (byte) (MASK.length & 0xFF);
|
||||
// Bytes 0-1: Structured Field Length (0x0034 = 52 decimal per IBM HODInput.java / GOCA architecture).
|
||||
// Preserved from MASK; do not overwrite with MASK.length (56).
|
||||
|
||||
// Bytes 16-19: Cursor Row & Column
|
||||
if (row > 0 || col > 0) {
|
||||
sf[16] = (byte) (row & 0xFF);
|
||||
sf[17] = (byte) (col & 0xFF);
|
||||
sf[18] = (byte) (row & 0xFF);
|
||||
sf[19] = (byte) (col & 0xFF);
|
||||
}
|
||||
// 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.
|
||||
|
||||
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
|
||||
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
|
||||
@@ -72,15 +80,15 @@ public class GraphicInputBuilder {
|
||||
sf[27] = (byte) (gocaY & 0xFF);
|
||||
|
||||
if (isMouseAction) {
|
||||
// Byte 28-31: Picked Segment ID (4 bytes big-endian)
|
||||
sf[28] = (byte) ((pickedSegId >> 24) & 0xFF);
|
||||
sf[29] = (byte) ((pickedSegId >> 16) & 0xFF);
|
||||
sf[30] = (byte) ((pickedSegId >> 8) & 0xFF);
|
||||
sf[31] = (byte) (pickedSegId & 0xFF);
|
||||
// Byte 28-31: Fixed mouse trigger class constant (0x00000004 per IBM HODInput.java)
|
||||
sf[28] = 0x00;
|
||||
sf[29] = 0x00;
|
||||
sf[30] = 0x00;
|
||||
sf[31] = 0x04;
|
||||
|
||||
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
|
||||
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
|
||||
sf[33] = (byte) (pickTag & 0xFF);
|
||||
// Byte 32-33: Fixed mouse correlation class constant (0x0004 per IBM HODInput.java)
|
||||
sf[32] = 0x00;
|
||||
sf[33] = 0x04;
|
||||
|
||||
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
|
||||
sf[35] = (byte) (buttonOrAidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
|
||||
|
||||
@@ -36,14 +36,14 @@ public class GraphicsPlane {
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
||||
};
|
||||
|
||||
private int canvasWidth = 800;
|
||||
private int canvasHeight = 600;
|
||||
// 3179G Presentation Space metrics: 9x16 cell pitch (720x384 for Model 2, 720x688 for Model 4)
|
||||
private int screenCols = 80;
|
||||
private int screenRows = 24;
|
||||
private int canvasWidth = 720;
|
||||
private int canvasHeight = 384;
|
||||
private int[] rgbBuffer;
|
||||
private boolean hasContent = false;
|
||||
private long updateCount = 0;
|
||||
|
||||
private int screenCols = 80;
|
||||
private int screenRows = 24;
|
||||
private ProgramSymbolManager programSymbolManager;
|
||||
|
||||
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||
@@ -115,6 +115,7 @@ public class GraphicsPlane {
|
||||
public synchronized void setScreenDimensions(int cols, int rows) {
|
||||
this.screenCols = cols > 0 ? cols : 80;
|
||||
this.screenRows = rows > 0 ? rows : 24;
|
||||
this.transform.setScreenDimensions(this.screenCols, this.screenRows);
|
||||
}
|
||||
|
||||
public int getScreenCols() {
|
||||
@@ -125,14 +126,53 @@ public class GraphicsPlane {
|
||||
return screenRows;
|
||||
}
|
||||
|
||||
// Dedicated GDDM coordinate transformation scaling layer (matching IBM HODTransform)
|
||||
private final GddmCoordinateTransform transform = new GddmCoordinateTransform();
|
||||
|
||||
public GddmCoordinateTransform getTransform() {
|
||||
return transform;
|
||||
}
|
||||
|
||||
public int getTotalWidth() {
|
||||
int cols = screenCols > 0 ? screenCols : 80;
|
||||
return cols * 9;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total vertical presentation space units.
|
||||
*
|
||||
* <p>Historical / Architecture Note for Future Iterations:
|
||||
* <ul>
|
||||
* <li><b>Failed Approach 1 (Hardcoded 12-pitch / rows * 12)</b>:
|
||||
* Used 516 units (yMax = 257) on 43-row screen. Caused upper coordinate truncation;
|
||||
* menus at gy > 257 were clipped and mathematically impossible to pick.</li>
|
||||
* <li><b>Failed Approach 2 (Hardcoded 16-pitch / rows * 16 without viewport alignment)</b>:
|
||||
* Used 688 units (yMax = 343). Visual rendering was shifted downward by ~50 pixels relative
|
||||
* to GDDM's internal picture space viewport, causing clicks at Row 7 to emit gy = 226
|
||||
* which missed the GDDM menu hit box and caused terminal alarm beeps.</li>
|
||||
* <li><b>Failed Approach 3 (Overwriting Graphic Input SF bytes 16-19 with row/col)</b>:
|
||||
* Overwrote device correlation descriptor 0x00230023, causing GDDM Structured Field
|
||||
* parser to reject inbound correlation packets as malformed.</li>
|
||||
* <li><b>Failed Approach 4 (Misinterpreting Procedure Order 0x21 as Segment Redraw loop)</b>:
|
||||
* Order 0x21 is HODCurrentDefaults (Set Current Defaults), not an executive redraw loop.
|
||||
* Treating 0x21 as segment execution caused previous menu frames and stored segments
|
||||
* to be repeatedly repainted over active screens, leaving behind visual ghost artifacts.</li>
|
||||
* <li><b>Failed Approach 5 (Static yMax=343 scaling vs GDDM ADMDRAW Viewport bounds)</b>:
|
||||
* In ADMDRAW, GDDM defines the graphics presentation space between y = -169 (canvas bottom)
|
||||
* and y = +200 (top menu bar), with total height ~370 units. Mapping this into a static
|
||||
* yMax = 343 (688 total height) renders the top menu at Row 9.2 (middle of the screen) with
|
||||
* a large vertical void above it, and clicking the visual menu sends gy = 224 which misses
|
||||
* the [187..200] menu bounding box. Furthermore, multiple dropdown menus (FILE, DRAW, TRANSFORM)
|
||||
* overlapped simultaneously because segment retention and clearing was decoupled from GDDM's
|
||||
* actual viewport state.</li>
|
||||
* <li><b>Solution Path</b>: Use {@link GddmCoordinateTransform} for dynamic display and aspect
|
||||
* scaling, ensuring host Query Reply metrics (SDH/AH) and GDDM Picture Space Window
|
||||
* orders are synchronized with client-side mouse transformation.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public int getTotalHeight() {
|
||||
int rows = screenRows > 0 ? screenRows : 24;
|
||||
return rows * 12;
|
||||
return rows * 16;
|
||||
}
|
||||
|
||||
public int getXMax() {
|
||||
@@ -158,7 +198,7 @@ public class GraphicsPlane {
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
|
||||
* IBM 3179G / HOD presentation space coordinate range is [-yMax .. +yMax] (height = rows * 12).
|
||||
* IBM 3179G / HOD presentation space coordinate range is [-yMax .. +yMax] (height = rows * 16).
|
||||
*/
|
||||
public double mapYDouble(double gocaY) {
|
||||
int totalH = getTotalHeight();
|
||||
@@ -555,8 +595,19 @@ public class GraphicsPlane {
|
||||
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int bg = bgColorArgb;
|
||||
|
||||
boolean isTransparentBlack = (fill == GocaConstants.GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != GocaConstants.MIX_OVER);
|
||||
if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary) && !isTransparentBlack) {
|
||||
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
|
||||
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
|
||||
// Background Mix (GSBMX / bgMix):
|
||||
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
|
||||
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
|
||||
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
|
||||
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
|
||||
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
|
||||
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
|
||||
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
||||
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
|
||||
|
||||
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
||||
// Find polygon vertical bounds across all points
|
||||
int minY = py[0];
|
||||
int maxY = py[0];
|
||||
|
||||
@@ -244,7 +244,43 @@ public class InputProcessor {
|
||||
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();
|
||||
|
||||
out.write(aidCode);
|
||||
@@ -298,7 +334,7 @@ public class InputProcessor {
|
||||
sendAidResponse(out.toByteArray());
|
||||
}
|
||||
|
||||
private void sendAidResponse(byte[] data) {
|
||||
protected void sendAidResponse(byte[] data) {
|
||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
||||
fsm.sendSscpLuData(data);
|
||||
} else if (fsm != null) {
|
||||
@@ -307,10 +343,10 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G.
|
||||
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
|
||||
*/
|
||||
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||
if (fsm == null || !fsm.getConnectionState().isFullSession()) {
|
||||
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
|
||||
return;
|
||||
}
|
||||
if (isKeyboardLocked()) {
|
||||
@@ -322,8 +358,6 @@ public class InputProcessor {
|
||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
||||
int gx = gocaDecoder.getGraphicCursorX();
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
|
||||
int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
|
||||
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
||||
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
||||
int row = cursorAddr / cols;
|
||||
@@ -340,16 +374,53 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
gx, gy, row, col, button, true, isShift, isCtrl, pickedSeg, pickTag
|
||||
gx, gy, row, col, button, true, isShift, isCtrl
|
||||
);
|
||||
System.err.println(String.format(
|
||||
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
|
||||
gx, gy, row, col, pickedSeg, pickTag, button, isShift, isCtrl
|
||||
|
||||
StringBuilder sfHex = new StringBuilder();
|
||||
for (byte b : sf) {
|
||||
sfHex.append(String.format("%02X ", b & 0xFF));
|
||||
}
|
||||
log.info(String.format(
|
||||
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
|
||||
gx, gy, row, col, button, isShift, isCtrl, sfHex.toString().trim()
|
||||
));
|
||||
|
||||
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
||||
out.write(AID_SF);
|
||||
try {
|
||||
out.write(sf);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
|
||||
// Trailing AID + cursor address + modified fields matching HOD DS3270.sendMouseAid
|
||||
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()) {
|
||||
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;
|
||||
out.write(ORDER_SBA);
|
||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||
out.write(addr[0] & 0xFF);
|
||||
out.write(addr[1] & 0xFF);
|
||||
|
||||
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 {
|
||||
out.write(aidCode);
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
@@ -363,11 +434,11 @@ public class InputProcessor {
|
||||
// ========== Cursor movement ==========
|
||||
|
||||
/**
|
||||
* Simulate a Text Light Pen selection.
|
||||
* Simulate a Text Light Pen selection at the specified screen address.
|
||||
* Conforms to IBM 3270 / Host On-Demand PS3270 processCurSelKey specifications.
|
||||
*/
|
||||
public boolean lightPenSelect(int address) {
|
||||
if (screen == null || !screen.isFormatted()) {
|
||||
System.out.println("LP: screen null or unformatted");
|
||||
return false;
|
||||
}
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
@@ -375,15 +446,13 @@ public class InputProcessor {
|
||||
address = ((address % size) + size) % size;
|
||||
int faPos = screen.findFieldAttribute(address);
|
||||
if (faPos < 0) {
|
||||
System.out.println("LP: no FA found for addr=" + address);
|
||||
return false;
|
||||
}
|
||||
|
||||
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||
int fa = faCell.fa & 0xFF;
|
||||
System.out.println("LP: addr=" + address + " faPos=" + faPos + " fa=0x" + String.format("%02X", fa)
|
||||
+ " selectable=" + haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa));
|
||||
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
|
||||
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) ||
|
||||
haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -392,33 +461,52 @@ public class InputProcessor {
|
||||
int ebcdic = desCell.ec & 0xFF;
|
||||
char ascii = (char) desCell.ucs4;
|
||||
|
||||
screen.setCursorAddress(designatorPos);
|
||||
// Set cursor to the clicked/selected position matching HOD / 3270 standards
|
||||
screen.setCursorAddress(address);
|
||||
|
||||
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || (ebcdic != 0x50 && ascii != '&' && ebcdic != 0x6F && ascii != '?' && ebcdic != 0x6E && ascii != '>')) {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||
return true;
|
||||
} else if (ebcdic == 0x50 || ascii == '&') {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
return true;
|
||||
} else if (ebcdic == 0x6F || ascii == '?') {
|
||||
desCell.ec = (byte) 0x6E;
|
||||
desCell.ucs4 = '>';
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
} else if (ebcdic == 0x6E || ascii == '>') {
|
||||
desCell.ec = (byte) 0x6F;
|
||||
desCell.ucs4 = '?';
|
||||
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
} else {
|
||||
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
|
||||
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||
return true;
|
||||
}
|
||||
// Enter Immediate (&) -> AID_ENTER (0x7D)
|
||||
else if (ebcdic == 0x50 || ascii == '&' || ascii == '\uFF06') {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
return true;
|
||||
}
|
||||
// Deferred Selection (? -> >)
|
||||
else if (ebcdic == 0x6F || ascii == '?' || ascii == '\uFF1F') {
|
||||
desCell.ec = (byte) 0x6E;
|
||||
desCell.ucs4 = '>';
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
}
|
||||
// Deferred Deselection (> -> ?)
|
||||
else if (ebcdic == 0x6E || ascii == '>' || ascii == '\uFF1E') {
|
||||
desCell.ec = (byte) 0x6F;
|
||||
desCell.ucs4 = '?';
|
||||
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
}
|
||||
// Any other character is an invalid designator and cannot be selected
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate the 3270 CURSR SEL (Cursor Select) key at the current cursor position.
|
||||
* Equivalent to processCurSelKey in IBM Host On-Demand PS3270.
|
||||
*/
|
||||
public boolean cursorSelect() {
|
||||
if (screen == null) return false;
|
||||
return lightPenSelect(screen.getCursorAddress());
|
||||
}
|
||||
|
||||
public void cursorUp() {
|
||||
|
||||
+5
-1
@@ -132,11 +132,15 @@ public class QueryReplyBuilderTest {
|
||||
assertEquals(0x81, replies[3] & 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 len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||
assertEquals(23, len2);
|
||||
assertEquals(0x81, replies[pos2 + 2] & 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
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class FTDftTest {
|
||||
|
||||
private EbcdicTranslator translator;
|
||||
private ScreenBuffer screen;
|
||||
private InputProcessor input;
|
||||
private TestListener listener;
|
||||
private FTDft dft;
|
||||
private FTConfig config;
|
||||
|
||||
private static class TestListener implements FTDft.FTDftListener {
|
||||
boolean dftRunningCalled = false;
|
||||
boolean completeCalled = false;
|
||||
boolean abortCalled = false;
|
||||
String completeMsg = null;
|
||||
String abortMsg = null;
|
||||
long bytesTransferred = 0;
|
||||
FTConstants.FTState state = FTConstants.FTState.RUNNING;
|
||||
FTConfig config;
|
||||
|
||||
@Override
|
||||
public void onDftRunning() {
|
||||
dftRunningCalled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferComplete(String errorMessage) {
|
||||
completeCalled = true;
|
||||
completeMsg = errorMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferAborted(String errorMessage) {
|
||||
abortCalled = true;
|
||||
abortMsg = errorMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBytesTransferred(long bytes) {
|
||||
bytesTransferred = bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FTConstants.FTState getCurrentState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setState(FTConstants.FTState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FTConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getLocalFile() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
listener = new TestListener();
|
||||
config = new FTConfig();
|
||||
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||
config.setTransferMode(FTConfig.TransferMode.BINARY);
|
||||
listener.config = config;
|
||||
|
||||
dft = new FTDft(input, translator, listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenMessageStreamAsciiAndHandleTrans14Error() {
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
dft.initTransfer(null, outStream);
|
||||
|
||||
// 1. Simulate TR_OPEN_REQ with ASCII "FT:MSG" matching j3270.log:
|
||||
// SF length = 0x0023, SF type = 0xD0, Request code = 0x0012, stream name "FT:MSG " at offset 28
|
||||
byte[] openReq = new byte[] {
|
||||
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
|
||||
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
|
||||
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
|
||||
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // "FT:MSG "
|
||||
};
|
||||
|
||||
dft.processStructuredField(openReq, 0, openReq.length);
|
||||
|
||||
// Should NOT trigger onDftRunning because it's a message stream
|
||||
assertFalse(listener.dftRunningCalled);
|
||||
|
||||
// 2. Simulate TR_DATA_INSERT on the message stream with "TRANS14 Error reading file from host: ..."
|
||||
String errorMsg = "TRANS14 Error reading file from host: file format invalid";
|
||||
byte[] msgBytes = errorMsg.getBytes(StandardCharsets.ISO_8859_1);
|
||||
int totalPayloadLen = msgBytes.length + 5;
|
||||
|
||||
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
|
||||
sfOut.write(0); sfOut.write(0); // placeholder length
|
||||
sfOut.write(0xD0); // SF_TRANSFER_DATA
|
||||
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
|
||||
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
|
||||
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
|
||||
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
|
||||
sfOut.write(FTConstants.TR_BEGIN_DATA);
|
||||
sfOut.write((totalPayloadLen >> 8) & 0xFF);
|
||||
sfOut.write(totalPayloadLen & 0xFF);
|
||||
sfOut.write(msgBytes, 0, msgBytes.length);
|
||||
|
||||
byte[] insertData = sfOut.toByteArray();
|
||||
int sfLen = insertData.length;
|
||||
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
|
||||
insertData[1] = (byte) (sfLen & 0xFF);
|
||||
|
||||
dft.processStructuredField(insertData, 0, insertData.length);
|
||||
|
||||
// Verify that onTransferAborted was called with the error message
|
||||
assertTrue(listener.abortCalled, "onTransferAborted should be called for TRANS14 error");
|
||||
assertNotNull(listener.abortMsg);
|
||||
assertTrue(listener.abortMsg.contains("TRANS14"));
|
||||
assertFalse(listener.completeCalled, "onTransferComplete should NOT be called for an error");
|
||||
|
||||
// Verify that the error message was NOT written to the destination file stream!
|
||||
assertEquals(0, outStream.size(), "Error message should not be written to destination file");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenDataStreamAndWriteData() {
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
dft.initTransfer(null, outStream);
|
||||
|
||||
// 1. Simulate TR_OPEN_REQ with ASCII "FT:DATA"
|
||||
byte[] openReq = new byte[] {
|
||||
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
|
||||
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
|
||||
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
|
||||
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // "FT:DATA"
|
||||
};
|
||||
|
||||
dft.processStructuredField(openReq, 0, openReq.length);
|
||||
assertTrue(listener.dftRunningCalled, "onDftRunning should be called for file data stream");
|
||||
|
||||
// 2. Simulate TR_DATA_INSERT with binary file payload
|
||||
byte[] fileData = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34 }; // "%PDF-1.4"
|
||||
int totalPayloadLen = fileData.length + 5;
|
||||
|
||||
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
|
||||
sfOut.write(0); sfOut.write(0);
|
||||
sfOut.write(0xD0);
|
||||
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
|
||||
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
|
||||
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
|
||||
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
|
||||
sfOut.write(FTConstants.TR_BEGIN_DATA);
|
||||
sfOut.write((totalPayloadLen >> 8) & 0xFF);
|
||||
sfOut.write(totalPayloadLen & 0xFF);
|
||||
sfOut.write(fileData, 0, fileData.length);
|
||||
|
||||
byte[] insertData = sfOut.toByteArray();
|
||||
int sfLen = insertData.length;
|
||||
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
|
||||
insertData[1] = (byte) (sfLen & 0xFF);
|
||||
|
||||
dft.processStructuredField(insertData, 0, insertData.length);
|
||||
|
||||
// Data must be written to file stream
|
||||
assertArrayEquals(fileData, outStream.toByteArray());
|
||||
assertEquals(8, listener.bytesTransferred);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.awt.Point;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class GddmCoordinateTransformTest {
|
||||
|
||||
@Test
|
||||
public void testModel4PresentationSpaceMetrics() {
|
||||
GddmCoordinateTransform transform = new GddmCoordinateTransform(80, 43, 9, 16);
|
||||
|
||||
assertEquals(720, transform.getTotalWidth());
|
||||
assertEquals(688, transform.getTotalHeight());
|
||||
assertEquals(360, transform.getXMax());
|
||||
assertEquals(343, transform.getYMax());
|
||||
|
||||
// Screen center (0, 0)
|
||||
Point baseCenter = transform.gocaToBase(0, 0);
|
||||
assertEquals(360, baseCenter.x);
|
||||
assertEquals(343, baseCenter.y);
|
||||
assertEquals(new Point(0, 0), transform.baseToGoca(360, 343));
|
||||
|
||||
// Bidirectional screen pixel transformation
|
||||
// Cell width = 10, Cell height = 20, Offset = (100, 50)
|
||||
int ox = 100, oy = 50, cw = 10, ch = 20;
|
||||
Point screenPt = transform.gocaToScreenPixel(0, 0, ox, oy, cw, ch);
|
||||
Point roundtripGoca = transform.screenPixelToGoca(screenPt.x, screenPt.y, ox, oy, cw, ch);
|
||||
assertEquals(0, roundtripGoca.x);
|
||||
assertEquals(0, roundtripGoca.y);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModel2PresentationSpaceMetrics() {
|
||||
GddmCoordinateTransform transform = new GddmCoordinateTransform(80, 24, 9, 16);
|
||||
|
||||
assertEquals(720, transform.getTotalWidth());
|
||||
assertEquals(384, transform.getTotalHeight());
|
||||
assertEquals(360, transform.getXMax());
|
||||
assertEquals(191, transform.getYMax());
|
||||
}
|
||||
}
|
||||
@@ -221,6 +221,12 @@ public class GocaDecoderTest {
|
||||
GraphicsPlane plane = new GraphicsPlane(1000, 750);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
|
||||
// Standard IBM 3179G 43-row presentation space (80*9 = 720 wide, 43*16 = 688 high)
|
||||
assertEquals(720, plane.getTotalWidth());
|
||||
assertEquals(688, plane.getTotalHeight());
|
||||
assertEquals(360, plane.getXMax());
|
||||
assertEquals(343, plane.getYMax());
|
||||
|
||||
// Screen center (0, 0) should map to canvas center
|
||||
assertEquals(500, plane.mapX(0));
|
||||
assertEquals(374, plane.mapY(0));
|
||||
@@ -240,6 +246,13 @@ public class GocaDecoderTest {
|
||||
assertEquals(0, plane.unmapY(plane.mapY(0)));
|
||||
assertEquals(100, plane.unmapY(plane.mapY(100)));
|
||||
assertEquals(-200, plane.unmapY(plane.mapY(-200)));
|
||||
|
||||
// Standard IBM 3179G Model 2 (24-row) presentation space (24*16 = 384 high, yMax = 191)
|
||||
plane.setScreenDimensions(80, 24);
|
||||
assertEquals(720, plane.getTotalWidth());
|
||||
assertEquals(384, plane.getTotalHeight());
|
||||
assertEquals(360, plane.getXMax());
|
||||
assertEquals(191, plane.getYMax());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -735,8 +748,9 @@ public class GocaDecoderTest {
|
||||
out.write(0x00); out.write(20); out.write(0x00); out.write(40);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
// 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, GSCOL White inside GBAR)
|
||||
// 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, Pattern 15/Empty, GSCOL White inside GBAR)
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x08); // Black (background)
|
||||
out.write(GocaConstants.G_GSPT); out.write(0x0F); // Empty pattern (transparent interior)
|
||||
out.write(GocaConstants.G_GBAR); out.write(0x80);
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x07); // White line color
|
||||
out.write(GocaConstants.G_GSLT); out.write(GocaConstants.LT_DOT); // Dotted line
|
||||
|
||||
@@ -138,7 +138,7 @@ public class InputProcessorTest {
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
|
||||
assertEquals(56, sf.length);
|
||||
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[3]); // SF ID = 0x0F
|
||||
|
||||
@@ -154,38 +154,170 @@ public class InputProcessorTest {
|
||||
assertEquals((byte) 0xFF, sf[34]);
|
||||
assertEquals((byte) AID_ENTER, sf[35]);
|
||||
|
||||
// Mouse Button 1 with picked segment 2 and tag 5
|
||||
// Mouse Button 1 matching HODInput.java (0x04 trigger constants and 0x23 correlation descriptor)
|
||||
byte[] sfMouse = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
294, 192, 1, true, false, false, 2, 5
|
||||
294, 192, 1, true, false, false
|
||||
);
|
||||
assertEquals(56, sfMouse.length);
|
||||
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(0x00, sfMouse[17]);
|
||||
assertEquals(0x23, sfMouse[18]);
|
||||
assertEquals(0x00, sfMouse[19]);
|
||||
int mx = (sfMouse[24] << 8) | (sfMouse[25] & 0xFF);
|
||||
int my = (sfMouse[26] << 8) | (sfMouse[27] & 0xFF);
|
||||
assertEquals(294, (short) mx);
|
||||
assertEquals(192, (short) my);
|
||||
int segId = ((sfMouse[28] & 0xFF) << 24) | ((sfMouse[29] & 0xFF) << 16) |
|
||||
((sfMouse[30] & 0xFF) << 8) | (sfMouse[31] & 0xFF);
|
||||
assertEquals(2, segId);
|
||||
int tag = ((sfMouse[32] & 0xFF) << 8) | (sfMouse[33] & 0xFF);
|
||||
assertEquals(5, tag);
|
||||
assertEquals(0x00, sfMouse[28]);
|
||||
assertEquals(0x00, sfMouse[29]);
|
||||
assertEquals(0x00, sfMouse[30]);
|
||||
assertEquals(0x04, sfMouse[31]); // Mouse trigger class constant (0x04)
|
||||
assertEquals(0x00, sfMouse[32]);
|
||||
assertEquals(0x04, sfMouse[33]); // Mouse correlation class constant (0x04)
|
||||
assertEquals(0x00, sfMouse[34]);
|
||||
assertEquals(0x01, sfMouse[35]); // Button 1
|
||||
|
||||
// Mouse Button 2 (Action) with no segment (0)
|
||||
// Mouse Button 2 (Action) with Shift modifier
|
||||
byte[] sfMouse2 = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
0, 0, 2, true, true, false, 0, 0
|
||||
0, 0, 2, true, true, false
|
||||
);
|
||||
int segId2 = ((sfMouse2[28] & 0xFF) << 24) | ((sfMouse2[29] & 0xFF) << 16) |
|
||||
((sfMouse2[30] & 0xFF) << 8) | (sfMouse2[31] & 0xFF);
|
||||
assertEquals(0, segId2);
|
||||
int tag2 = ((sfMouse2[32] & 0xFF) << 8) | (sfMouse2[33] & 0xFF);
|
||||
assertEquals(0, tag2);
|
||||
assertEquals(0x23, sfMouse2[16]);
|
||||
assertEquals(0x00, sfMouse2[17]);
|
||||
assertEquals(0x23, sfMouse2[18]);
|
||||
assertEquals(0x00, sfMouse2[19]);
|
||||
assertEquals(0x04, sfMouse2[31]);
|
||||
assertEquals(0x04, sfMouse2[33]);
|
||||
assertEquals((byte) 0x80, sfMouse2[34]); // Shift modifier
|
||||
assertEquals(0x02, sfMouse2[35]); // Button 2
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLightPenSelectImmediateSpace() {
|
||||
screen.erase(false);
|
||||
// Field attribute at pos 0: unprotected, intensified selectable (0x08)
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
|
||||
// Designator character at pos 1: space (immediate select)
|
||||
screen.getCell(1).ec = 0x40;
|
||||
screen.getCell(1).ucs4 = ' ';
|
||||
screen.getCell(2).ec = (byte) 0xC1;
|
||||
screen.getCell(2).ucs4 = 'A';
|
||||
|
||||
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
public void sendAid(int aidCode) {
|
||||
sentAid.set(aidCode);
|
||||
}
|
||||
};
|
||||
|
||||
boolean result = input.lightPenSelect(2);
|
||||
assertTrue(result);
|
||||
assertEquals(AID_SELECT, sentAid.get());
|
||||
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||
assertEquals(2, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLightPenSelectEnterImmediateAmpersand() {
|
||||
screen.erase(false);
|
||||
// Field attribute at pos 0: normal detectable (0x04)
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||
// Designator character at pos 1: '&' (enter immediate)
|
||||
screen.getCell(1).ec = 0x50;
|
||||
screen.getCell(1).ucs4 = '&';
|
||||
|
||||
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
public void sendAid(int aidCode) {
|
||||
sentAid.set(aidCode);
|
||||
}
|
||||
};
|
||||
|
||||
boolean result = input.lightPenSelect(1);
|
||||
assertTrue(result);
|
||||
assertEquals(AID_ENTER, sentAid.get());
|
||||
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLightPenSelectDeferredToggle() {
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||
// Designator at pos 1: '?' (deferred select)
|
||||
screen.getCell(1).ec = 0x6F;
|
||||
screen.getCell(1).ucs4 = '?';
|
||||
|
||||
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
public void sendAid(int aidCode) {
|
||||
sentAid.set(aidCode);
|
||||
}
|
||||
};
|
||||
|
||||
// First click: ? -> > and MDT=1, no transmission
|
||||
boolean r1 = input.lightPenSelect(1);
|
||||
assertTrue(r1);
|
||||
assertEquals(-1, sentAid.get());
|
||||
assertEquals('>', screen.getCell(1).ucs4);
|
||||
assertEquals((byte) 0x6E, screen.getCell(1).ec);
|
||||
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||
|
||||
// Second click: > -> ? and MDT=0, no transmission
|
||||
boolean r2 = input.lightPenSelect(1);
|
||||
assertTrue(r2);
|
||||
assertEquals(-1, sentAid.get());
|
||||
assertEquals('?', screen.getCell(1).ucs4);
|
||||
assertEquals((byte) 0x6F, screen.getCell(1).ec);
|
||||
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLightPenSelectInvalidDesignatorRejected() {
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||
// Designator at pos 1: 'A' (invalid designator)
|
||||
screen.getCell(1).ec = (byte) 0xC1;
|
||||
screen.getCell(1).ucs4 = 'A';
|
||||
|
||||
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
public void sendAid(int aidCode) {
|
||||
sentAid.set(aidCode);
|
||||
}
|
||||
};
|
||||
|
||||
boolean result = input.lightPenSelect(1);
|
||||
assertFalse(result);
|
||||
assertEquals(-1, sentAid.get());
|
||||
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCursorSelectKey() {
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||
screen.getCell(1).ec = 0x50;
|
||||
screen.getCell(1).ucs4 = '&';
|
||||
screen.setCursorAddress(5);
|
||||
|
||||
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
public void sendAid(int aidCode) {
|
||||
sentAid.set(aidCode);
|
||||
}
|
||||
};
|
||||
|
||||
boolean result = input.cursorSelect();
|
||||
assertTrue(result);
|
||||
assertEquals(AID_ENTER, sentAid.get());
|
||||
assertEquals(5, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAidSuppressesNullsInModifiedField() {
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
@@ -231,4 +363,92 @@ public class InputProcessorTest {
|
||||
assertEquals((byte) 0xC9, result[8]); // 'I'
|
||||
assertEquals((byte) 0xE3, result[9]); // 'T'
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendGraphicMouseAidFraming() {
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
screen.getCell(1).ec = (byte) 0xC1; // 'A'
|
||||
screen.setCellFA(5, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setCursorAddress(2);
|
||||
|
||||
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
|
||||
goca.setGraphicsCursorActive(true);
|
||||
goca.setGraphicCursorPosition(150, -80);
|
||||
|
||||
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.setGocaDecoder(goca);
|
||||
|
||||
input.sendGraphicMouseAid(AID_ENTER, 1, false, false);
|
||||
|
||||
byte[] result = sent.get();
|
||||
assertNotNull(result);
|
||||
// Total expected length:
|
||||
// 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((byte) AID_SF, result[0]);
|
||||
// SF length = 52 (0x00 0x34) per IBM HOD / GOCA specification
|
||||
assertEquals(0x00, result[1]);
|
||||
assertEquals(0x34, result[2]);
|
||||
// SF ID = 0x0F0F
|
||||
assertEquals(0x0F, result[3]);
|
||||
assertEquals(0x0F, result[4]);
|
||||
// Coordinates in SF at index 1 + 24 = 25
|
||||
int gx = (result[25] << 8) | (result[26] & 0xFF);
|
||||
int gy = (result[27] << 8) | (result[28] & 0xFF);
|
||||
assertEquals(150, (short) gx);
|
||||
assertEquals(-80, (short) gy);
|
||||
// Mouse constant at index 1 + 31 = 32
|
||||
assertEquals(0x04, result[32]);
|
||||
assertEquals(0x04, result[34]);
|
||||
// Button 1 at index 1 + 35 = 36
|
||||
assertEquals(0x01, result[36]);
|
||||
|
||||
// Trailing AID at index 57
|
||||
assertEquals((byte) AID_ENTER, result[57]);
|
||||
// Trailing SBA at 60
|
||||
assertEquals((byte) ORDER_SBA, result[60]);
|
||||
// Trailing field content 'A' at 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