Add tn3270(no e) and fix animations
Release j3270 / Build & Publish Release (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m10s

This commit is contained in:
2026-08-21 16:32:48 -04:00
parent bf5e4410e6
commit 1212b2cceb
31 changed files with 698 additions and 198 deletions
@@ -34,6 +34,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private int lastPort = 23;
private FileTransfer fileTransfer;
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator");
@@ -363,20 +364,23 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
@Override
public void onScreenUpdated() {
SwingUtilities.invokeLater(() -> {
// During an active file transfer, let the CUT/DFT handler drive
// keyboard state. In x3270, ft_cut_data() runs before WCC
// keyboard-restore is applied — the keyboard stays locked for the
// entire CUT transfer.
if (fileTransfer != null) {
fileTransfer.onScreenUpdated();
}
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
client.getInputProcessor().setKeyboardLocked(false);
}
terminalPanel.repaint();
statusBar.updateStatus();
});
if (screenUpdatePending.compareAndSet(false, true)) {
SwingUtilities.invokeLater(() -> {
screenUpdatePending.set(false);
// During an active file transfer, let the CUT/DFT handler drive
// keyboard state. In x3270, ft_cut_data() runs before WCC
// keyboard-restore is applied — the keyboard stays locked for the
// entire CUT transfer.
if (fileTransfer != null) {
fileTransfer.onScreenUpdated();
}
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
client.getInputProcessor().setKeyboardLocked(false);
}
terminalPanel.repaint();
statusBar.updateStatus();
});
}
}
@Override
@@ -442,6 +446,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
boolean debug = false;
boolean cliTls = false;
boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null;
org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
@@ -452,6 +457,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
cliTls = true;
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(args[i]) || "--plain-tn3270".equals(args[i]) || "--plain".equals(args[i]) || "-P".equals(args[i]) || "-p".equals(args[i]) || "--non-e".equals(args[i])) {
cliTn3270e = false;
} else if ("--tn3270e".equals(args[i])) {
cliTn3270e = true;
} else if (args[i].startsWith("--graphics=")) {
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11));
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
@@ -529,6 +538,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final boolean finalTls = cliTls;
final boolean finalNoVerify = cliNoVerifyCert;
final Boolean finalTn3270e = cliTn3270e;
final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
SwingUtilities.invokeLater(() -> {
@@ -560,6 +570,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (finalNoVerify) {
config.setTlsVerifyCert(false);
}
if (finalTn3270e != null) {
config.setTn3270eEnabled(finalTn3270e);
}
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
@@ -578,9 +591,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls();
boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert();
boolean tn3270e = org.pubvm.j3270.config.Settings.getAutoConnectTn3270e();
if (host != null && !host.isEmpty()) {
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
@@ -78,13 +78,21 @@ public class Settings {
prefs.putBoolean("autoConnectVerifyCert", verify);
}
public static boolean getAutoConnectTn3270e() {
return prefs.getBoolean("autoConnectTn3270e", true);
}
public static void setAutoConnectTn3270e(boolean tn3270e) {
prefs.putBoolean("autoConnectTn3270e", tn3270e);
}
public static org.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.NONE.name());
String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.BOTH.name());
return org.lib3270j.graphics.GraphicsMode.fromString(modeStr);
}
public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.NONE).name());
prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.BOTH).name());
}
public static Color getColorOverride(int index, Color defaultColor) {
@@ -243,6 +251,11 @@ public class Settings {
case "tlsVerifyCert":
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
break;
case "autoConnectTn3270e":
case "tn3270e":
case "enableTn3270e":
setAutoConnectTn3270e(Boolean.parseBoolean(value));
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
default:
log.warning("Unknown behavior/connection key: " + key);
@@ -309,6 +322,7 @@ public class Settings {
w.println("autoConnectPort = " + getAutoConnectPort());
w.println("autoConnectTls = " + getAutoConnectTls());
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
}
w.println("blockSelectMode = " + getBlockSelectMode());
w.println();
@@ -18,6 +18,7 @@ public class ConnectDialog extends JDialog {
private JTextField luField;
private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox;
private JCheckBox tn3270eCheckBox;
private boolean confirmed;
private ConnectionConfig result;
@@ -146,6 +147,17 @@ public class ConnectDialog extends JDialog {
verifyCertCheckBox.setFocusPainted(false);
mainPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 7;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
tn3270eCheckBox.setForeground(fg);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
tn3270eCheckBox.setSelected(org.pubvm.j3270.config.Settings.getAutoConnectTn3270e());
tn3270eCheckBox.setFocusPainted(false);
mainPanel.add(tn3270eCheckBox, gbc);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setBackground(new Color(30, 30, 30));
@@ -169,7 +181,7 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 7;
gbc.gridy = 8;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
@@ -214,6 +226,7 @@ public class ConnectDialog extends JDialog {
result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
result.setUseTls(tlsCheckBox.isSelected());
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
confirmed = true;
dispose();
}
@@ -243,4 +256,8 @@ public class ConnectDialog extends JDialog {
public void setInitialVerifyCert(boolean verify) {
verifyCertCheckBox.setSelected(verify);
}
public void setInitialTn3270e(boolean tn3270e) {
tn3270eCheckBox.setSelected(tn3270e);
}
}
@@ -22,6 +22,7 @@ public class TerminalPanel extends JPanel {
// Font and cell dimensions
private Font terminalFont;
private Font boldTerminalFont;
private int cellWidth;
private int cellHeight;
private int fontAscent;
@@ -47,6 +48,14 @@ public class TerminalPanel extends JPanel {
// ========== Selection / Copy-Paste state ==========
private boolean blockSelectMode = false;
private static final String[] CHAR_STRINGS = new String[128];
static {
for (int i = 0; i < 128; i++) {
CHAR_STRINGS[i] = String.valueOf((char) i);
}
}
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
private int selectionStartRow = -1, selectionStartCol = -1;
private int selectionEndRow = -1, selectionEndCol = -1;
private boolean isDragging = false;
@@ -779,6 +788,7 @@ public class TerminalPanel extends JPanel {
cellHeight = fm.getHeight();
fontAscent = fm.getAscent();
fontDescent = fm.getDescent();
boldTerminalFont = terminalFont.deriveFont(Font.BOLD);
}
private void setupCursorBlink() {
@@ -818,6 +828,8 @@ public class TerminalPanel extends JPanel {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
// Clear entire panel with background color
g2.setColor(bgColor);
@@ -828,8 +840,8 @@ public class TerminalPanel extends JPanel {
int oy = getRenderOffsetY();
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getRows();
int cols = sb.getCols();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
boolean isColorModel = client.getConfig().getModel().isColor();
// Track current field attribute for monochrome color decisions
@@ -839,7 +851,7 @@ public class TerminalPanel extends JPanel {
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int baddr = row * cols + col;
ExtendedAttribute ea = sb.getCell(baddr);
ExtendedAttribute ea = sb.getDisplayCell(baddr);
int x = ox + col * cellWidth;
int y = oy + row * cellHeight;
@@ -854,10 +866,7 @@ public class TerminalPanel extends JPanel {
if (ea.isFieldAttribute()) {
currentFA = ea.fa;
currentFieldEa = ea;
// Field attributes display as blanks
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
// Selection highlight on field attribute cells too
// Selection highlight on field attribute cells
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
@@ -892,12 +901,9 @@ public class TerminalPanel extends JPanel {
// Handle invisible fields (zero intensity / password fields)
// Modern UX: render '*' for typed characters so user sees length/digit count
if (faIsZero(currentFA & 0xFF)) {
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
Font f = bold ? boldTerminalFont : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
g2.drawString("*", x, y + fontAscent);
@@ -917,9 +923,11 @@ public class TerminalPanel extends JPanel {
bgColor = tmp;
}
// Draw background
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
// Draw background only if different from default panel bgColor or if inverted
if (!bgColor.equals(this.bgColor) || reverse) {
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
}
// Draw character or Programmed Symbol
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
@@ -927,21 +935,25 @@ public class TerminalPanel extends JPanel {
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
if (slot != null) {
int[] rgb = slot.getRgbPixels(fgColor.getRGB(), bgColor.getRGB());
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(slot.getWidth(), slot.getHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB);
img.setRGB(0, 0, slot.getWidth(), slot.getHeight(), rgb, 0, slot.getWidth());
g2.drawImage(img, x, y, cellWidth, cellHeight, null);
drawnAsPs = true;
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), bgColor.getRGB());
if (img != null) {
g2.drawImage(img, x, y, null);
drawnAsPs = true;
}
}
}
if (!drawnAsPs) {
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
Font f = bold ? boldTerminalFont : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
g2.drawString(String.valueOf(ch), x, y + fontAscent);
if (ch < 128) {
g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent);
} else {
g2.drawString(String.valueOf(ch), x, y + fontAscent);
}
}
}
@@ -975,13 +987,13 @@ public class TerminalPanel extends JPanel {
// Draw cursor
if (cursorVisible && client.getConnectionState().isFullSession()) {
int curAddr = sb.getCursorAddress();
int curAddr = sb.getDisplayCursorAddress();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int cx = ox + curCol * cellWidth;
int cy = oy + curRow * cellHeight;
g2.setColor(new Color(255, 255, 255, 180));
g2.setColor(CURSOR_COLOR);
g2.setXORMode(bgColor);
g2.fillRect(cx, cy, cellWidth, cellHeight);
g2.setPaintMode();