From d9df1052a155f64f66fe3c33db66f34cfaa3c5ef Mon Sep 17 00:00:00 2001 From: Rudi Date: Mon, 24 Aug 2026 16:35:24 +0000 Subject: [PATCH] Lightpen on --- .gitignore | 3 + .../main/java/org/pubvm/j3270/J3270App.java | 22 ++++--- .../java/org/pubvm/j3270/ui/StatusBar.java | 19 +++++- .../org/pubvm/j3270/ui/TerminalPanel.java | 42 +++++++++++-- .../org/lib3270j/graphics/GocaDecoder.java | 59 +++++++++++-------- .../org/lib3270j/graphics/GraphicsPlane.java | 14 +++++ .../org/lib3270j/input/InputProcessor.java | 53 +++++++++++++++++ 7 files changed, 176 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 8da0dd3..70dbc44 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ j3270.log.* +*.txt +*.py +*.pcap build/* *.log .DS_Store diff --git a/j3270/src/main/java/org/pubvm/j3270/J3270App.java b/j3270/src/main/java/org/pubvm/j3270/J3270App.java index f2ecd39..d7c3311 100644 --- a/j3270/src/main/java/org/pubvm/j3270/J3270App.java +++ b/j3270/src/main/java/org/pubvm/j3270/J3270App.java @@ -131,6 +131,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate terminalPanel.repaint(); })); 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)); menuBar.add(actionsMenu); @@ -149,18 +153,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate return menu; } - private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) { - JMenuItem item = new JMenuItem(text); + private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) { + JMenuItem item = new JMenuItem(name); item.setBackground(new Color(40, 40, 40)); item.setForeground(new Color(200, 200, 200)); - if (acceleratorKey > 0) { - item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); + if (mnemonic > 0) { + int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier)); } item.addActionListener(e -> action.run()); return item; } + private JMenuItem createMenuItem(String name, int mnemonic, Runnable action) { + return createMenuItem(name, mnemonic, action, false); + } + private void showFileTransferDialog() { if (client == null) { 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); terminalPanel.setClient(client); - statusBar.setClient(client); + statusBar.setClient(client, terminalPanel); String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : ""; setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator); @@ -312,7 +320,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate client.disconnect(); client = null; terminalPanel.setClient(null); - statusBar.setClient(null); + statusBar.setClient(null, terminalPanel); terminalPanel.repaint(); setTitle("j3270 — Java TN3270 Terminal Emulator"); } diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java b/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java index e3a7733..b4bc6e7 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/StatusBar.java @@ -22,6 +22,7 @@ public class StatusBar extends JPanel { private final JLabel modelInfo; private Telnet3270Client client; + private TerminalPanel terminalPanel; // OIA colors 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); 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(connectionStatus); add(Box.createHorizontalStrut(10)); @@ -52,6 +66,8 @@ public class StatusBar extends JPanel { add(luName); add(Box.createHorizontalStrut(12)); add(lockStatus); + add(Box.createHorizontalStrut(12)); + add(lpButton); add(Box.createHorizontalGlue()); add(modelInfo); add(Box.createHorizontalStrut(12)); @@ -66,8 +82,9 @@ public class StatusBar extends JPanel { return label; } - public void setClient(Telnet3270Client client) { + public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) { this.client = client; + this.terminalPanel = terminalPanel; } public void updateStatus() { diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java index ccce71c..a7e6660 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java @@ -62,6 +62,9 @@ public class TerminalPanel extends JPanel { private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100); // ========== Resize guard ========== + private boolean lightPenMode = false; + private long lastGraphicsUpdateCount = -1; + private java.awt.image.BufferedImage cachedGraphicsImage = null; private boolean resizeGuard = false; /** @@ -207,6 +210,12 @@ public class TerminalPanel extends JPanel { int newAddr = selectionStartRow * sb.getCols() + selectionStartCol; sb.setCursorAddress(newAddr); clearSelection(); + if (lightPenMode) { + if (client.getInputProcessor().lightPenSelect(newAddr)) { + refreshScreen(); + return; + } + } if (client.getGocaDecoder() != null && client.getGraphicsPlane() != null) { int ox = getRenderOffsetX(); @@ -558,6 +567,7 @@ public class TerminalPanel extends JPanel { int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); 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_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN"); // Create actions for all bound keys am.put("j3270-ENTER", createAction(this::handleEnter)); @@ -586,6 +596,7 @@ public class TerminalPanel extends JPanel { // Copy/Paste actions am.put("j3270-COPY", createAction(this::copySelection)); am.put("j3270-PASTE", createAction(this::pasteClipboard)); + am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen)); for (int i = 1; i <= 24; i++) { final int pf = i; @@ -900,9 +911,13 @@ public class TerminalPanel extends JPanel { 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); + long currentUpdateCount = client.getGraphicsPlane().getUpdateCount(); + if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) { + 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) { int gocaX = client.getGocaDecoder().getGraphicCursorX(); int gocaY = client.getGocaDecoder().getGraphicCursorY(); - int px = ox + client.getGraphicsPlane().mapX(gocaX); - int py = oy + client.getGraphicsPlane().mapY(gocaY); + int canvasPx = client.getGraphicsPlane().mapX(gocaX); + 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.setXORMode(Color.BLACK); @@ -1103,4 +1124,15 @@ public class TerminalPanel extends JPanel { if (blinkTimer != null) 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; + } } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java index fda679e..858e3b8 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java @@ -829,39 +829,51 @@ public class GocaDecoder { private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) { int pos = off; - int startX = curX; - int startY = curY; + int centerX = curX; + int centerY = curY; if (!fromCurPos && pos + 4 <= off + len) { - startX = readCoord(data, pos); - startY = readCoord(data, pos + 2); + centerX = readCoord(data, pos); + centerY = readCoord(data, pos + 2); pos += 4; } - double multiplier = 1.0; - if (pos + 2 <= off + len) { - multiplier = (data[pos] & 0xFF) + ((data[pos + 1] & 0xFF) / 255.0); - if (multiplier == 0.0) multiplier = 1.0; + double sweepFraction = 1.0; + if (!isFull && pos + 2 <= off + len) { + byte intPart = data[pos]; + double fracPart = (data[pos + 1] & 0xFF) / 256.0; + sweepFraction = intPart + (intPart >= 0 ? fracPart : -fracPart); } - int dxP = Math.abs(arcParamP - arcParamR); - int dyQ = Math.abs(arcParamQ - arcParamS); - if (dxP == 0) dxP = 10; - if (dyQ == 0) dyQ = 10; + int pxCenter = plane.mapX(centerX); + int pyCenter = plane.mapY(centerY); + int pxCur = plane.mapX(curX); + int pyCur = plane.mapY(curY); - int rxVirtual = (int) (dxP * multiplier); - int ryVirtual = (int) (dyQ * multiplier); + double radius = Math.sqrt(Math.pow(pxCur - pxCenter, 2) + Math.pow(pyCur - pyCenter, 2)); + 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)); - int ry = Math.abs(plane.mapY(ryVirtual) - plane.mapY(0)); - if (rx <= 0) rx = 10; - if (ry <= 0) ry = 10; + double startAngleRad = Math.atan2(pyCenter - pyCur, pxCur - pxCenter); + double startAngleDeg = Math.toDegrees(startAngleRad); + if (startAngleDeg < 0) startAngleDeg += 360.0; + + double sweepAngleDeg = isFull ? 360.0 : (sweepFraction * 360.0); - plane.drawArc(plane.mapX(startX), plane.mapY(startY), rx, ry, 0.0, 360.0, + plane.drawArc(pxCenter, pyCenter, rx, ry, startAngleDeg, sweepAngleDeg, curColor, lineType, lineWidth, isFull); - curX = startX; - curY = startY; + // Arc ends at the end of the sweep + 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) { @@ -932,8 +944,9 @@ public class GocaDecoder { int textLen = end - pos; if (textLen <= 0) return; - int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 12; - int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 20; + // IBM 3279 vector graphics base cell is 9x12 + 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) { for (int i = 0; i < textLen; i++) { diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java index 414ccb1..2787c45 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java @@ -40,6 +40,7 @@ public class GraphicsPlane { private int canvasHeight = 600; private int[] rgbBuffer; private boolean hasContent = false; + private long updateCount = 0; private int screenCols = 80; private int screenRows = 24; @@ -79,6 +80,11 @@ public class GraphicsPlane { Arrays.fill(rgbBuffer, 0); } hasContent = false; + updateCount++; + } + + public synchronized long getUpdateCount() { + return updateCount; } public synchronized boolean hasContent() { @@ -162,6 +168,7 @@ public class GraphicsPlane { if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { rgbBuffer[y * canvasWidth + x] = colorArgb; hasContent = true; + updateCount++; } } @@ -203,6 +210,7 @@ public class GraphicsPlane { } } hasContent = true; + updateCount++; } private boolean shouldPlotLinePixel(int step, int lineType) { @@ -266,6 +274,7 @@ public class GraphicsPlane { prevY = nextY; } hasContent = true; + updateCount++; } /** @@ -304,6 +313,7 @@ public class GraphicsPlane { } } hasContent = true; + updateCount++; } /** @@ -380,6 +390,7 @@ public class GraphicsPlane { } } hasContent = true; + updateCount++; } /** @@ -445,6 +456,7 @@ public class GraphicsPlane { break; } hasContent = true; + updateCount++; } private static final int[] VSS_OFFSETS = new int[256]; @@ -493,6 +505,7 @@ public class GraphicsPlane { } } hasContent = true; + updateCount++; } private void drawVssChar(int x, int y, char c, int color, int cw, int ch) { @@ -558,5 +571,6 @@ public class GraphicsPlane { } } hasContent = true; + updateCount++; } } diff --git a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java index 801e0e2..0d4579f 100644 --- a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java @@ -352,6 +352,59 @@ public class InputProcessor { // ========== 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() { int addr = screen.getCursorAddress(); addr -= screen.getCols();