ADMOPSLA and ADMCHART are happier
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m7s

This commit is contained in:
2026-08-23 01:27:45 +00:00
parent 42eef519a6
commit 0837de8db3
19 changed files with 792 additions and 114 deletions
@@ -180,6 +180,22 @@ public class TerminalPanel extends JPanel {
} }
} }
@Override
public void mouseMoved(MouseEvent e) {
if (client != null && client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
ScreenBuffer sb = client.getScreenBuffer();
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);
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
}
}
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (isDragging) { if (isDragging) {
@@ -188,8 +204,35 @@ public class TerminalPanel extends JPanel {
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) { if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer(); ScreenBuffer sb = client.getScreenBuffer();
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol); int newAddr = selectionStartRow * sb.getCols() + selectionStartCol;
sb.setCursorAddress(newAddr);
clearSelection(); clearSelection();
if (client.getGocaDecoder() != null && client.getGraphicsPlane() != null) {
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
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);
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
if (client.getGocaDecoder().isGraphicsCursorActive()) {
// Light-pen / Graphic cursor touch event (matches IBM Host On-Demand PS3179G)
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
client.getInputProcessor().sendGraphicMouseAid(
org.lib3270j.protocol.DS3270Constants.AID_ENTER,
button,
e.isShiftDown(),
e.isControlDown()
);
refreshScreen();
return;
}
}
refreshScreen(); refreshScreen();
return; return;
} }
@@ -849,6 +892,20 @@ public class TerminalPanel extends JPanel {
int cols = sb.getDisplayCols(); int cols = sb.getDisplayCols();
boolean isColorModel = client.getConfig().getModel().isColor(); boolean isColorModel = client.getConfig().getModel().isColor();
// Draw Vector Graphics Plane under text if present
if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) {
int gridW = cols * cellWidth;
int gridH = rows * cellHeight;
int gWidth = client.getGraphicsPlane().getCanvasWidth();
int gHeight = client.getGraphicsPlane().getCanvasHeight();
int[] rgb = client.getGraphicsPlane().getRgbBuffer();
if (rgb != null && gWidth > 0 && gHeight > 0) {
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gWidth, gHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB);
img.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
g2.drawImage(img, ox, oy, gridW, gridH, null);
}
}
// Track current field attribute for monochrome color decisions // Track current field attribute for monochrome color decisions
byte currentFA = 0; byte currentFA = 0;
ExtendedAttribute currentFieldEa = null; ExtendedAttribute currentFieldEa = null;
@@ -940,7 +997,8 @@ public class TerminalPanel extends JPanel {
if (cs >= 0x40 && client.getProgramSymbolManager() != null) { if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF); org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
if (slot != null) { if (slot != null) {
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), bgColor.getRGB()); int symBg = (!bgColor.equals(this.bgColor) || reverse) ? bgColor.getRGB() : 0;
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), symBg);
if (img != null) { if (img != null) {
g2.drawImage(img, x, y, null); g2.drawImage(img, x, y, null);
drawnAsPs = true; drawnAsPs = true;
@@ -977,20 +1035,22 @@ public class TerminalPanel extends JPanel {
} }
} }
// Draw Vector Graphics Plane overlay if present // Draw Graphic Cursor (Light-Pen / interactive graphics pointer) if active
if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) { if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
int gridW = cols * cellWidth; int gocaX = client.getGocaDecoder().getGraphicCursorX();
int gridH = rows * cellHeight; int gocaY = client.getGocaDecoder().getGraphicCursorY();
client.getGraphicsPlane().resize(gridW, gridH); int px = ox + client.getGraphicsPlane().mapX(gocaX);
int[] rgb = client.getGraphicsPlane().getRgbBuffer(); int py = oy + client.getGraphicsPlane().mapY(gocaY);
if (rgb != null) {
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gridW, gridH, java.awt.image.BufferedImage.TYPE_INT_ARGB); g2.setColor(Color.WHITE);
img.setRGB(0, 0, gridW, gridH, rgb, 0, gridW); g2.setXORMode(Color.BLACK);
g2.drawImage(img, ox, oy, gridW, gridH, null); // Draw a crosshair cursor for the light-pen / graphic cursor
} g2.drawLine(px - 6, py, px + 6, py);
g2.drawLine(px, py - 6, px, py + 6);
g2.setPaintMode();
} }
// Draw cursor // Draw 3270 text cursor
if (cursorVisible && client.getConnectionState().isFullSession()) { if (cursorVisible && client.getConnectionState().isFullSession()) {
int curAddr = sb.getDisplayCursorAddress(); int curAddr = sb.getDisplayCursorAddress();
int curRow = curAddr / cols; int curRow = curAddr / cols;
@@ -26,7 +26,7 @@ public class DiagnosticClient {
String host = args.length >= 1 ? args[0] : "192.168.0.30"; String host = args.length >= 1 ? args[0] : "192.168.0.30";
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270; int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270;
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_2); ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4);
client = new Telnet3270Client(config); client = new Telnet3270Client(config);
client.addConnectionListener(new ConnectionListener() { client.addConnectionListener(new ConnectionListener() {
@@ -54,6 +54,7 @@ public class Telnet3270Client {
dsProcessor.setOutputSender(fsm::send3270Data); dsProcessor.setOutputSender(fsm::send3270Data);
dsProcessor.setInputProcessor(inputProcessor); dsProcessor.setInputProcessor(inputProcessor);
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane()); inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
} }
/** /**
@@ -89,6 +89,10 @@ public class DataStreamProcessor {
this.inputProcessor = inputProcessor; this.inputProcessor = inputProcessor;
} }
public org.lib3270j.input.InputProcessor getInputProcessor() {
return inputProcessor;
}
public void addScreenUpdateListener(ScreenUpdateListener l) { public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l); screenListeners.add(l);
} }
@@ -175,6 +179,7 @@ public class DataStreamProcessor {
case CMD_WSF: case CMD_WSF:
case SNA_CMD_WSF: case SNA_CMD_WSF:
processWriteStructuredField(data, offset, length); processWriteStructuredField(data, offset, length);
keyboardRestore = true;
break; break;
case CMD_NOP: case CMD_NOP:
log.info(">>> NOP command"); log.info(">>> NOP command");
@@ -190,6 +195,10 @@ public class DataStreamProcessor {
screen.updateDisplaySnapshot(); screen.updateDisplaySnapshot();
} }
if (keyboardRestore && inputProcessor != null) {
inputProcessor.setKeyboardLocked(false);
}
// Debug: dump non-empty screen lines // Debug: dump non-empty screen lines
if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) { if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) {
int r = screen.getRows(); int r = screen.getRows();
@@ -782,22 +791,27 @@ public class DataStreamProcessor {
case SF_READ_PART: case SF_READ_PART:
processSFReadPartition(data, pos, fieldLen); processSFReadPartition(data, pos, fieldLen);
break; break;
case SF_ERASE_RESET: case SF_ERASE_RESET: {
if (fieldLen >= 4) { boolean alt = (fieldLen >= 4) && ((data[pos + 3] & 0xFF) == SF_ER_ALT);
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT; screen.erase(alt);
screen.erase(alt); graphicsPlane.clear();
graphicsPlane.clear(); gocaDecoder.resetDefaults();
notifyScreenSizeChanged(); notifyScreenSizeChanged();
}
break; break;
}
case SF_SET_REPLY_MODE: case SF_SET_REPLY_MODE:
if (fieldLen >= 5) { if (fieldLen >= 5) {
screen.setReplyMode((byte) (data[pos + 4] & 0xFF)); screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
} }
break; break;
case SF_CREATE_PART: case SF_CREATE_PART:
// Acknowledged — we use implicit partition if (fieldLen >= 4) {
int pid = data[pos + 3] & 0xFF;
screen.setActivePartition(pid);
log.fine("Created active partition ID=" + pid);
}
graphicsPlane.clear(); graphicsPlane.clear();
gocaDecoder.resetDefaults();
break; break;
case SF_OUTBOUND_DS: case SF_OUTBOUND_DS:
if (fieldLen > 5) { if (fieldLen > 5) {
@@ -879,6 +893,13 @@ public class DataStreamProcessor {
} }
break; break;
} }
case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
if (fieldLen > 3) {
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3);
notifyScreenUpdated();
}
break;
case org.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA case org.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
if (fieldLen > 3) { if (fieldLen > 3) {
@@ -915,6 +936,8 @@ public class DataStreamProcessor {
switch (type) { switch (type) {
case SF_RP_QUERY: case SF_RP_QUERY:
log.info("ReadPartition Query — sending all query replies"); log.info("ReadPartition Query — sending all query replies");
graphicsPlane.clear();
gocaDecoder.resetDefaults();
sendAllQueryReplies(); sendAllQueryReplies();
break; break;
case SF_RP_QLIST: case SF_RP_QLIST:
@@ -197,7 +197,7 @@ public class QueryReplyBuilder {
case QR_RPQ_NAMES: case QR_RPQ_NAMES:
case QR_RPQNAMES: case QR_RPQNAMES:
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); appendQueryReply(out, code, buildRpqNames());
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -54,6 +54,7 @@ public final class GocaConstants {
public static final int G_GSVW = 0x27; // Set Viewing Window public static final int G_GSVW = 0x27; // Set Viewing Window
public static final int G_GSPT = 0x28; // Set Pattern Symbol public static final int G_GSPT = 0x28; // Set Pattern Symbol
public static final int G_GSMT = 0x29; // Set Marker Symbol / Type public static final int G_GSMT = 0x29; // Set Marker Symbol / Type
public static final int G_GCALL = 0x2A; // Call Segment
public static final int G_GSCH = 0x33; // Set Character Cell public static final int G_GSCH = 0x33; // Set Character Cell
public static final int G_GSCA = 0x34; // Set Character Angle public static final int G_GSCA = 0x34; // Set Character Angle
public static final int G_GSCR = 0x35; // Set Character Shear public static final int G_GSCR = 0x35; // Set Character Shear
@@ -176,6 +177,9 @@ public final class GocaConstants {
* Returns Green (0xFF00FF00) if the index is out of range. * Returns Green (0xFF00FF00) if the index is out of range.
*/ */
public static int getGocaColorArgb(int colorIndex) { public static int getGocaColorArgb(int colorIndex) {
if (colorIndex == 0xFF) {
return GOCA_COLORS[7];
}
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) { if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
return GOCA_COLORS[colorIndex]; return GOCA_COLORS[colorIndex];
} }
@@ -20,6 +20,8 @@ public class GocaDecoder {
private int curX = 0; private int curX = 0;
private int curY = 0; private int curY = 0;
private int curColor = GocaConstants.GOCA_COLORS[0]; private int curColor = GocaConstants.GOCA_COLORS[0];
private int bgMix = 2; // BMX_OVERPAINT default
private int bgColor = GocaConstants.GOCA_COLORS[8]; // Black
private int lineType = GocaConstants.LT_SOLID; private int lineType = GocaConstants.LT_SOLID;
private int lineWidth = GocaConstants.LW_NORMAL; private int lineWidth = GocaConstants.LW_NORMAL;
private int markerType = GocaConstants.MK_PLUS; private int markerType = GocaConstants.MK_PLUS;
@@ -42,6 +44,7 @@ public class GocaDecoder {
// Area accumulation // Area accumulation
private boolean inArea = false; private boolean inArea = false;
private boolean areaDrawBoundary = true; private boolean areaDrawBoundary = true;
private boolean areaFill = true;
private final List<Integer> areaPointsX = new ArrayList<>(); private final List<Integer> areaPointsX = new ArrayList<>();
private final List<Integer> areaPointsY = new ArrayList<>(); private final List<Integer> areaPointsY = new ArrayList<>();
@@ -53,6 +56,18 @@ public class GocaDecoder {
private int imgHeight = 0; private int imgHeight = 0;
private final List<Byte> imgBuffer = new ArrayList<>(); private final List<Byte> imgBuffer = new ArrayList<>();
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
private final java.util.Map<Integer, byte[]> segmentStore = new java.util.HashMap<>();
private final java.util.Map<Integer, Integer> segmentChainMap = new java.util.HashMap<>();
private final java.util.List<Integer> segmentOrderList = new java.util.ArrayList<>();
private final java.util.Set<Integer> chainedTargets = new java.util.HashSet<>();
private int callDepth = 0;
// Graphic Cursor (Light-Pen) state
private boolean graphicsCursorActive = false;
private int graphicCursorX = 0;
private int graphicCursorY = 0;
public GocaDecoder(GraphicsPlane plane) { public GocaDecoder(GraphicsPlane plane) {
this.plane = plane; this.plane = plane;
} }
@@ -73,14 +88,51 @@ public class GocaDecoder {
return curY; return curY;
} }
public synchronized boolean isGraphicsCursorActive() {
return graphicsCursorActive;
}
public synchronized void setGraphicsCursorActive(boolean active) {
this.graphicsCursorActive = active;
}
public synchronized int getGraphicCursorX() {
return graphicCursorX;
}
public synchronized int getGraphicCursorY() {
return graphicCursorY;
}
public synchronized void setGraphicCursorPosition(int x, int y) {
this.graphicCursorX = x;
this.graphicCursorY = y;
}
public synchronized void setGraphicCursorFromPixel(int px, int py) {
if (plane != null) {
this.graphicCursorX = plane.unmapX(px);
this.graphicCursorY = plane.unmapY(py);
}
}
public synchronized void resetDefaults() { public synchronized void resetDefaults() {
curX = 0; curX = 0;
curY = 0; curY = 0;
graphicsCursorActive = false;
graphicCursorX = 0;
graphicCursorY = 0;
segmentStore.clear();
segmentChainMap.clear();
segmentOrderList.clear();
chainedTargets.clear();
resetAttributes(); resetAttributes();
} }
public synchronized void resetAttributes() { public synchronized void resetAttributes() {
curColor = getColor(0); curColor = getColor(0);
bgMix = 2; // BMX_OVERPAINT
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID; lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL; lineWidth = GocaConstants.LW_NORMAL;
markerType = GocaConstants.MK_PLUS; markerType = GocaConstants.MK_PLUS;
@@ -92,6 +144,8 @@ public class GocaDecoder {
charAngle = 0.0; charAngle = 0.0;
charSet = 0; charSet = 0;
inArea = false; inArea = false;
areaDrawBoundary = true;
areaFill = true;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
inImage = false; inImage = false;
@@ -115,14 +169,14 @@ public class GocaDecoder {
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW || order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW ||
order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW || order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW ||
order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT ||
order == GocaConstants.G_GSMT || order == GocaConstants.G_GSCS || order == GocaConstants.G_GSMT || order == GocaConstants.G_GSMCEL ||
order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCS || order == GocaConstants.G_GSMP ||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCC ||
order == GocaConstants.G_GBAR) { order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
return 2; return 2;
} }
if (order == GocaConstants.G_GSAP || order == GocaConstants.G_GBIMG || order == 0x91) { if (order == GocaConstants.G_GCALL) { // Call Segment (0x2A <32-bit segment ID>)
return 10; return 5;
} }
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
@@ -130,6 +184,61 @@ public class GocaDecoder {
return (data[idx + 1] & 0xFF) + 2; return (data[idx + 1] & 0xFF) + 2;
} }
private void indexSegments(byte[] data, int offset, int length) {
int idx = offset;
int end = offset + length;
while (idx < end) {
int order = data[idx] & 0xFF;
if (order == GocaConstants.G_BEGSEGM) {
int segStart = idx;
int segLen = getOrderLength(data, idx, end);
if (segLen <= 0 || idx + 5 >= end) {
break;
}
int segId = ((data[idx + 2] & 0xFF) << 24) |
((data[idx + 3] & 0xFF) << 16) |
((data[idx + 4] & 0xFF) << 8) |
(data[idx + 5] & 0xFF);
int nextId = 0;
if (segLen >= 14 && (data[idx + 1] & 0xFF) >= 12) {
nextId = ((data[idx + 10] & 0xFF) << 24) |
((data[idx + 11] & 0xFF) << 16) |
((data[idx + 12] & 0xFF) << 8) |
(data[idx + 13] & 0xFF);
}
int searchIdx = idx + segLen;
while (searchIdx < end) {
int o = data[searchIdx] & 0xFF;
int oLen = getOrderLength(data, searchIdx, end);
if (oLen <= 0) break;
if (o == GocaConstants.G_ENDSEGM) {
searchIdx += oLen;
break;
}
searchIdx += oLen;
}
int fullSegLen = searchIdx - segStart;
if (fullSegLen > 0 && segStart + fullSegLen <= end) {
byte[] segBytes = new byte[fullSegLen];
System.arraycopy(data, segStart, segBytes, 0, fullSegLen);
segmentStore.put(segId, segBytes);
segmentOrderList.add(segId);
if (nextId != 0) {
segmentChainMap.put(segId, nextId);
chainedTargets.add(nextId);
}
}
idx = searchIdx;
} else {
int oLen = getOrderLength(data, idx, end);
if (oLen <= 0) break;
idx += oLen;
}
}
}
/** /**
* Decodes a stream of GOCA drawing orders. * Decodes a stream of GOCA drawing orders.
*/ */
@@ -155,6 +264,15 @@ public class GocaDecoder {
end = offset + length; end = offset + length;
} }
if (callDepth == 0) {
indexSegments(inputData, idx, end - idx);
}
decodeStreamDirect(inputData, idx, end - idx);
}
private void decodeStreamDirect(byte[] inputData, int idx, int length) {
int end = idx + length;
while (idx < end) { while (idx < end) {
int order = inputData[idx] & 0xFF; int order = inputData[idx] & 0xFF;
int orderLen = getOrderLength(inputData, idx, end); int orderLen = getOrderLength(inputData, idx, end);
@@ -225,6 +343,22 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GCALL: { // Call Segment (0x2A)
if (callDepth < 16 && idx + 4 < end) {
int targetSegId = ((inputData[idx + 1] & 0xFF) << 24) |
((inputData[idx + 2] & 0xFF) << 16) |
((inputData[idx + 3] & 0xFF) << 8) |
(inputData[idx + 4] & 0xFF);
byte[] targetSeg = segmentStore.get(targetSegId);
if (targetSeg != null) {
callDepth++;
decodeStream(targetSeg, 0, targetSeg.length);
callDepth--;
}
}
idx += orderLen;
break;
}
case GocaConstants.G_GSCA: { // Set Character Angle (0x34) case GocaConstants.G_GSCA: { // Set Character Angle (0x34)
if (payloadLen >= 4 && idx + 5 < end) { if (payloadLen >= 4 && idx + 5 < end) {
int ax = readCoord(inputData, idx + 2); int ax = readCoord(inputData, idx + 2);
@@ -312,7 +446,6 @@ public class GocaDecoder {
} }
case 0x04: case 0x04:
case GocaConstants.G_GSMX: case GocaConstants.G_GSMX:
case GocaConstants.G_GSBMX:
case GocaConstants.G_GSFLW: case GocaConstants.G_GSFLW:
case GocaConstants.G_GSMP: case GocaConstants.G_GSMP:
case GocaConstants.G_GSCC: case GocaConstants.G_GSCC:
@@ -321,9 +454,17 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSBMX: { // Set Background Mix (0x0D)
bgMix = inputData[idx + 1] & 0xFF;
idx += orderLen;
break;
}
case GocaConstants.G_GBAR: { // Begin Area (0x68) case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = inputData[idx + 1] & 0xFF; int flags = inputData[idx + 1] & 0xFF;
beginArea((flags & 0x40) != 0); boolean drawBoundary = (flags & 0x80) != 0 || (flags == 0);
boolean fill = (flags == 0) || (flags & 0x40) != 0 ||
(pattern >= 1 && pattern <= 14);
beginArea(drawBoundary, fill);
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -426,7 +567,7 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GBIMG: { // Begin Image (0xD1) case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
if (idx + 9 < end) { if (payloadLen >= 8 && idx + 2 + payloadLen <= end) {
int x = readCoord(inputData, idx + 2); int x = readCoord(inputData, idx + 2);
int y = readCoord(inputData, idx + 4); int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6); int w = readCoord(inputData, idx + 6);
@@ -468,41 +609,57 @@ public class GocaDecoder {
int order = data[idx] & 0xFF; int order = data[idx] & 0xFF;
switch (order) { switch (order) {
case GocaConstants.P_NOP1: case GocaConstants.P_NOP1: {
case GocaConstants.P_ATTCUR: idx += (idx + 1 < end && data[idx + 1] == 0) ? 2 : 1;
case GocaConstants.P_DETCUR: break;
}
case GocaConstants.P_ATTCUR: { // 0x08: Attach Graphic Cursor
this.graphicsCursorActive = true;
idx += 2;
break;
}
case GocaConstants.P_DETCUR: { // 0x09: Detach Graphic Cursor
this.graphicsCursorActive = false;
idx += 2;
break;
}
case GocaConstants.P_STOPDR: { case GocaConstants.P_STOPDR: {
idx++; idx += 2;
break; break;
} }
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
plane.clear(); plane.clear();
resetDefaults(); resetDefaults();
idx++; idx += 2;
break; break;
} }
case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12) case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12)
int len = 12; idx += 12;
if (idx + 1 < end && data[idx + 1] != 0) {
len = (data[idx + 1] & 0xFF) + 2;
}
idx += len;
break; break;
} }
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults case GocaConstants.P_SCUDEF: { // 0x21: Drawing Process Control / Segment Execute
if (idx + 1 < end) {
int len = (data[idx + 1] & 0xFF) + 2;
idx += len;
} else {
idx++;
}
break;
}
case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position
if (idx + 5 <= end) {
this.graphicCursorX = readCoord(data, idx + 2);
this.graphicCursorY = readCoord(data, idx + 4);
}
if (idx + 1 < end) { if (idx + 1 < end) {
int len = data[idx + 1] & 0xFF; int len = data[idx + 1] & 0xFF;
if (idx + 2 + len <= end) {
decodeStream(data, idx + 2, len);
}
idx += 2 + len; idx += 2 + len;
} else { } else {
idx++; idx++;
} }
break; break;
} }
case GocaConstants.P_COMT: case GocaConstants.P_COMT: {
case GocaConstants.P_SETCUR: {
if (idx + 1 < end) { if (idx + 1 < end) {
int len = data[idx + 1] & 0xFF; int len = data[idx + 1] & 0xFF;
idx += 2 + len; idx += 2 + len;
@@ -524,9 +681,10 @@ public class GocaDecoder {
} }
} }
private void beginArea(boolean drawBoundary) { private void beginArea(boolean drawBoundary, boolean fill) {
this.inArea = true; this.inArea = true;
this.areaDrawBoundary = drawBoundary; this.areaDrawBoundary = drawBoundary;
this.areaFill = fill;
this.fillColor = this.curColor; this.fillColor = this.curColor;
this.areaPointsX.clear(); this.areaPointsX.clear();
this.areaPointsY.clear(); this.areaPointsY.clear();
@@ -547,7 +705,8 @@ public class GocaDecoder {
py[i] = plane.mapY(areaPointsY.get(i)); py[i] = plane.mapY(areaPointsY.get(i));
} }
plane.fillArea(px, py, n, fillColor, pattern, areaDrawBoundary, curColor, lineType, lineWidth); plane.fillArea(px, py, n, fillColor, areaFill ? pattern : GocaConstants.PT_EMPTY,
areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor);
inArea = false; inArea = false;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
@@ -555,8 +714,11 @@ public class GocaDecoder {
private void addAreaPoint(int x, int y) { private void addAreaPoint(int x, int y) {
if (inArea) { if (inArea) {
areaPointsX.add(x); int sz = areaPointsX.size();
areaPointsY.add(y); if (sz == 0 || areaPointsX.get(sz - 1) != x || areaPointsY.get(sz - 1) != y) {
areaPointsX.add(x);
areaPointsY.add(y);
}
} }
} }
@@ -0,0 +1,59 @@
package org.lib3270j.graphics;
/**
* Builds the 56-byte IBM 3179G / 3270G Graphic Input Structured Field.
* Used for light-pen and graphics cursor interactive input per IBM HOD / GDDM specifications.
*/
public class GraphicInputBuilder {
// 56-byte template mask from IBM Host On-Demand (HODInput.java)
private static final byte[] MASK = new byte[] {
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, 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
};
/**
* Builds the 56-byte Graphic Input Structured Field.
*
* @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
* @return 56-byte payload
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl) {
byte[] sf = new byte[MASK.length];
System.arraycopy(MASK, 0, sf, 0, MASK.length);
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
sf[25] = (byte) (gocaX & 0xFF);
// Byte 26-27: GOCA Y coordinate (signed 16-bit big-endian)
sf[26] = (byte) ((gocaY >> 8) & 0xFF);
sf[27] = (byte) (gocaY & 0xFF);
if (isMouseAction) {
sf[31] = 0x04;
sf[33] = 0x04;
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick
} else {
// Keyboard AID (Enter, PF keys)
sf[31] = 0x07;
sf[33] = 0x07;
sf[34] = (byte) 0xFF;
sf[35] = (byte) (aidCode & 0xFF);
}
return sf;
}
}
@@ -97,9 +97,14 @@ public class GraphicsPlane {
return canvasHeight; return canvasHeight;
} }
public void setScreenDimensions(int cols, int rows) { public synchronized void setScreenDimensions(int cols, int rows) {
this.screenCols = cols > 0 ? cols : 80; this.screenCols = cols > 0 ? cols : 80;
this.screenRows = rows > 0 ? rows : 24; this.screenRows = rows > 0 ? rows : 24;
int targetW = this.screenCols * 9;
int targetH = this.screenRows * 12;
if (this.canvasWidth != targetW || this.canvasHeight != targetH) {
resize(targetW, targetH);
}
} }
public int getScreenCols() { public int getScreenCols() {
@@ -124,12 +129,32 @@ public class GraphicsPlane {
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down). * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
*/ */
public int mapY(int gocaY) { public int mapY(int gocaY) {
int nominalHeight = screenRows * 16; int nominalHeight = screenRows * 12;
int yMax = (nominalHeight - 1) / 2; int yMax = (nominalHeight - 1) / 2;
int ny = yMax - gocaY; int ny = yMax - gocaY;
return (int) Math.round((double) ny * canvasHeight / nominalHeight); return (int) Math.round((double) ny * canvasHeight / nominalHeight);
} }
/**
* Maps a canvas pixel X coordinate back to GOCA signed coordinate (-xMax..+xMax).
*/
public int unmapX(int px) {
int nominalWidth = screenCols * 9;
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
int nx = (int) Math.round((double) px * nominalWidth / (canvasWidth > 0 ? canvasWidth : 1));
return nx - xMax;
}
/**
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up).
*/
public int unmapY(int py) {
int nominalHeight = screenRows * 12;
int yMax = (nominalHeight - 1) / 2;
int ny = (int) Math.round((double) py * nominalHeight / (canvasHeight > 0 ? canvasHeight : 1));
return yMax - ny;
}
/** /**
* Safely plots a pixel at (x, y). * Safely plots a pixel at (x, y).
*/ */
@@ -286,9 +311,19 @@ public class GraphicsPlane {
*/ */
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern, public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) { boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) {
fillArea(px, py, numPoints, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, 2, 0xFF000000);
}
/**
* Fills a closed polygon area with a solid color or hatching pattern and background mix.
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
if (px == null || py == null || numPoints < 3) return; if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF; int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
int bg = bgColorArgb;
if (pattern != GocaConstants.PT_EMPTY) { if (pattern != GocaConstants.PT_EMPTY) {
// Find polygon vertical bounds // Find polygon vertical bounds
@@ -323,12 +358,14 @@ public class GraphicsPlane {
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
for (int x = leftX; x <= rightX; x++) { for (int x = leftX; x <= rightX; x++) {
if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) { if (pattern == GocaConstants.PT_SOLID || pattern >= 16) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else { } else {
int b = patRows[y & 7] & 0xFF; int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) { if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else if (bgMix != 0) { // BMX_OVERPAINT (opaque background)
setPixel(x, y, bg);
} }
} }
} }
@@ -506,13 +543,14 @@ public class GraphicsPlane {
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) { public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
if (imageData == null || width <= 0 || height <= 0) return; if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF; int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF;
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) { for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) { for (int col = 0; col < width; col++) {
int bitIndex = row * width + col; int byteIdx = rowOffset + (col / 8);
int byteIdx = bitIndex / 8;
if (byteIdx < imageData.length) { if (byteIdx < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0; boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) { if (bit) {
setPixel(x + col, y + row, fgColor); setPixel(x + col, y + row, fgColor);
} }
@@ -87,8 +87,11 @@ public class ProgramSymbolManager {
} }
ProgramSymbolSet set = lcidMap[lcid]; ProgramSymbolSet set = lcidMap[lcid];
if (set == null) { if (set == null) {
for (ProgramSymbolSet s : sets) {
if (s != null && s.getLcid() == lcid) return s;
}
for (ProgramSymbolSet s : stagingSets) { for (ProgramSymbolSet s : stagingSets) {
if (s.getLcid() == lcid) return s; if (s != null && s.getLcid() == lcid) return s;
} }
} }
return set; return set;
@@ -98,18 +101,7 @@ public class ProgramSymbolManager {
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE). * Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
*/ */
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) { public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
if (lcid <= 0 || lcid >= 256) { ProgramSymbolSet set = getSymbolSet(lcid);
return null;
}
ProgramSymbolSet set = lcidMap[lcid];
if (set == null) {
for (ProgramSymbolSet s : stagingSets) {
if (s.getLcid() == lcid) {
set = s;
break;
}
}
}
if (set == null) { if (set == null) {
return null; return null;
} }
@@ -153,7 +145,7 @@ public class ProgramSymbolManager {
} }
boolean isTriplePlane = (rws >= 4 && rws <= 7); boolean isTriplePlane = (rws >= 4 && rws <= 7);
ProgramSymbolSet set = isTriplePlane ? stagingSets[setIndex] : sets[setIndex]; ProgramSymbolSet set = sets[setIndex];
int extHeaderLen = 0; int extHeaderLen = 0;
int cellWidth = defaultCellWidth; int cellWidth = defaultCellWidth;
@@ -176,7 +168,7 @@ public class ProgramSymbolManager {
} }
set.setLcid(lcid); set.setLcid(lcid);
if (!isTriplePlane && lcid > 0 && lcid < 256) { if (lcid > 0 && lcid < 256) {
lcidMap[lcid] = set; lcidMap[lcid] = set;
} }
@@ -32,11 +32,20 @@ public class InputProcessor {
} }
private org.lib3270j.graphics.GraphicsPlane graphicsPlane; private org.lib3270j.graphics.GraphicsPlane graphicsPlane;
private org.lib3270j.graphics.GocaDecoder gocaDecoder;
public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) { public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) {
this.graphicsPlane = gp; this.graphicsPlane = gp;
} }
public void setGocaDecoder(org.lib3270j.graphics.GocaDecoder gd) {
this.gocaDecoder = gd;
}
public org.lib3270j.graphics.GocaDecoder getGocaDecoder() {
return gocaDecoder;
}
public enum OiaStatus { public enum OiaStatus {
NOT_CONNECTED("OFFLINE"), NOT_CONNECTED("OFFLINE"),
X_SYSTEM("X SYSTEM"), X_SYSTEM("X SYSTEM"),
@@ -159,7 +168,11 @@ public class InputProcessor {
* Send an AID key (Enter, PF1-24, PA1-3, Clear). * Send an AID key (Enter, PF1-24, PA1-3, Clear).
*/ */
public void sendAid(int aidCode) { public void sendAid(int aidCode) {
if (keyboardLocked && aidCode != AID_CLEAR) return; System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
if (keyboardLocked && aidCode != AID_CLEAR) {
System.err.println("Keyboard locked, dropping AID");
return;
}
lastAid = aidCode; lastAid = aidCode;
setKeyboardLocked(true); setKeyboardLocked(true);
@@ -213,15 +226,30 @@ public class InputProcessor {
} }
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) { if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
// PA keys: send AID + cursor address only (no modified data) // PA keys: send AID + optional PID + cursor address only (no modified data)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
byte[] data = new byte[] { (byte) aidCode, caddr[0], caddr[1] }; out.write(caddr[0] & 0xFF);
sendAidResponse(data); out.write(caddr[1] & 0xFF);
sendAidResponse(out.toByteArray());
return; return;
} }
// Enter, PF keys: send AID + cursor address + modified field data // Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
ByteArrayOutputStream out = new ByteArrayOutputStream(); 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); out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
@@ -267,28 +295,16 @@ public class InputProcessor {
} }
} }
} else { } else {
// Unformatted screen in 3270 mode (e.g. line-mode console): // Unformatted screen in 3270 mode:
// Send AID + cursor address + only the active input line (row containing cursor) // Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line
int cols = screen.getCols(); // or we can just send everything up to the last non-null on the screen.
int curAddr = screen.getCursorAddress(); // IBM spec: "all alphanumeric characters... Nulls are suppressed."
int row = curAddr / cols; // Actually, the simplest is to send everything, but suppress nulls.
int rowStart = row * cols; int size = screen.getRows() * screen.getCols();
int rowEnd = rowStart + cols; for (int i = 0; i < size; i++) {
int b = screen.getCell(i).ec & 0xFF;
// Find last non-null, non-blank character on the current line if (b != 0x00) {
int lastChar = rowStart - 1; out.write(b);
for (int i = rowEnd - 1; i >= rowStart; i--) {
int ec = screen.getCell(i).ec & 0xFF;
if (ec != 0x00 && ec != 0x40) {
lastChar = i;
break;
}
}
if (lastChar >= rowStart) {
for (int i = rowStart; i <= lastChar; i++) {
int b = screen.getCell(i).ec & 0xFF;
out.write(b != 0 ? b : 0x40);
} }
} }
} }
@@ -304,6 +320,36 @@ public class InputProcessor {
} }
} }
/**
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G.
*/
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
if (fsm == null || !fsm.getConnectionState().isFullSession()) {
return;
}
if (isKeyboardLocked()) {
return;
}
setKeyboardLocked(true);
ByteArrayOutputStream out = new ByteArrayOutputStream();
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);
out.write(AID_SF);
try {
out.write(sf);
} catch (java.io.IOException ignored) {}
}
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());
}
// ========== Cursor movement ========== // ========== Cursor movement ==========
public void cursorUp() { public void cursorUp() {
@@ -76,6 +76,7 @@ public final class TelnetConstants {
case TELOPT_BINARY: return "BINARY"; case TELOPT_BINARY: return "BINARY";
case TELOPT_ECHO: return "ECHO"; case TELOPT_ECHO: return "ECHO";
case TELOPT_SGA: return "SGA"; case TELOPT_SGA: return "SGA";
case TELOPT_TM: return "TIMING-MARK";
case TELOPT_TTYPE: return "TTYPE"; case TELOPT_TTYPE: return "TTYPE";
case TELOPT_EOR: return "EOR"; case TELOPT_EOR: return "EOR";
case TELOPT_NAWS: return "NAWS"; case TELOPT_NAWS: return "NAWS";
@@ -24,6 +24,9 @@ public class ScreenBuffer {
private boolean screenAlt; // Using alternate screen? private boolean screenAlt; // Using alternate screen?
private boolean formatted; // Screen has at least one field attribute? private boolean formatted; // Screen has at least one field attribute?
private byte replyMode = SF_SRM_FIELD; private byte replyMode = SF_SRM_FIELD;
private int activePartition = 0; // 0 = implicit partition
private boolean explicitPartitionActive = false;
// Change tracking // Change tracking
private boolean screenChanged; private boolean screenChanged;
@@ -167,6 +170,10 @@ public class ScreenBuffer {
public byte getReplyMode() { return replyMode; } public byte getReplyMode() { return replyMode; }
public void setReplyMode(byte mode) { this.replyMode = mode; } public void setReplyMode(byte mode) { this.replyMode = mode; }
public int getActivePartition() { return activePartition; }
public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = true; }
public boolean isExplicitPartitionActive() { return explicitPartitionActive; }
// ========== Screen erase ========== // ========== Screen erase ==========
@@ -187,6 +194,19 @@ public class ScreenBuffer {
updateDisplaySnapshot(); updateDisplaySnapshot();
} }
public void setFieldAttribute(int pos, byte fa) {
ExtendedAttribute ea = buffer[pos];
ea.clear();
ea.fa = fa;
if (!formatted) {
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
}
formatted = true;
screenChanged = true;
// The display logic needs to render this attribute character
ea.ec = 0x00; // Typically null character for the attribute space itself
}
/** Clear the entire buffer. */ /** Clear the entire buffer. */
public synchronized void clear() { public synchronized void clear() {
for (ExtendedAttribute ea : buffer) { for (ExtendedAttribute ea : buffer) {
@@ -195,6 +215,9 @@ public class ScreenBuffer {
cursorAddress = 0; cursorAddress = 0;
bufferAddress = 0; bufferAddress = 0;
formatted = false; formatted = false;
replyMode = SF_SRM_FIELD;
activePartition = 0;
explicitPartitionActive = false;
screenChanged = true; screenChanged = true;
defaultFg = 0x00; defaultFg = 0x00;
@@ -8,6 +8,7 @@ import org.lib3270j.listener.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -59,6 +60,32 @@ public class TelnetFSM {
private int responseRequired = RSF_NO_RESPONSE; private int responseRequired = RSF_NO_RESPONSE;
private boolean deferredWillTtype; private boolean deferredWillTtype;
private boolean tn3270eDeviceTypeSent; private boolean tn3270eDeviceTypeSent;
private int ttypeIndex = 0;
private List<String> getCandidateTerminalTypes() {
List<String> list = new ArrayList<>();
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
list.add(config.getTerminalName().trim());
return list;
}
TerminalModel model = config.getModel();
list.add(model.getTerminalType());
list.add(model.getBaseTerminalType());
if (model.isColor()) {
try {
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
list.add(mono.getTerminalType());
list.add(mono.getBaseTerminalType());
} catch (Exception ignored) {}
}
if (model.getModelNumber() != 2) {
list.add("IBM-3279-2-E");
list.add("IBM-3279-2");
list.add("IBM-3278-2");
}
list.add("UNKNOWN");
return list;
}
// Connection references // Connection references
private TelnetConnection connection; private TelnetConnection connection;
@@ -109,6 +136,7 @@ public class TelnetFSM {
tn3270eBound = false; tn3270eBound = false;
eXmitSeq = 0; eXmitSeq = 0;
deferredWillTtype = false; deferredWillTtype = false;
ttypeIndex = 0;
ibuf.reset(); ibuf.reset();
sbbuf.reset(); sbbuf.reset();
@@ -116,6 +144,9 @@ public class TelnetFSM {
eFuncs[FUNC_BIND_IMAGE] = true; eFuncs[FUNC_BIND_IMAGE] = true;
eFuncs[FUNC_RESPONSES] = true; eFuncs[FUNC_RESPONSES] = true;
eFuncs[FUNC_SYSREQ] = true; eFuncs[FUNC_SYSREQ] = true;
eFuncs[FUNC_SNA_SENSE] = true;
eFuncs[FUNC_DATA_STREAM_CTL] = true;
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
changeState(ConnectionState.TELNET_PENDING); changeState(ConnectionState.TELNET_PENDING);
} }
@@ -240,6 +271,11 @@ public class TelnetFSM {
log.fine("RCVD GA"); log.fine("RCVD GA");
state = TNS_DATA; state = TNS_DATA;
break; break;
case AYT:
log.info("RCVD AYT - sending acknowledgment");
sendBytes(new byte[] { (byte) IAC, (byte) NOP });
state = TNS_DATA;
break;
case NOP: case NOP:
log.fine("RCVD NOP"); log.fine("RCVD NOP");
state = TNS_DATA; state = TNS_DATA;
@@ -267,6 +303,11 @@ public class TelnetFSM {
} }
break; break;
case TELOPT_TM:
// RFC 860: Timing Mark - reply with DO TM
sendCommand(DO, opt);
break;
case TELOPT_TN3270E: case TELOPT_TN3270E:
if (!config.isTn3270eEnabled()) { if (!config.isTn3270eEnabled()) {
sendCommand(DONT, opt); sendCommand(DONT, opt);
@@ -310,6 +351,11 @@ public class TelnetFSM {
} }
break; break;
case TELOPT_TM:
// RFC 860: Timing Mark - reply with WILL TM
sendCommand(WILL, opt);
break;
case TELOPT_TTYPE: case TELOPT_TTYPE:
if (!myOpts[opt]) { if (!myOpts[opt]) {
myOpts[opt] = true; myOpts[opt] = true;
@@ -426,8 +472,12 @@ public class TelnetFSM {
private void handleTTypeSB(byte[] data) { private void handleTTypeSB(byte[] data) {
if (data.length >= 2 && data[1] == TELQUAL_SEND) { if (data.length >= 2 && data[1] == TELQUAL_SEND) {
// Host asks for terminal type // Host asks for terminal type — cycle per RFC 1091
String termType = config.getEffectiveTerminalType(); List<String> candidates = getCandidateTerminalTypes();
String termType = candidates.get(Math.min(ttypeIndex, candidates.size() - 1));
if (ttypeIndex < candidates.size() - 1) {
ttypeIndex++;
}
log.info("RCVD SB TTYPE SEND - Responding with: " + termType); log.info("RCVD SB TTYPE SEND - Responding with: " + termType);
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -538,7 +588,7 @@ public class TelnetFSM {
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) { if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
// Try fallback model 2 if we were requesting something else // Try fallback model 2 if we were requesting something else
if (config.getModel() != TerminalModel.IBM_3278_2 && if (config.getModel() != TerminalModel.IBM_3278_2 &&
config.getModel() != TerminalModel.IBM_3279_2) { config.getModel() != TerminalModel.IBM_3279_4) {
log.warning("TN3270E device-type rejected (" + log.warning("TN3270E device-type rejected (" +
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2"); TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
config.setModel(TerminalModel.IBM_3278_2); config.setModel(TerminalModel.IBM_3278_2);
@@ -880,6 +930,23 @@ public class TelnetFSM {
tn3270eSubmode = TN3270ESubmode.E_NVT; tn3270eSubmode = TN3270ESubmode.E_NVT;
break; break;
case DT_REQUEST:
log.info("Received DT_REQUEST requestFlag=" + requestFlag);
if ((requestFlag & RQF_KEYBOARD_RESTORE) != 0) {
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
dsProcessor.getInputProcessor().setKeyboardLocked(false);
}
}
if ((requestFlag & RQF_SIGNAL) != 0) {
for (ScreenUpdateListener l : screenListeners) {
l.onSoundAlarm();
}
}
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
break;
case DT_RESPONSE: case DT_RESPONSE:
log.fine("Received response, seq=" + seqNumber); log.fine("Received response, seq=" + seqNumber);
break; break;
@@ -903,7 +970,7 @@ public class TelnetFSM {
} }
} }
private void sendTN3270EPositiveResponse(int seqNumber) { public void sendTN3270EPositiveResponse(int seqNumber) {
byte[] resp = new byte[EH_SIZE + 1]; byte[] resp = new byte[EH_SIZE + 1];
resp[0] = (byte) DT_RESPONSE; resp[0] = (byte) DT_RESPONSE;
resp[1] = 0; resp[1] = 0;
@@ -915,6 +982,31 @@ public class TelnetFSM {
sendRecord(resp); sendRecord(resp);
} }
public void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
byte[] resp = new byte[EH_SIZE + 1];
resp[0] = (byte) DT_RESPONSE;
resp[1] = 0;
resp[2] = (byte) RSF_NEGATIVE_RESPONSE;
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
resp[4] = (byte) (seqNumber & 0xFF);
resp[5] = (byte) (negCode & 0xFF);
sendRecord(resp);
}
public void sendTN3270ESnaSenseResponse(int seqNumber, int sense1, int sense2) {
byte[] resp = new byte[EH_SIZE + 2];
resp[0] = (byte) DT_RESPONSE;
resp[1] = 0;
resp[2] = (byte) RSF_SNA_SENSE;
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
resp[4] = (byte) (seqNumber & 0xFF);
resp[5] = (byte) (sense1 & 0xFF);
resp[6] = (byte) (sense2 & 0xFF);
sendRecord(resp);
}
// ========== Check if we should transition to 3270 mode ========== // ========== Check if we should transition to 3270 mode ==========
private void checkIn3270() { private void checkIn3270() {
@@ -232,4 +232,30 @@ public class QueryReplyBuilderTest {
assertEquals(0x07, replies[descOffset + 49] & 0xFF); assertEquals(0x07, replies[descOffset + 49] & 0xFF);
assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF); assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF);
} }
@Test
public void testQueryReplyImageAndRpqNames() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_RPQNAMES, (byte) QR_GIMAGE };
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
assertNotNull(replies);
assertEquals((byte) AID_SF, replies[0]);
// First SF should be QR_NULL (0xFF) because 0x82 is 3270 Image SF (not supported on 3179G)
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF
// Second SF should be QR_RPQNAMES (0xA1) matching requested code
int pos2 = 1 + len1;
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
assertEquals(QR_RPQNAMES, replies[pos2 + 3] & 0xFF); // 0xA1
// Third SF should be QR_GIMAGE (0xB1)
int pos3 = pos2 + len2;
assertEquals(0x81, replies[pos3 + 2] & 0xFF);
assertEquals(QR_GIMAGE, replies[pos3 + 3] & 0xFF); // 0xB1
}
} }
@@ -158,6 +158,7 @@ public class GocaDecoderTest {
byte[] stream = out.toByteArray(); byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length); decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent()); assertTrue(plane.hasContent());
} }
@@ -203,7 +204,7 @@ public class GocaDecoderTest {
// 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10) // 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10)
out.write(GocaConstants.G_GCRLIN); out.write(GocaConstants.G_GCRLIN);
out.write(0x04); // len = 4 (2 steps) out.write(0x04); // len = 4 (2 deltas * 2 bytes)
out.write(0x0A); out.write(0x14); // dx = +10, dy = +20 out.write(0x0A); out.write(0x14); // dx = +10, dy = +20
out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10 out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10
@@ -217,21 +218,74 @@ public class GocaDecoderTest {
@Test @Test
public void test3179GCoordinateMapping() { public void test3179GCoordinateMapping() {
GraphicsPlane plane = new GraphicsPlane(720, 688); GraphicsPlane plane = new GraphicsPlane(720, 516);
plane.setScreenDimensions(80, 43); plane.setScreenDimensions(80, 43);
// Screen center (0, 0) should map to canvas center (360, 343) // Screen center (0, 0) should map to canvas center (360, 257)
assertEquals(360, plane.mapX(0)); assertEquals(360, plane.mapX(0));
assertEquals(343, plane.mapY(0)); assertEquals(257, plane.mapY(0));
// Left edge (-360) should map to 0 // Left edge (-360) should map to 0
assertEquals(0, plane.mapX(-360)); assertEquals(0, plane.mapX(-360));
// Right edge (+359) should map to 719 // Right edge (+359) should map to 719
assertEquals(719, plane.mapX(359)); assertEquals(719, plane.mapX(359));
// Top edge (+343) should map to 0 // Top edge (+257) should map to 0
assertEquals(0, plane.mapY(343)); assertEquals(0, plane.mapY(257));
// Bottom edge (-344) should map to 687 // Bottom edge (-258) should map to 515
assertEquals(687, plane.mapY(-344)); assertEquals(515, plane.mapY(-258));
}
@Test
public void testImageOrdersGbimgGimdGeimg() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// G_GBIMG (0xD1): Begin Image: order, len=8, x=100, y=100, w=16, h=2
out.write(GocaConstants.G_GBIMG);
out.write(0x08);
out.write(0x00); out.write(0x64); // x = 100
out.write(0x00); out.write(0x64); // y = 100
out.write(0x00); out.write(0x10); // w = 16
out.write(0x00); out.write(0x02); // h = 2
// G_GIMD (0x92): Image Data: order, len=4, 4 bytes of bitmap (16x2 pixels = 32 bits = 4 bytes)
out.write(GocaConstants.G_GIMD);
out.write(0x04);
out.write(0xFF); out.write(0x00);
out.write(0xAA); out.write(0x55);
// G_GEIMG (0x91): End Image
out.write(GocaConstants.G_GEIMG);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
}
@Test
public void testDirectObjectControlSf24() {
org.lib3270j.screen.ScreenBuffer sb = new org.lib3270j.screen.ScreenBuffer(
org.lib3270j.TerminalModel.IBM_3279_4, new org.lib3270j.charset.EbcdicTranslator());
org.lib3270j.datastream.DataStreamProcessor dsp = new org.lib3270j.datastream.DataStreamProcessor(
sb, new org.lib3270j.charset.EbcdicTranslator());
// First draw something on graphics plane
dsp.getGraphicsPlane().setPixel(10, 10, 0xFFFFFFFF);
assertTrue(dsp.getGraphicsPlane().hasContent());
// Send WSF with SF_OBJCNTL (0x24) containing P_ERASE (0x0A)
byte[] wsf = new byte[] {
(byte) org.lib3270j.protocol.DS3270Constants.CMD_WSF,
0x00, 0x04, // Field length = 4
(byte) org.lib3270j.graphics.GocaConstants.SF_OBJCNTL, // 0x24
(byte) org.lib3270j.graphics.GocaConstants.P_ERASE // 0x0A
};
dsp.processRecord(wsf, 0, wsf.length, false);
assertFalse(dsp.getGraphicsPlane().hasContent(), "Expected graphics plane to be cleared after SF_OBJCNTL P_ERASE");
} }
} }
@@ -131,4 +131,26 @@ public class InputProcessorTest {
assertEquals(0, screen.getDisplayCell(2).ucs4); assertEquals(0, screen.getDisplayCell(2).ucs4);
assertEquals('A', screen.getDisplayCell(1).ucs4); assertEquals('A', screen.getDisplayCell(1).ucs4);
} }
@Test
public void testGraphicInputBuilderStructure() {
byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
assertEquals(56, sf.length);
assertEquals(0x00, sf[0]);
assertEquals(0x34, sf[1]); // Length = 52
assertEquals(0x0F, sf[2]); // SF ID = 0x0F
assertEquals(0x0F, sf[3]); // SF ID = 0x0F
// Coordinates
int x = (sf[24] << 8) | (sf[25] & 0xFF);
int y = (sf[26] << 8) | (sf[27] & 0xFF);
assertEquals(100, (short) x);
assertEquals(-50, (short) y);
// Keyboard AID
assertEquals(0x07, sf[31]);
assertEquals(0x07, sf[33]);
assertEquals((byte) 0xFF, sf[34]);
assertEquals((byte) AID_ENTER, sf[35]);
}
} }
@@ -219,4 +219,79 @@ public class TelnetFSMTest {
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(3))); assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
assertEquals('O', trans.ebcdicToUnicode(screenBuffer.getCellEC(4))); assertEquals('O', trans.ebcdicToUnicode(screenBuffer.getCellEC(4)));
} }
@Test
public void testTelnetTimingMarkOption6() {
fsm.onConnected();
connection.sentData.clear();
// Host sends DO TELOPT_TM (DO 6) -> Client must reply WILL TELOPT_TM (WILL 6) per RFC 860
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TM);
boolean foundWillTm = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.WILL && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TM) {
foundWillTm = true;
}
}
assertTrue(foundWillTm, "Expected IAC WILL TELOPT_TM reply upon receiving IAC DO TELOPT_TM");
// Host sends WILL TELOPT_TM (WILL 6) -> Client must reply DO TELOPT_TM (DO 6)
connection.sentData.clear();
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TM);
boolean foundDoTm = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.DO && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TM) {
foundDoTm = true;
}
}
assertTrue(foundDoTm, "Expected IAC DO TELOPT_TM reply upon receiving IAC WILL TELOPT_TM");
}
@Test
public void testIACAYT() {
fsm.onConnected();
connection.sentData.clear();
// Host sends IAC AYT
feedBytes(TelnetConstants.IAC, TelnetConstants.AYT);
boolean foundNop = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 2 && (pkt[0] & 0xFF) == TelnetConstants.IAC && (pkt[1] & 0xFF) == TelnetConstants.NOP) {
foundNop = true;
}
}
assertTrue(foundNop, "Expected IAC NOP acknowledgment for IAC AYT");
}
@Test
public void testTTypeCycling() {
fsm.onConnected();
connection.sentData.clear();
// Enable TTYPE
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
// First SEND request -> Expect configured type IBM-3279-4-E
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
byte[] lastPkt = connection.sentData.get(connection.sentData.size() - 1);
String resp1 = new String(lastPkt, 4, lastPkt.length - 6);
assertEquals("IBM-3279-4-E", resp1);
// Second SEND request -> Expect fallback IBM-3279-4
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
lastPkt = connection.sentData.get(connection.sentData.size() - 1);
String resp2 = new String(lastPkt, 4, lastPkt.length - 6);
assertEquals("IBM-3279-4", resp2);
// Third SEND request -> Expect monochrome fallback IBM-3278-4-E
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
lastPkt = connection.sentData.get(connection.sentData.size() - 1);
String resp3 = new String(lastPkt, 4, lastPkt.length - 6);
assertEquals("IBM-3278-4-E", resp3);
}
} }
@@ -31,7 +31,7 @@ public class TlsConfigTest {
assertEquals(992, c1.getPort()); assertEquals(992, c1.getPort());
// ssl: prefix with explicit port // ssl: prefix with explicit port
ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_2); ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_4);
assertTrue(c2.isUseTls()); assertTrue(c2.isUseTls());
assertEquals("zos.local", c2.getHost()); assertEquals("zos.local", c2.getHost());
assertEquals(2323, c2.getPort()); assertEquals(2323, c2.getPort());