Keep par0dy with a3270
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m1s

This commit is contained in:
2026-08-24 20:46:45 +00:00
parent d2bdb9bb0a
commit b5a4978b24
9 changed files with 458 additions and 131 deletions
@@ -86,6 +86,10 @@ public class StatusBar extends JPanel {
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
this.client = client;
this.terminalPanel = terminalPanel;
if (terminalPanel != null) {
terminalPanel.setOnLightPenToggle(this::updateStatus);
}
updateStatus();
}
public void updateStatus() {
@@ -159,11 +159,20 @@ public class TerminalPanel extends JPanel {
col = Math.max(0, Math.min(col, displayCols - 1));
row = Math.max(0, Math.min(row, displayRows - 1));
// Start selection
// 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.
// 3. In standard alphanumeric mode, click-drag selects text for copy-paste.
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
if (isGraphic || lightPenMode) {
clearSelection();
} else {
selectionStartRow = row;
selectionStartCol = col;
selectionEndRow = row;
selectionEndCol = col;
}
isDragging = true;
repaint();
}
@@ -172,9 +181,21 @@ public class TerminalPanel extends JPanel {
@Override
public void mouseDragged(MouseEvent e) {
if (isDragging && client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer();
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
ScreenBuffer sb = client.getScreenBuffer();
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
if (isGraphic && client.getGraphicsPlane() != null) {
int gridW = sb.getDisplayCols() * cellWidth;
int gridH = sb.getDisplayRows() * cellHeight;
int gWidth = client.getGraphicsPlane().getCanvasWidth();
int gHeight = client.getGraphicsPlane().getCanvasHeight();
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
int py = (gridH > 0 && gHeight > 0) ? (int) Math.round((double) (e.getY() - oy) * gHeight / gridH) : (e.getY() - oy);
px = Math.max(0, Math.min(px, gWidth - 1));
py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
} else if (!lightPenMode) {
int col = (e.getX() - ox) / cellWidth;
int row = (e.getY() - oy) / cellHeight;
col = Math.max(0, Math.min(col, sb.getDisplayCols() - 1));
@@ -182,6 +203,7 @@ public class TerminalPanel extends JPanel {
selectionEndRow = row;
selectionEndCol = col;
}
repaint();
}
}
@@ -198,6 +220,8 @@ public class TerminalPanel extends JPanel {
int gHeight = client.getGraphicsPlane().getCanvasHeight();
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
int py = (gridH > 0 && gHeight > 0) ? (int) Math.round((double) (e.getY() - oy) * gHeight / gridH) : (e.getY() - oy);
px = Math.max(0, Math.min(px, gWidth - 1));
py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
}
}
@@ -206,40 +230,40 @@ public class TerminalPanel extends JPanel {
public void mouseReleased(MouseEvent e) {
if (isDragging) {
isDragging = false;
// If start == end, treat as a click (position cursor, clear selection)
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
if (client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer();
int displayCols = sb.getDisplayCols();
int newAddr = selectionStartRow * displayCols + selectionStartCol;
sb.setCursorAddress(newAddr);
clearSelection();
if (lightPenMode) {
System.out.println("LP click: row=" + selectionStartRow + " col=" + selectionStartCol
+ " addr=" + newAddr + " mouseY=" + e.getY() + " oy=" + getRenderOffsetY()
+ " cellH=" + cellHeight);
boolean result = client.getInputProcessor().lightPenSelect(newAddr);
System.out.println("LP lightPenSelect result: " + result);
if (result) {
refreshScreen();
return;
}
}
if (client.getGocaDecoder() != null && client.getGraphicsPlane() != null) {
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
int gridW = sb.getDisplayCols() * cellWidth;
int gridH = sb.getDisplayRows() * cellHeight;
int displayCols = sb.getDisplayCols();
int displayRows = sb.getDisplayRows();
int col = (e.getX() - ox) / cellWidth;
int row = (e.getY() - oy) / cellHeight;
col = Math.max(0, Math.min(col, displayCols - 1));
row = Math.max(0, Math.min(row, displayRows - 1));
int clickAddr = row * displayCols + col;
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null;
if (isGraphic) {
clearSelection();
int gridW = displayCols * cellWidth;
int gridH = displayRows * cellHeight;
int gWidth = client.getGraphicsPlane().getCanvasWidth();
int gHeight = client.getGraphicsPlane().getCanvasHeight();
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
int py = (gridH > 0 && gHeight > 0) ? (int) Math.round((double) (e.getY() - oy) * gHeight / gridH) : (e.getY() - oy);
px = Math.max(0, Math.min(px, gWidth - 1));
py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
if (client.getGocaDecoder().isGraphicsCursorActive()) {
// Light-pen / Graphic cursor touch event (matches IBM Host On-Demand PS3179G)
int gx = client.getGocaDecoder().getGraphicCursorX();
int gy = client.getGocaDecoder().getGraphicCursorY();
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
));
client.getInputProcessor().sendGraphicMouseAid(
org.lib3270j.protocol.DS3270Constants.AID_ENTER,
button,
@@ -249,8 +273,20 @@ public class TerminalPanel extends JPanel {
refreshScreen();
return;
}
if (lightPenMode) {
clearSelection();
sb.setCursorAddress(clickAddr);
boolean result = client.getInputProcessor().lightPenSelect(clickAddr);
if (result) {
refreshScreen();
return;
}
}
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
sb.setCursorAddress(clickAddr);
clearSelection();
refreshScreen();
return;
}
@@ -1140,9 +1176,18 @@ public class TerminalPanel extends JPanel {
if (blinkTimer != null)
blinkTimer.stop();
}
private Runnable onLightPenToggle;
public void setOnLightPenToggle(Runnable callback) {
this.onLightPenToggle = callback;
}
public void toggleLightPen() {
this.lightPenMode = !this.lightPenMode;
System.out.println("Light Pen mode: " + (this.lightPenMode ? "ON" : "OFF"));
if (onLightPenToggle != null) {
onLightPenToggle.run();
}
repaint();
}
@@ -125,8 +125,8 @@ public class QueryReplyBuilder {
// Vector Graphics QRs if enabled
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
appendQueryReply(out, QR_GRAPHICS, buildGraphics()); // 0xB0
appendQueryReply(out, QR_GIMAGE, buildGImage()); // 0xB1
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
appendOemFmt(out); // 0xB3
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
@@ -204,14 +204,14 @@ public class QueryReplyBuilder {
break;
case QR_GRAPHICS:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GRAPHICS, buildGraphics());
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows));
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break;
case QR_GIMAGE:
if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GIMAGE, buildGImage());
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows));
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
@@ -422,13 +422,25 @@ public class QueryReplyBuilder {
return out.toByteArray();
}
private byte[] buildGraphics() {
return new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 };
private byte[] buildGraphics(int maxCols, int maxRows) {
int width = maxCols * 9;
int height = maxRows * 12;
return new byte[]{
(byte) 0x80, 0x02,
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
0x00
};
}
private byte[] buildGImage() {
private byte[] buildGImage(int maxCols, int maxRows) {
int width = maxCols * 9;
int height = maxRows * 12;
return new byte[]{
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
0x00, 0x01,
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
@@ -61,6 +61,7 @@ public final class GocaConstants {
public static final int G_GSMCEL = 0x37; // Set Marker Cell
public static final int G_GSCS = 0x38; // Set Character Set
public static final int G_GSMP = 0x39; // Set Marker Precision
public static final int G_GSETAG = 0x39; // Set Pick Identifier / Tag
public static final int G_GSCD = 0x3A; // Set Character Direction
public static final int G_GSCC = 0x3B; // Set Character Precision
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
@@ -63,6 +63,39 @@ public class GocaDecoder {
private final java.util.Set<Integer> chainedTargets = new java.util.HashSet<>();
private int callDepth = 0;
/**
* Bounding box and pick correlation tracking for GOCA segments.
*/
public static class SegmentBounds {
public final int segId;
public int minX = Integer.MAX_VALUE;
public int maxX = Integer.MIN_VALUE;
public int minY = Integer.MAX_VALUE;
public int maxY = Integer.MIN_VALUE;
public int tag = 0;
public SegmentBounds(int segId) {
this.segId = segId;
}
public void include(int x, int y) {
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
public boolean contains(int x, int y, int margin) {
if (minX == Integer.MAX_VALUE) return false;
return x >= minX - margin && x <= maxX + margin &&
y >= minY - margin && y <= maxY + margin;
}
}
private final java.util.Map<Integer, SegmentBounds> segmentBoundsMap = new java.util.HashMap<>();
private final java.util.List<SegmentBounds> activeSegmentsInOrder = new java.util.ArrayList<>();
private int currentSegId = 0;
// Graphic Cursor (Light-Pen) state
private boolean graphicsCursorActive = false;
private int graphicCursorX = 0;
@@ -88,6 +121,10 @@ public class GocaDecoder {
return curY;
}
public int getCharSet() {
return charSet;
}
public synchronized boolean isGraphicsCursorActive() {
return graphicsCursorActive;
}
@@ -116,6 +153,35 @@ public class GocaDecoder {
}
}
public synchronized int findPickedSegment(int gx, int gy) {
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
SegmentBounds sb = activeSegmentsInOrder.get(i);
if (sb.contains(gx, gy, 12)) {
System.err.println(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
));
return sb.segId;
}
}
System.err.println(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
return 0;
}
public synchronized int getSegmentTag(int segId) {
SegmentBounds sb = segmentBoundsMap.get(segId);
return sb != null ? sb.tag : 0;
}
private void trackPoint(int x, int y) {
if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) {
sb.include(x, y);
}
}
}
public synchronized void resetDefaults() {
curX = 0;
curY = 0;
@@ -126,6 +192,9 @@ public class GocaDecoder {
segmentChainMap.clear();
segmentOrderList.clear();
chainedTargets.clear();
segmentBoundsMap.clear();
activeSegmentsInOrder.clear();
currentSegId = 0;
resetAttributes();
}
@@ -154,6 +223,19 @@ public class GocaDecoder {
private byte[] partialOrderBuffer = new byte[0];
/**
* Determines the total byte length of a GOCA drawing order starting at data[idx].
*
* IMPORTANT ARCHITECTURE NOTE:
* GOCA orders follow IBM GA23-0059 architecture rules:
* 1. 1-byte standalone orders (NOP, etc.) -> length 1.
* 2. Delimiter orders (GEAR, ENDSEGM, ENDPROLOGUE, GEIMG) -> 1 or 2 bytes (with 0x00 trailing byte).
* 3. Fixed 1-byte immediate operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes.
* 4. Orders with opcode >= 0x20 (including GCALL 0x2A, GSCS 0x38, GSCD 0x3A, GSPT 0x28, GSMT 0x29, GBAR 0x68,
* GLINE 0xC1, GARC 0xC6, GCHST 0xC3, etc.) are self-defining with a 1-byte length field data[idx+1],
* making total length = payloadLen + 2.
* Never hardcode orders >= 0x20 to 2 bytes, as that desynchronizes the GOCA order stream.
*/
private int getOrderLength(byte[] data, int idx, int end) {
int order = data[idx] & 0xFF;
if (order == GocaConstants.G_NOP1 || order == 0xFF) {
@@ -164,23 +246,26 @@ public class GocaDecoder {
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) {
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
}
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS ||
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW ||
order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW ||
order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT ||
order == GocaConstants.G_GSMT || order == GocaConstants.G_GSMCEL ||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSMP ||
order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCC ||
order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
return 2;
}
if (order == GocaConstants.G_GCALL) { // Call Segment (0x2A <32-bit segment ID>)
return 5;
}
if (idx + 1 >= end) {
return -1;
}
// Fixed 2-byte orders: 1-byte opcode + 1-byte operand
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS ||
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSLT ||
order == GocaConstants.G_GSLW || order == GocaConstants.G_GSMS) {
return 2;
}
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1)
if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT ||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD ||
order == GocaConstants.G_GBAR) {
return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2;
}
if (order == GocaConstants.G_GCALL) {
return (data[idx + 1] == 0x04 && idx + 5 < end) ? 6 : 5;
}
// Self-defining orders with 1-byte length field: order (1) + length byte (1) + payload (len)
return (data[idx + 1] & 0xFF) + 2;
}
@@ -303,6 +388,16 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
if (idx + 5 < end) {
int segId = ((inputData[idx + 2] & 0xFF) << 24) |
((inputData[idx + 3] & 0xFF) << 16) |
((inputData[idx + 4] & 0xFF) << 8) |
(inputData[idx + 5] & 0xFF);
currentSegId = segId;
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb);
}
if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) {
resetAttributes();
}
@@ -310,6 +405,27 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) {
System.err.println(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
));
}
}
currentSegId = 0;
idx += orderLen;
break;
}
case GocaConstants.G_GSETAG: { // Set Pick Identifier / Tag (0x39)
if (currentSegId != 0 && payloadLen >= 2 && idx + 3 < end) {
int tag = ((inputData[idx + 2] & 0xFF) << 8) | (inputData[idx + 3] & 0xFF);
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) {
sb.tag = tag;
}
}
idx += orderLen;
break;
}
@@ -344,16 +460,34 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_GCALL: { // Call Segment (0x2A)
if (callDepth < 16 && idx + 4 < end) {
int targetSegId = ((inputData[idx + 1] & 0xFF) << 24) |
if (callDepth < 16) {
int targetSegId;
if (orderLen == 6 && idx + 5 < end) {
targetSegId = ((inputData[idx + 2] & 0xFF) << 24) |
((inputData[idx + 3] & 0xFF) << 16) |
((inputData[idx + 4] & 0xFF) << 8) |
(inputData[idx + 5] & 0xFF);
} else if (idx + 4 < end) {
targetSegId = ((inputData[idx + 1] & 0xFF) << 24) |
((inputData[idx + 2] & 0xFF) << 16) |
((inputData[idx + 3] & 0xFF) << 8) |
(inputData[idx + 4] & 0xFF);
} else {
targetSegId = -1;
}
if (targetSegId != -1) {
byte[] targetSeg = segmentStore.get(targetSegId);
if (targetSeg != null) {
int savedSegId = currentSegId;
currentSegId = targetSegId;
SegmentBounds targetSb = segmentBoundsMap.computeIfAbsent(targetSegId, SegmentBounds::new);
activeSegmentsInOrder.remove(targetSb);
activeSegmentsInOrder.add(targetSb);
callDepth++;
decodeStream(targetSeg, 0, targetSeg.length);
decodeStreamDirect(targetSeg, 0, targetSeg.length);
callDepth--;
currentSegId = savedSegId;
}
}
}
idx += orderLen;
@@ -420,7 +554,7 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_GSMT: { // Set Marker Type (0x29)
markerType = inputData[idx + 1] & 0xFF;
markerType = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
idx += orderLen;
break;
}
@@ -430,24 +564,23 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28)
pattern = inputData[idx + 1] & 0xFF;
pattern = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
idx += orderLen;
break;
}
case GocaConstants.G_GSCD: { // Set Character Direction (0x3A)
charDir = inputData[idx + 1] & 0xFF;
charDir = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
idx += orderLen;
break;
}
case GocaConstants.G_GSCS: { // Set Character Set (0x38)
charSet = inputData[idx + 1] & 0xFF;
charSet = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
idx += orderLen;
break;
}
case 0x04:
case GocaConstants.G_GSMX:
case GocaConstants.G_GSFLW:
case GocaConstants.G_GSMP:
case GocaConstants.G_GSCC:
case GocaConstants.G_GSMS_SET:
case GocaConstants.G_GPOP: {
@@ -460,7 +593,7 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = inputData[idx + 1] & 0xFF;
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0 || (flags == 0);
boolean fill = (flags == 0) || (flags & 0x40) != 0 ||
(pattern >= 1 && pattern <= 14);
@@ -504,21 +637,21 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_GCFARC: { // Full Arc Current Position (0x87)
if (payloadLen >= 2) {
if (payloadLen >= 0) {
processArc(inputData, idx + 2, payloadLen, true, true);
}
idx += orderLen;
break;
}
case GocaConstants.G_GARC: { // Partial Arc Absolute (0xC6)
if (payloadLen >= 8) {
if (payloadLen >= 4) {
processArc(inputData, idx + 2, payloadLen, false, false);
}
idx += orderLen;
break;
}
case GocaConstants.G_GCARC: { // Partial Arc Current Position (0x86)
if (payloadLen >= 4) {
if (payloadLen >= 0) {
processArc(inputData, idx + 2, payloadLen, true, false);
}
idx += orderLen;
@@ -761,6 +894,8 @@ public class GocaDecoder {
pos += 4;
}
trackPoint(startX, startY);
if (inArea) {
addAreaPoint(startX, startY);
}
@@ -770,6 +905,8 @@ public class GocaDecoder {
int nextY = readCoord(data, pos + 2);
pos += 4;
trackPoint(nextX, nextY);
if (inArea) {
addAreaPoint(nextX, nextY);
} else {
@@ -799,6 +936,8 @@ public class GocaDecoder {
pos += 4;
}
trackPoint(startX, startY);
if (inArea) {
addAreaPoint(startX, startY);
}
@@ -811,6 +950,8 @@ public class GocaDecoder {
int nextX = startX + dx;
int nextY = startY + dy;
trackPoint(nextX, nextY);
if (inArea) {
addAreaPoint(nextX, nextY);
} else {
@@ -859,19 +1000,28 @@ public class GocaDecoder {
// Semi-axis lengths in GOCA coordinate space
double semiAxis1 = Math.sqrt(pScaled * pScaled + qScaled * qScaled);
double semiAxis2 = Math.sqrt(rScaled * rScaled + sScaled * sScaled);
// If arc parameters are at default (1,0,0,1) and curPos != center, use distance to center as radius
double dist = Math.hypot(curX - centerX, curY - centerY);
if (semiAxis1 <= 1.01 && dist > 1.0) {
semiAxis1 = dist * multiplier;
semiAxis2 = dist * multiplier;
}
if (semiAxis1 < 1.0) semiAxis1 = 1.0;
if (semiAxis2 < 1.0) semiAxis2 = 1.0;
// Map to pixel space
int rx = Math.abs(plane.mapX((int) Math.round(semiAxis1)) - plane.mapX(0));
int ry = Math.abs(plane.mapY(0) - plane.mapY((int) Math.round(semiAxis2)));
if (rx <= 0) rx = 1;
if (ry <= 0) ry = 1;
if (rx <= 0) rx = Math.max(1, (int) Math.round(semiAxis1));
if (ry <= 0) ry = Math.max(1, (int) Math.round(semiAxis2));
// For partial arcs, read sweep start and sweep angle
// For partial arcs, determine start angle and sweep angle
double startAngleDeg = 0.0;
double sweepAngleDeg = 360.0;
if (!isFull && pos + 4 <= off + len) {
if (!isFull) {
if (pos + 4 <= off + len) {
// Sweep start: integer.fraction of full revolution
int startInt = data[pos];
int startFrac = data[pos + 1] & 0xFF;
@@ -881,14 +1031,32 @@ public class GocaDecoder {
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
} else if (!isFull) {
// No sweep data default to full circle
} else if (pos + 2 <= off + len) {
// Single sweep parameter
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapY(centerY) - plane.mapY(curY), plane.mapX(curX) - plane.mapX(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
} else {
// No sweep data if we have a current point, start there and sweep full circle
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapY(centerY) - plane.mapY(curY), plane.mapX(curX) - plane.mapX(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
sweepAngleDeg = 360.0;
}
}
System.out.println("processArc: center=(" + centerX + "," + centerY + ") mult=" + multiplier
+ " P=" + arcParamP + " Q=" + arcParamQ + " R=" + arcParamR + " S=" + arcParamS
+ " rx=" + rx + " ry=" + ry + " start=" + startAngleDeg + " sweep=" + sweepAngleDeg
trackPoint(centerX - (int) Math.round(semiAxis1), centerY - (int) Math.round(semiAxis2));
trackPoint(centerX + (int) Math.round(semiAxis1), centerY + (int) Math.round(semiAxis2));
System.out.println("processArc: center=(" + centerX + "," + centerY + ") cur=(" + curX + "," + curY
+ ") rx=" + rx + " ry=" + ry + " start=" + startAngleDeg + " sweep=" + sweepAngleDeg
+ " isFull=" + isFull);
plane.drawArc(plane.mapX(centerX), plane.mapY(centerY), rx, ry, startAngleDeg, sweepAngleDeg,
@@ -937,12 +1105,14 @@ public class GocaDecoder {
int end = off + len;
if (fromCurPos) {
trackPoint(curX, curY);
plane.drawMarker(plane.mapX(curX), plane.mapY(curY), markerType, markerSize, markerColor);
}
while (pos + 4 <= end) {
int x = readCoord(data, pos);
int y = readCoord(data, pos + 2);
trackPoint(x, y);
plane.drawMarker(plane.mapX(x), plane.mapY(y), markerType, markerSize, markerColor);
curX = x;
curY = y;
@@ -970,6 +1140,12 @@ public class GocaDecoder {
int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10;
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 12.0)) : 14;
int totalW = textLen * (charWidth > 0 ? charWidth : 9);
int totalH = (charHeight > 0 ? charHeight : 14);
trackPoint(startX, startY);
trackPoint(startX + totalW, startY + totalH);
trackPoint(startX + totalW, startY - totalH);
if (charSet != 0 && programSymbolManager != null) {
for (int i = 0; i < textLen; i++) {
int code = data[pos + i] & 0xFF;
@@ -30,6 +30,34 @@ public class GraphicInputBuilder {
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl) {
return buildGraphicInput(gocaX, gocaY, aidCode, isMouseAction, isShift, isCtrl, 0, 0);
}
/**
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
*
* IMPORTANT ARCHITECTURE NOTE:
* Per IBM GA23-0059 / GDDM specifications:
* - Bytes 24-27: (gocaX, gocaY) cursor coordinates.
* - Bytes 28-31: Picked Segment Identifier (32-bit big-endian). When clicking on a menu item or
* interactive element (e.g. DRAW button = Segment 2, EXIT button = Segment 5), GDDM requires
* the exact segment ID in bytes 28-31. If hardcoded or mismatched, GDDM rejects the click
* with a WCC 0xF7 alarm beep.
* - Bytes 32-33: Pick Correlation Tag (16-bit big-endian) set by G_GSETAG (0x39).
*
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
* @param isShift true if shift key was down
* @param isCtrl true if ctrl key was down
* @param pickedSegId Picked GOCA segment ID (0 if none)
* @param pickTag Pick correlation tag
* @return 56-byte payload
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl,
int pickedSegId, int pickTag) {
byte[] sf = new byte[MASK.length];
System.arraycopy(MASK, 0, sf, 0, MASK.length);
@@ -41,11 +69,22 @@ public class GraphicInputBuilder {
sf[26] = (byte) ((gocaY >> 8) & 0xFF);
sf[27] = (byte) (gocaY & 0xFF);
if (pickedSegId != 0) {
// 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);
}
if (isMouseAction) {
sf[31] = 0x04;
sf[33] = 0x04;
if (pickTag != 0) {
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
sf[33] = (byte) (pickTag & 0xFF);
}
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
} else {
// Keyboard AID (Enter, PF keys)
sf[31] = 0x07;
@@ -123,6 +123,7 @@ public class GraphicsPlane {
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
* Coordinate space is symmetric: -xMax to +xMax, where nominalWidth = cols * 9 (e.g. 720 for 80 cols).
*/
public int mapX(int gocaX) {
int nominalWidth = screenCols * 9;
@@ -133,6 +134,8 @@ public class GraphicsPlane {
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
* Coordinate space is symmetric: -yMax to +yMax, where nominalHeight = rows * 12 (e.g. 516 for 43 rows).
* NOTE: Do not apply arbitrary offsets here. The GOCA coordinate system is 1:1 synchronized with host GDDM.
*/
public int mapY(int gocaY) {
int nominalHeight = screenRows * 12;
@@ -143,6 +146,7 @@ public class GraphicsPlane {
/**
* Maps a canvas pixel X coordinate back to GOCA signed coordinate (-xMax..+xMax).
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
*/
public int unmapX(int px) {
int nominalWidth = screenCols * 9;
@@ -153,6 +157,7 @@ public class GraphicsPlane {
/**
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up).
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
*/
public int unmapY(int py) {
int nominalHeight = screenRows * 12;
@@ -239,17 +239,6 @@ public class InputProcessor {
// Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
ByteArrayOutputStream out = new ByteArrayOutputStream();
// If GDDM attached graphic cursor (interactive graphics / light-pen mode):
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive() && aidCode != AID_CLEAR) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(gx, gy, aidCode, false, false, false);
out.write(AID_SF); // 0x88
try {
out.write(sf);
} catch (java.io.IOException ignored) {}
}
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
@@ -336,16 +325,25 @@ public class InputProcessor {
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(gx, gy, aidCode, true, isShift, isCtrl);
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, aidCode, true, isShift, isCtrl, pickedSeg, pickTag
);
System.err.println(String.format(
"sendGraphicMouseAid: goca=(%d, %d) pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
gx, gy, pickedSeg, pickTag, button, isShift, isCtrl
));
out.write(AID_SF);
try {
out.write(sf);
} catch (java.io.IOException ignored) {}
}
} else {
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
}
sendAidResponse(out.toByteArray());
}
@@ -234,6 +234,12 @@ public class GocaDecoderTest {
assertEquals(0, plane.mapY(257));
// Bottom edge (-258) should map to 515
assertEquals(515, plane.mapY(-258));
// Bidirectional roundtrip mapping must be exact
assertEquals(0, plane.unmapX(plane.mapX(0)));
assertEquals(0, plane.unmapY(plane.mapY(0)));
assertEquals(100, plane.unmapY(plane.mapY(100)));
assertEquals(-200, plane.unmapY(plane.mapY(-200)));
}
@Test
@@ -288,4 +294,45 @@ public class GocaDecoderTest {
assertFalse(dsp.getGraphicsPlane().hasContent(), "Expected graphics plane to be cleared after SF_OBJCNTL P_ERASE");
}
@Test
public void testGcallAndGscsWithLengthBytes() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Construct Segment 2: Begin Segment (id=2), Set Color, draw line, End Segment
ByteArrayOutputStream s2 = new ByteArrayOutputStream();
s2.write(GocaConstants.G_BEGSEGM);
s2.write(0x0C);
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x02); // id=2
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
s2.write(GocaConstants.G_GSCS); s2.write(0x01); s2.write(0x40); // charSet = 0x40
s2.write(GocaConstants.G_GSCOL); s2.write(0x02); // Color = Red
s2.write(GocaConstants.G_GLINE); s2.write(0x08);
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
s2.write(0x00); s2.write(0x32); s2.write(0x00); s2.write(0x32);
s2.write(GocaConstants.G_ENDSEGM); s2.write(0x00);
byte[] s2Bytes = s2.toByteArray();
decoder.decodeStream(s2Bytes, 0, s2Bytes.length);
// Segment 1 calls Segment 2 via GCALL (0x2A len=4 segId=2)
ByteArrayOutputStream s1 = new ByteArrayOutputStream();
s1.write(GocaConstants.G_BEGSEGM);
s1.write(0x0C);
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x01); // id=1
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
// GCALL (0x2A len=4 segId=2)
s1.write(GocaConstants.G_GCALL); s1.write(0x04);
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x02);
s1.write(GocaConstants.G_ENDSEGM); s1.write(0x00);
byte[] s1Bytes = s1.toByteArray();
decoder.decodeStream(s1Bytes, 0, s1Bytes.length);
assertEquals(0x40, decoder.getCharSet());
assertTrue(plane.hasContent(), "Expected plane to have content after GCALL segment execution");
}
}