Lightpen on

This commit is contained in:
2026-08-24 16:35:24 +00:00
parent 0837de8db3
commit d9df1052a1
7 changed files with 176 additions and 36 deletions
+3
View File
@@ -1,4 +1,7 @@
j3270.log.* j3270.log.*
*.txt
*.py
*.pcap
build/* build/*
*.log *.log
.DS_Store .DS_Store
@@ -131,6 +131,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
terminalPanel.repaint(); terminalPanel.repaint();
})); }));
actionsMenu.addSeparator(); actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
terminalPanel.toggleLightPen();
}, true));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog)); actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
menuBar.add(actionsMenu); menuBar.add(actionsMenu);
@@ -149,18 +153,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return menu; return menu;
} }
private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) { private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
JMenuItem item = new JMenuItem(text); JMenuItem item = new JMenuItem(name);
item.setBackground(new Color(40, 40, 40)); item.setBackground(new Color(40, 40, 40));
item.setForeground(new Color(200, 200, 200)); item.setForeground(new Color(200, 200, 200));
if (acceleratorKey > 0) { if (mnemonic > 0) {
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey, int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
} }
item.addActionListener(e -> action.run()); item.addActionListener(e -> action.run());
return item; return item;
} }
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action) {
return createMenuItem(name, mnemonic, action, false);
}
private void showFileTransferDialog() { private void showFileTransferDialog() {
if (client == null) { if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.", JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
@@ -278,7 +286,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.addScreenUpdateListener(this); client.addScreenUpdateListener(this);
terminalPanel.setClient(client); terminalPanel.setClient(client);
statusBar.setClient(client); statusBar.setClient(client, terminalPanel);
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : ""; String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator); setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
@@ -312,7 +320,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.disconnect(); client.disconnect();
client = null; client = null;
terminalPanel.setClient(null); terminalPanel.setClient(null);
statusBar.setClient(null); statusBar.setClient(null, terminalPanel);
terminalPanel.repaint(); terminalPanel.repaint();
setTitle("j3270 — Java TN3270 Terminal Emulator"); setTitle("j3270 — Java TN3270 Terminal Emulator");
} }
@@ -22,6 +22,7 @@ public class StatusBar extends JPanel {
private final JLabel modelInfo; private final JLabel modelInfo;
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel;
// OIA colors // OIA colors
private static final Color OIA_BG = new Color(20, 20, 20); private static final Color OIA_BG = new Color(20, 20, 20);
@@ -44,6 +45,19 @@ public class StatusBar extends JPanel {
modelInfo = createLabel("", oiaFont, OIA_DIM); modelInfo = createLabel("", oiaFont, OIA_DIM);
cursorPosition = createLabel("001/001", oiaFont, OIA_FG); cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
JButton lpButton = new JButton("LightPen: OFF");
lpButton.setFont(oiaFont);
lpButton.setForeground(OIA_FG);
lpButton.setBackground(OIA_BG);
lpButton.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
lpButton.setFocusable(false);
lpButton.addActionListener(e -> {
if (terminalPanel != null) {
terminalPanel.toggleLightPen();
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
}
});
add(Box.createHorizontalStrut(6)); add(Box.createHorizontalStrut(6));
add(connectionStatus); add(connectionStatus);
add(Box.createHorizontalStrut(10)); add(Box.createHorizontalStrut(10));
@@ -52,6 +66,8 @@ public class StatusBar extends JPanel {
add(luName); add(luName);
add(Box.createHorizontalStrut(12)); add(Box.createHorizontalStrut(12));
add(lockStatus); add(lockStatus);
add(Box.createHorizontalStrut(12));
add(lpButton);
add(Box.createHorizontalGlue()); add(Box.createHorizontalGlue());
add(modelInfo); add(modelInfo);
add(Box.createHorizontalStrut(12)); add(Box.createHorizontalStrut(12));
@@ -66,8 +82,9 @@ public class StatusBar extends JPanel {
return label; return label;
} }
public void setClient(Telnet3270Client client) { public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
this.client = client; this.client = client;
this.terminalPanel = terminalPanel;
} }
public void updateStatus() { public void updateStatus() {
@@ -62,6 +62,9 @@ public class TerminalPanel extends JPanel {
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100); private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
// ========== Resize guard ========== // ========== Resize guard ==========
private boolean lightPenMode = false;
private long lastGraphicsUpdateCount = -1;
private java.awt.image.BufferedImage cachedGraphicsImage = null;
private boolean resizeGuard = false; private boolean resizeGuard = false;
/** /**
@@ -207,6 +210,12 @@ public class TerminalPanel extends JPanel {
int newAddr = selectionStartRow * sb.getCols() + selectionStartCol; int newAddr = selectionStartRow * sb.getCols() + selectionStartCol;
sb.setCursorAddress(newAddr); sb.setCursorAddress(newAddr);
clearSelection(); clearSelection();
if (lightPenMode) {
if (client.getInputProcessor().lightPenSelect(newAddr)) {
refreshScreen();
return;
}
}
if (client.getGocaDecoder() != null && client.getGraphicsPlane() != null) { if (client.getGocaDecoder() != null && client.getGraphicsPlane() != null) {
int ox = getRenderOffsetX(); int ox = getRenderOffsetX();
@@ -558,6 +567,7 @@ public class TerminalPanel extends JPanel {
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN");
// Create actions for all bound keys // Create actions for all bound keys
am.put("j3270-ENTER", createAction(this::handleEnter)); am.put("j3270-ENTER", createAction(this::handleEnter));
@@ -586,6 +596,7 @@ public class TerminalPanel extends JPanel {
// Copy/Paste actions // Copy/Paste actions
am.put("j3270-COPY", createAction(this::copySelection)); am.put("j3270-COPY", createAction(this::copySelection));
am.put("j3270-PASTE", createAction(this::pasteClipboard)); am.put("j3270-PASTE", createAction(this::pasteClipboard));
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
for (int i = 1; i <= 24; i++) { for (int i = 1; i <= 24; i++) {
final int pf = i; final int pf = i;
@@ -900,9 +911,13 @@ public class TerminalPanel extends JPanel {
int gHeight = client.getGraphicsPlane().getCanvasHeight(); int gHeight = client.getGraphicsPlane().getCanvasHeight();
int[] rgb = client.getGraphicsPlane().getRgbBuffer(); int[] rgb = client.getGraphicsPlane().getRgbBuffer();
if (rgb != null && gWidth > 0 && gHeight > 0) { 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); long currentUpdateCount = client.getGraphicsPlane().getUpdateCount();
img.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth); if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) {
g2.drawImage(img, ox, oy, gridW, gridH, null); cachedGraphicsImage = new java.awt.image.BufferedImage(gWidth, gHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB);
cachedGraphicsImage.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
lastGraphicsUpdateCount = currentUpdateCount;
}
g2.drawImage(cachedGraphicsImage, ox, oy, gridW, gridH, null);
} }
} }
@@ -1039,8 +1054,14 @@ public class TerminalPanel extends JPanel {
if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) { if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
int gocaX = client.getGocaDecoder().getGraphicCursorX(); int gocaX = client.getGocaDecoder().getGraphicCursorX();
int gocaY = client.getGocaDecoder().getGraphicCursorY(); int gocaY = client.getGocaDecoder().getGraphicCursorY();
int px = ox + client.getGraphicsPlane().mapX(gocaX); int canvasPx = client.getGraphicsPlane().mapX(gocaX);
int py = oy + client.getGraphicsPlane().mapY(gocaY); int canvasPy = client.getGraphicsPlane().mapY(gocaY);
int gridW = cols * cellWidth;
int gridH = rows * cellHeight;
int gWidth = client.getGraphicsPlane().getCanvasWidth();
int gHeight = client.getGraphicsPlane().getCanvasHeight();
int px = ox + (gWidth > 0 ? (int)Math.round((double)canvasPx * gridW / gWidth) : canvasPx);
int py = oy + (gHeight > 0 ? (int)Math.round((double)canvasPy * gridH / gHeight) : canvasPy);
g2.setColor(Color.WHITE); g2.setColor(Color.WHITE);
g2.setXORMode(Color.BLACK); g2.setXORMode(Color.BLACK);
@@ -1103,4 +1124,15 @@ public class TerminalPanel extends JPanel {
if (blinkTimer != null) if (blinkTimer != null)
blinkTimer.stop(); blinkTimer.stop();
} }
public void toggleLightPen() {
this.lightPenMode = !this.lightPenMode;
if (client != null && client.getGocaDecoder() != null) {
client.getGocaDecoder().setGraphicsCursorActive(this.lightPenMode);
repaint();
}
}
public boolean isLightPenMode() {
return this.lightPenMode;
}
} }
@@ -829,39 +829,51 @@ public class GocaDecoder {
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) { private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
int pos = off; int pos = off;
int startX = curX; int centerX = curX;
int startY = curY; int centerY = curY;
if (!fromCurPos && pos + 4 <= off + len) { if (!fromCurPos && pos + 4 <= off + len) {
startX = readCoord(data, pos); centerX = readCoord(data, pos);
startY = readCoord(data, pos + 2); centerY = readCoord(data, pos + 2);
pos += 4; pos += 4;
} }
double multiplier = 1.0; double sweepFraction = 1.0;
if (pos + 2 <= off + len) { if (!isFull && pos + 2 <= off + len) {
multiplier = (data[pos] & 0xFF) + ((data[pos + 1] & 0xFF) / 255.0); byte intPart = data[pos];
if (multiplier == 0.0) multiplier = 1.0; double fracPart = (data[pos + 1] & 0xFF) / 256.0;
sweepFraction = intPart + (intPart >= 0 ? fracPart : -fracPart);
} }
int dxP = Math.abs(arcParamP - arcParamR); int pxCenter = plane.mapX(centerX);
int dyQ = Math.abs(arcParamQ - arcParamS); int pyCenter = plane.mapY(centerY);
if (dxP == 0) dxP = 10; int pxCur = plane.mapX(curX);
if (dyQ == 0) dyQ = 10; int pyCur = plane.mapY(curY);
int rxVirtual = (int) (dxP * multiplier); double radius = Math.sqrt(Math.pow(pxCur - pxCenter, 2) + Math.pow(pyCur - pyCenter, 2));
int ryVirtual = (int) (dyQ * multiplier); int rx = (int) Math.round(radius);
int ry = rx;
if (rx <= 0) {
rx = 10;
ry = 10;
}
int rx = Math.abs(plane.mapX(rxVirtual) - plane.mapX(0)); double startAngleRad = Math.atan2(pyCenter - pyCur, pxCur - pxCenter);
int ry = Math.abs(plane.mapY(ryVirtual) - plane.mapY(0)); double startAngleDeg = Math.toDegrees(startAngleRad);
if (rx <= 0) rx = 10; if (startAngleDeg < 0) startAngleDeg += 360.0;
if (ry <= 0) ry = 10;
plane.drawArc(plane.mapX(startX), plane.mapY(startY), rx, ry, 0.0, 360.0, double sweepAngleDeg = isFull ? 360.0 : (sweepFraction * 360.0);
plane.drawArc(pxCenter, pyCenter, rx, ry, startAngleDeg, sweepAngleDeg,
curColor, lineType, lineWidth, isFull); curColor, lineType, lineWidth, isFull);
curX = startX; // Arc ends at the end of the sweep
curY = startY; double endAngleRad = Math.toRadians(startAngleDeg + sweepAngleDeg);
int pxEnd = (int) Math.round(pxCenter + rx * Math.cos(endAngleRad));
int pyEnd = (int) Math.round(pyCenter - ry * Math.sin(endAngleRad));
curX = plane.unmapX(pxEnd);
curY = plane.unmapY(pyEnd);
} }
private void processFillet(byte[] data, int off, int len, boolean fromCurPos) { private void processFillet(byte[] data, int off, int len, boolean fromCurPos) {
@@ -932,8 +944,9 @@ public class GocaDecoder {
int textLen = end - pos; int textLen = end - pos;
if (textLen <= 0) return; if (textLen <= 0) return;
int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 12; // IBM 3279 vector graphics base cell is 9x12
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 20; 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;
if (charSet != 0 && programSymbolManager != null) { if (charSet != 0 && programSymbolManager != null) {
for (int i = 0; i < textLen; i++) { for (int i = 0; i < textLen; i++) {
@@ -40,6 +40,7 @@ public class GraphicsPlane {
private int canvasHeight = 600; private int canvasHeight = 600;
private int[] rgbBuffer; private int[] rgbBuffer;
private boolean hasContent = false; private boolean hasContent = false;
private long updateCount = 0;
private int screenCols = 80; private int screenCols = 80;
private int screenRows = 24; private int screenRows = 24;
@@ -79,6 +80,11 @@ public class GraphicsPlane {
Arrays.fill(rgbBuffer, 0); Arrays.fill(rgbBuffer, 0);
} }
hasContent = false; hasContent = false;
updateCount++;
}
public synchronized long getUpdateCount() {
return updateCount;
} }
public synchronized boolean hasContent() { public synchronized boolean hasContent() {
@@ -162,6 +168,7 @@ public class GraphicsPlane {
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
rgbBuffer[y * canvasWidth + x] = colorArgb; rgbBuffer[y * canvasWidth + x] = colorArgb;
hasContent = true; hasContent = true;
updateCount++;
} }
} }
@@ -203,6 +210,7 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
private boolean shouldPlotLinePixel(int step, int lineType) { private boolean shouldPlotLinePixel(int step, int lineType) {
@@ -266,6 +274,7 @@ public class GraphicsPlane {
prevY = nextY; prevY = nextY;
} }
hasContent = true; hasContent = true;
updateCount++;
} }
/** /**
@@ -304,6 +313,7 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
/** /**
@@ -380,6 +390,7 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
/** /**
@@ -445,6 +456,7 @@ public class GraphicsPlane {
break; break;
} }
hasContent = true; hasContent = true;
updateCount++;
} }
private static final int[] VSS_OFFSETS = new int[256]; private static final int[] VSS_OFFSETS = new int[256];
@@ -493,6 +505,7 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
private void drawVssChar(int x, int y, char c, int color, int cw, int ch) { private void drawVssChar(int x, int y, char c, int color, int cw, int ch) {
@@ -558,5 +571,6 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
} }
@@ -352,6 +352,59 @@ public class InputProcessor {
// ========== Cursor movement ========== // ========== Cursor movement ==========
/**
* Simulate a Text Light Pen selection.
*/
public boolean lightPenSelect(int address) {
if (screen == null || !screen.isFormatted()) {
return false;
}
int size = screen.getRows() * screen.getCols();
int faPos = screen.findFieldAttribute(address);
if (faPos < 0) {
return false;
}
ExtendedAttribute faCell = screen.getCell(faPos);
int fa = faCell.fa & 0xFF;
if (!org.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
return false;
}
int designatorPos = (faPos + 1) % size;
ExtendedAttribute desCell = screen.getCell(designatorPos);
int ebcdic = desCell.ec & 0xFF;
char ascii = (char) desCell.ucs4;
screen.setCursorAddress(designatorPos);
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || (ebcdic != 0x50 && ascii != '&' && ebcdic != 0x6F && ascii != '?' && ebcdic != 0x6E && ascii != '>')) {
faCell.fa |= org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(org.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
} else if (ebcdic == 0x50 || ascii == '&') {
faCell.fa |= org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER);
return true;
} else if (ebcdic == 0x6F || ascii == '?') {
desCell.ec = (byte) 0x6E;
desCell.ucs4 = '>';
faCell.fa |= org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.updateDisplaySnapshot();
return true;
} else if (ebcdic == 0x6E || ascii == '>') {
desCell.ec = (byte) 0x6F;
desCell.ucs4 = '?';
faCell.fa &= ~org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.updateDisplaySnapshot();
return true;
} else {
faCell.fa |= org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(org.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
}
}
public void cursorUp() { public void cursorUp() {
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
addr -= screen.getCols(); addr -= screen.getCols();