Compare commits
8 Commits
v0.2.0-beta
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 42eef519a6 | |||
|
b57d8f960d
|
|||
|
09600260b5
|
|||
|
00c7b0770c
|
|||
|
1212b2cceb
|
|||
|
bf5e4410e6
|
|||
|
7e658a6c20
|
|||
|
c32521fd4b
|
@@ -1,3 +1,5 @@
|
|||||||
|
j3270.log.*
|
||||||
|
build/*
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.jar
|
*.jar
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# j3270
|
# j3270
|
||||||
|
|
||||||
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
||||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||||
[](https://adoptium.net)
|
[](https://adoptium.net)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E.
|
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E.
|
||||||
|
|
||||||
@@ -29,6 +29,11 @@ java -jar j3270.jar -s mainframe.example.com 992
|
|||||||
# Or using standard x3270 L: prefix
|
# Or using standard x3270 L: prefix
|
||||||
java -jar j3270.jar L:mainframe.example.com:992
|
java -jar j3270.jar L:mainframe.example.com:992
|
||||||
|
|
||||||
|
# Connect via Plain TN3270 (Non-E)
|
||||||
|
java -jar j3270.jar -P mainframe.example.com 23
|
||||||
|
# Or using standard x3270 P: prefix
|
||||||
|
java -jar j3270.jar P:mainframe.example.com:23
|
||||||
|
|
||||||
# Connect with unverified/self-signed certificate verification bypass
|
# Connect with unverified/self-signed certificate verification bypass
|
||||||
java -jar j3270.jar --tls --insecure mainframe.example.com 992
|
java -jar j3270.jar --tls --insecure mainframe.example.com 992
|
||||||
|
|
||||||
@@ -43,7 +48,7 @@ java -jar j3270.jar -c config.ini
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption.
|
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, plain TN3270 fallback, and SSL/TLS encryption.
|
||||||
- **SSL/TLS Security**:
|
- **SSL/TLS Security**:
|
||||||
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports.
|
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports.
|
||||||
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection.
|
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection.
|
||||||
@@ -71,7 +76,7 @@ The project includes self-contained, portable build scripts:
|
|||||||
# Build executable JAR in 2 seconds:
|
# Build executable JAR in 2 seconds:
|
||||||
sh ./build_all.sh
|
sh ./build_all.sh
|
||||||
|
|
||||||
# Run all 31 automated unit tests in ~180ms:
|
# Run all 57 automated unit tests in ~500ms:
|
||||||
sh ./test_all.sh
|
sh ./test_all.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,15 +90,13 @@ The resulting standalone JAR is created at `build/j3270.jar`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📜 Acknowledgements
|
## 📜 Acknowledgements
|
||||||
|
|
||||||
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
||||||
- [Antigravity](https://antigravity.google)
|
- [Antigravity](https://antigravity.google/)
|
||||||
- Claude Opus 4.6
|
- Claude Opus 4.6
|
||||||
- Gemini 3.1 Pro
|
- Gemini 3.1 Pro
|
||||||
|
- Gemini 3.7 Flash
|
||||||
|
- Gemma4 12B and 26B
|
||||||
|
- GPT-OSS 120B
|
||||||
|
- Qwen3 4B and 32B
|
||||||
|
|||||||
@@ -8,3 +8,6 @@
|
|||||||
- IND$FILE CMS and TSO:
|
- IND$FILE CMS and TSO:
|
||||||
Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
|
Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
|
||||||
|
|
||||||
|
- Local keyboard input and cursor updates not rendering in terminal:
|
||||||
|
Fixed in ScreenBuffer, InputProcessor, and TerminalPanel by ensuring display snapshot and cursor address are updated synchronously on user input operations (typing, backspace, delete, cursor movement, erase) so the presentation layer immediately renders user keystrokes.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
Manifest-Version: 1.0
|
|
||||||
Main-Class: org.pubvm.j3270.J3270App
|
|
||||||
Implementation-Title: j3270
|
|
||||||
Implementation-Version: 0.1.0
|
|
||||||
Created-By: j3270 build_all.sh
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -34,6 +34,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
private int lastPort = 23;
|
private int lastPort = 23;
|
||||||
|
|
||||||
private FileTransfer fileTransfer;
|
private FileTransfer fileTransfer;
|
||||||
|
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||||
|
|
||||||
public J3270App() {
|
public J3270App() {
|
||||||
super("j3270 — Java TN3270 Terminal Emulator");
|
super("j3270 — Java TN3270 Terminal Emulator");
|
||||||
@@ -363,7 +364,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScreenUpdated() {
|
public void onScreenUpdated() {
|
||||||
|
if (screenUpdatePending.compareAndSet(false, true)) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
screenUpdatePending.set(false);
|
||||||
// During an active file transfer, let the CUT/DFT handler drive
|
// During an active file transfer, let the CUT/DFT handler drive
|
||||||
// keyboard state. In x3270, ft_cut_data() runs before WCC
|
// keyboard state. In x3270, ft_cut_data() runs before WCC
|
||||||
// keyboard-restore is applied — the keyboard stays locked for the
|
// keyboard-restore is applied — the keyboard stays locked for the
|
||||||
@@ -378,6 +381,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
statusBar.updateStatus();
|
statusBar.updateStatus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSoundAlarm() {
|
public void onSoundAlarm() {
|
||||||
@@ -442,6 +446,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
boolean debug = false;
|
boolean debug = false;
|
||||||
boolean cliTls = false;
|
boolean cliTls = false;
|
||||||
boolean cliNoVerifyCert = false;
|
boolean cliNoVerifyCert = false;
|
||||||
|
Boolean cliTn3270e = null;
|
||||||
org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
||||||
String configFile = null;
|
String configFile = null;
|
||||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||||
@@ -452,6 +457,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
cliTls = true;
|
cliTls = true;
|
||||||
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
|
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
|
||||||
cliNoVerifyCert = true;
|
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=")) {
|
} else if (args[i].startsWith("--graphics=")) {
|
||||||
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11));
|
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("-")) {
|
} 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 finalTls = cliTls;
|
||||||
final boolean finalNoVerify = cliNoVerifyCert;
|
final boolean finalNoVerify = cliNoVerifyCert;
|
||||||
|
final Boolean finalTn3270e = cliTn3270e;
|
||||||
final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
@@ -560,6 +570,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
if (finalNoVerify) {
|
if (finalNoVerify) {
|
||||||
config.setTlsVerifyCert(false);
|
config.setTlsVerifyCert(false);
|
||||||
}
|
}
|
||||||
|
if (finalTn3270e != null) {
|
||||||
|
config.setTn3270eEnabled(finalTn3270e);
|
||||||
|
}
|
||||||
if (finalGraphicsMode != null) {
|
if (finalGraphicsMode != null) {
|
||||||
config.setGraphicsMode(finalGraphicsMode);
|
config.setGraphicsMode(finalGraphicsMode);
|
||||||
} else {
|
} else {
|
||||||
@@ -578,9 +591,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
||||||
boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls();
|
boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls();
|
||||||
boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert();
|
boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert();
|
||||||
|
boolean tn3270e = org.pubvm.j3270.config.Settings.getAutoConnectTn3270e();
|
||||||
if (host != null && !host.isEmpty()) {
|
if (host != null && !host.isEmpty()) {
|
||||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||||
config.setTlsVerifyCert(verify);
|
config.setTlsVerifyCert(verify);
|
||||||
|
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
||||||
if (finalGraphicsMode != null) {
|
if (finalGraphicsMode != null) {
|
||||||
config.setGraphicsMode(finalGraphicsMode);
|
config.setGraphicsMode(finalGraphicsMode);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -78,13 +78,21 @@ public class Settings {
|
|||||||
prefs.putBoolean("autoConnectVerifyCert", verify);
|
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() {
|
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);
|
return org.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
|
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) {
|
public static Color getColorOverride(int index, Color defaultColor) {
|
||||||
@@ -243,6 +251,11 @@ public class Settings {
|
|||||||
case "tlsVerifyCert":
|
case "tlsVerifyCert":
|
||||||
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
|
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
|
||||||
break;
|
break;
|
||||||
|
case "autoConnectTn3270e":
|
||||||
|
case "tn3270e":
|
||||||
|
case "enableTn3270e":
|
||||||
|
setAutoConnectTn3270e(Boolean.parseBoolean(value));
|
||||||
|
break;
|
||||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||||
default:
|
default:
|
||||||
log.warning("Unknown behavior/connection key: " + key);
|
log.warning("Unknown behavior/connection key: " + key);
|
||||||
@@ -309,6 +322,7 @@ public class Settings {
|
|||||||
w.println("autoConnectPort = " + getAutoConnectPort());
|
w.println("autoConnectPort = " + getAutoConnectPort());
|
||||||
w.println("autoConnectTls = " + getAutoConnectTls());
|
w.println("autoConnectTls = " + getAutoConnectTls());
|
||||||
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
|
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
|
||||||
|
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
|
||||||
}
|
}
|
||||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||||
w.println();
|
w.println();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
private JTextField luField;
|
private JTextField luField;
|
||||||
private JCheckBox tlsCheckBox;
|
private JCheckBox tlsCheckBox;
|
||||||
private JCheckBox verifyCertCheckBox;
|
private JCheckBox verifyCertCheckBox;
|
||||||
|
private JCheckBox tn3270eCheckBox;
|
||||||
private boolean confirmed;
|
private boolean confirmed;
|
||||||
private ConnectionConfig result;
|
private ConnectionConfig result;
|
||||||
|
|
||||||
@@ -146,6 +147,17 @@ public class ConnectDialog extends JDialog {
|
|||||||
verifyCertCheckBox.setFocusPainted(false);
|
verifyCertCheckBox.setFocusPainted(false);
|
||||||
mainPanel.add(verifyCertCheckBox, gbc);
|
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
|
// Buttons
|
||||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||||
@@ -169,7 +181,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
buttonPanel.add(connectBtn);
|
buttonPanel.add(connectBtn);
|
||||||
|
|
||||||
gbc.gridx = 0;
|
gbc.gridx = 0;
|
||||||
gbc.gridy = 7;
|
gbc.gridy = 8;
|
||||||
gbc.gridwidth = 2;
|
gbc.gridwidth = 2;
|
||||||
mainPanel.add(buttonPanel, gbc);
|
mainPanel.add(buttonPanel, gbc);
|
||||||
|
|
||||||
@@ -214,6 +226,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
||||||
result.setUseTls(tlsCheckBox.isSelected());
|
result.setUseTls(tlsCheckBox.isSelected());
|
||||||
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
||||||
|
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
|
||||||
confirmed = true;
|
confirmed = true;
|
||||||
dispose();
|
dispose();
|
||||||
}
|
}
|
||||||
@@ -243,4 +256,8 @@ public class ConnectDialog extends JDialog {
|
|||||||
public void setInitialVerifyCert(boolean verify) {
|
public void setInitialVerifyCert(boolean verify) {
|
||||||
verifyCertCheckBox.setSelected(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
|
// Font and cell dimensions
|
||||||
private Font terminalFont;
|
private Font terminalFont;
|
||||||
|
private Font boldTerminalFont;
|
||||||
private int cellWidth;
|
private int cellWidth;
|
||||||
private int cellHeight;
|
private int cellHeight;
|
||||||
private int fontAscent;
|
private int fontAscent;
|
||||||
@@ -47,6 +48,14 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
// ========== Selection / Copy-Paste state ==========
|
// ========== Selection / Copy-Paste state ==========
|
||||||
private boolean blockSelectMode = false;
|
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 selectionStartRow = -1, selectionStartCol = -1;
|
||||||
private int selectionEndRow = -1, selectionEndCol = -1;
|
private int selectionEndRow = -1, selectionEndCol = -1;
|
||||||
private boolean isDragging = false;
|
private boolean isDragging = false;
|
||||||
@@ -181,6 +190,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
|
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
repaint();
|
repaint();
|
||||||
@@ -714,6 +725,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refreshScreen() {
|
private void refreshScreen() {
|
||||||
|
if (client != null) {
|
||||||
|
client.getScreenBuffer().updateDisplaySnapshot();
|
||||||
|
}
|
||||||
repaint();
|
repaint();
|
||||||
// Notify parent to update status bar too
|
// Notify parent to update status bar too
|
||||||
Container parent = getParent();
|
Container parent = getParent();
|
||||||
@@ -779,6 +793,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
cellHeight = fm.getHeight();
|
cellHeight = fm.getHeight();
|
||||||
fontAscent = fm.getAscent();
|
fontAscent = fm.getAscent();
|
||||||
fontDescent = fm.getDescent();
|
fontDescent = fm.getDescent();
|
||||||
|
boldTerminalFont = terminalFont.deriveFont(Font.BOLD);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupCursorBlink() {
|
private void setupCursorBlink() {
|
||||||
@@ -818,6 +833,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
Graphics2D g2 = (Graphics2D) g;
|
Graphics2D g2 = (Graphics2D) g;
|
||||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
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_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
|
// Clear entire panel with background color
|
||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
@@ -828,8 +845,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
int oy = getRenderOffsetY();
|
int oy = getRenderOffsetY();
|
||||||
|
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
int rows = sb.getRows();
|
int rows = sb.getDisplayRows();
|
||||||
int cols = sb.getCols();
|
int cols = sb.getDisplayCols();
|
||||||
boolean isColorModel = client.getConfig().getModel().isColor();
|
boolean isColorModel = client.getConfig().getModel().isColor();
|
||||||
|
|
||||||
// Track current field attribute for monochrome color decisions
|
// Track current field attribute for monochrome color decisions
|
||||||
@@ -839,7 +856,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
for (int row = 0; row < rows; row++) {
|
for (int row = 0; row < rows; row++) {
|
||||||
for (int col = 0; col < cols; col++) {
|
for (int col = 0; col < cols; col++) {
|
||||||
int baddr = row * cols + col;
|
int baddr = row * cols + col;
|
||||||
ExtendedAttribute ea = sb.getCell(baddr);
|
ExtendedAttribute ea = sb.getDisplayCell(baddr);
|
||||||
|
|
||||||
int x = ox + col * cellWidth;
|
int x = ox + col * cellWidth;
|
||||||
int y = oy + row * cellHeight;
|
int y = oy + row * cellHeight;
|
||||||
@@ -854,10 +871,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (ea.isFieldAttribute()) {
|
if (ea.isFieldAttribute()) {
|
||||||
currentFA = ea.fa;
|
currentFA = ea.fa;
|
||||||
currentFieldEa = ea;
|
currentFieldEa = ea;
|
||||||
// Field attributes display as blanks
|
// Selection highlight on field attribute cells
|
||||||
g2.setColor(this.bgColor);
|
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
|
||||||
// Selection highlight on field attribute cells too
|
|
||||||
if (isCellSelected(row, col)) {
|
if (isCellSelected(row, col)) {
|
||||||
g2.setColor(SELECTION_COLOR);
|
g2.setColor(SELECTION_COLOR);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
@@ -892,12 +906,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Handle invisible fields (zero intensity / password fields)
|
// Handle invisible fields (zero intensity / password fields)
|
||||||
// Modern UX: render '*' for typed characters so user sees length/digit count
|
// Modern UX: render '*' for typed characters so user sees length/digit count
|
||||||
if (faIsZero(currentFA & 0xFF)) {
|
if (faIsZero(currentFA & 0xFF)) {
|
||||||
g2.setColor(this.bgColor);
|
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
|
||||||
|
|
||||||
char ch = ea.ucs4;
|
char ch = ea.ucs4;
|
||||||
if (ch > 0x20 && ch != 0xFF) {
|
if (ch > 0x20 && ch != 0xFF) {
|
||||||
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
|
Font f = bold ? boldTerminalFont : terminalFont;
|
||||||
g2.setFont(f);
|
g2.setFont(f);
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
g2.drawString("*", x, y + fontAscent);
|
g2.drawString("*", x, y + fontAscent);
|
||||||
@@ -917,26 +928,39 @@ public class TerminalPanel extends JPanel {
|
|||||||
bgColor = tmp;
|
bgColor = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw background
|
// Draw background only if different from default panel bgColor or if inverted
|
||||||
|
if (!bgColor.equals(this.bgColor) || reverse) {
|
||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw character or Programmed Symbol
|
// Draw character or Programmed Symbol
|
||||||
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
|
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
|
||||||
boolean drawnAsPs = false;
|
boolean drawnAsPs = false;
|
||||||
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
|
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
|
||||||
drawnAsPs = client.getProgramSymbolManager().drawSymbol(g2, cs, ea.ec & 0xFF, x, y, cellWidth, cellHeight, fgColor, bgColor);
|
org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
|
||||||
|
if (slot != null) {
|
||||||
|
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) {
|
if (!drawnAsPs) {
|
||||||
char ch = ea.ucs4;
|
char ch = ea.ucs4;
|
||||||
if (ch > 0x20 && ch != 0xFF) {
|
if (ch > 0x20 && ch != 0xFF) {
|
||||||
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
|
Font f = bold ? boldTerminalFont : terminalFont;
|
||||||
g2.setFont(f);
|
g2.setFont(f);
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
|
if (ch < 128) {
|
||||||
|
g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent);
|
||||||
|
} else {
|
||||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw underline
|
// Draw underline
|
||||||
if (underline) {
|
if (underline) {
|
||||||
@@ -958,18 +982,23 @@ public class TerminalPanel extends JPanel {
|
|||||||
int gridW = cols * cellWidth;
|
int gridW = cols * cellWidth;
|
||||||
int gridH = rows * cellHeight;
|
int gridH = rows * cellHeight;
|
||||||
client.getGraphicsPlane().resize(gridW, gridH);
|
client.getGraphicsPlane().resize(gridW, gridH);
|
||||||
g2.drawImage(client.getGraphicsPlane().getImage(), ox, oy, gridW, gridH, null);
|
int[] rgb = client.getGraphicsPlane().getRgbBuffer();
|
||||||
|
if (rgb != null) {
|
||||||
|
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gridW, gridH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||||
|
img.setRGB(0, 0, gridW, gridH, rgb, 0, gridW);
|
||||||
|
g2.drawImage(img, ox, oy, gridW, gridH, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw cursor
|
// Draw cursor
|
||||||
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
||||||
int curAddr = sb.getCursorAddress();
|
int curAddr = sb.getDisplayCursorAddress();
|
||||||
int curRow = curAddr / cols;
|
int curRow = curAddr / cols;
|
||||||
int curCol = curAddr % cols;
|
int curCol = curAddr % cols;
|
||||||
int cx = ox + curCol * cellWidth;
|
int cx = ox + curCol * cellWidth;
|
||||||
int cy = oy + curRow * cellHeight;
|
int cy = oy + curRow * cellHeight;
|
||||||
|
|
||||||
g2.setColor(new Color(255, 255, 255, 180));
|
g2.setColor(CURSOR_COLOR);
|
||||||
g2.setXORMode(bgColor);
|
g2.setXORMode(bgColor);
|
||||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||||
g2.setPaintMode();
|
g2.setPaintMode();
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
|
||||||
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ public class ConnectionConfig {
|
|||||||
private int connectTimeoutMs = 15000;
|
private int connectTimeoutMs = 15000;
|
||||||
private int nopIntervalSeconds = 0;
|
private int nopIntervalSeconds = 0;
|
||||||
private String terminalName = null; // override terminal type string
|
private String terminalName = null; // override terminal type string
|
||||||
private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.NONE;
|
private boolean tn3270eEnabled = true;
|
||||||
|
private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.BOTH;
|
||||||
|
|
||||||
public ConnectionConfig() {}
|
public ConnectionConfig() {}
|
||||||
|
|
||||||
@@ -66,6 +67,9 @@ public class ConnectionConfig {
|
|||||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||||
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
|
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
|
||||||
|
|
||||||
|
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
|
||||||
|
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
|
||||||
|
|
||||||
public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||||
public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
||||||
|
|
||||||
@@ -87,8 +91,8 @@ public class ConnectionConfig {
|
|||||||
public void setTerminalName(String name) { this.terminalName = name; }
|
public void setTerminalName(String name) { this.terminalName = name; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port")
|
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||||
* or standard "host:port" formats.
|
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats.
|
||||||
*/
|
*/
|
||||||
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
||||||
if (hostStr == null || hostStr.trim().isEmpty()) {
|
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||||
@@ -96,17 +100,35 @@ public class ConnectionConfig {
|
|||||||
}
|
}
|
||||||
String s = hostStr.trim();
|
String s = hostStr.trim();
|
||||||
boolean tls = false;
|
boolean tls = false;
|
||||||
|
boolean tn3270e = true;
|
||||||
|
|
||||||
// Check TLS prefixes
|
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
|
||||||
if (s.startsWith("L:") || s.startsWith("l:")) {
|
boolean prefixFound = true;
|
||||||
tls = true;
|
while (prefixFound) {
|
||||||
s = s.substring(2);
|
prefixFound = false;
|
||||||
} else if (s.startsWith("Y:") || s.startsWith("y:")) {
|
if (s.startsWith("L:") || s.startsWith("l:") || s.startsWith("Y:") || s.startsWith("y:")) {
|
||||||
tls = true;
|
tls = true;
|
||||||
s = s.substring(2);
|
s = s.substring(2);
|
||||||
|
prefixFound = true;
|
||||||
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
|
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
|
||||||
tls = true;
|
tls = true;
|
||||||
s = s.substring(4);
|
s = s.substring(4);
|
||||||
|
prefixFound = true;
|
||||||
|
} else if (s.startsWith("N:") || s.startsWith("n:") || s.toLowerCase().startsWith("notls:") || s.toLowerCase().startsWith("nossl:")) {
|
||||||
|
tls = false;
|
||||||
|
int colon = s.indexOf(':');
|
||||||
|
s = s.substring(colon + 1);
|
||||||
|
prefixFound = true;
|
||||||
|
} else if (s.startsWith("P:") || s.startsWith("p:")) {
|
||||||
|
tn3270e = false;
|
||||||
|
s = s.substring(2);
|
||||||
|
prefixFound = true;
|
||||||
|
} else if (s.toLowerCase().startsWith("plain:") || s.toLowerCase().startsWith("non-e:")) {
|
||||||
|
tn3270e = false;
|
||||||
|
int colon = s.indexOf(':');
|
||||||
|
s = s.substring(colon + 1);
|
||||||
|
prefixFound = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String host = s;
|
String host = s;
|
||||||
@@ -133,6 +155,7 @@ public class ConnectionConfig {
|
|||||||
|
|
||||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||||
config.setUseTls(tls);
|
config.setUseTls(tls);
|
||||||
|
config.setTn3270eEnabled(tn3270e);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import static org.lib3270j.protocol.DS3270Constants.*;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,16 +106,24 @@ public class DataStreamProcessor {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
int cmd = data[offset] & 0xFF;
|
int cmd = data[offset] & 0xFF;
|
||||||
log.info(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
|
synchronized (screen.getRenderLock()) {
|
||||||
|
if (log.isLoggable(Level.FINE)) {
|
||||||
|
log.fine(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
|
||||||
|
}
|
||||||
|
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case CMD_W:
|
case CMD_W:
|
||||||
case SNA_CMD_W:
|
case SNA_CMD_W:
|
||||||
|
programSymbolManager.commitStagedSymbols();
|
||||||
processWrite(data, offset, length, false);
|
processWrite(data, offset, length, false);
|
||||||
break;
|
break;
|
||||||
case CMD_EW:
|
case CMD_EW:
|
||||||
case SNA_CMD_EW:
|
case SNA_CMD_EW:
|
||||||
log.info(">>> ERASE/WRITE: clearing screen (default size)"); {
|
programSymbolManager.commitStagedSymbols();
|
||||||
|
if (log.isLoggable(Level.FINE)) {
|
||||||
|
log.fine(">>> ERASE/WRITE: clearing screen (default size)");
|
||||||
|
}
|
||||||
|
{
|
||||||
int oldRows = screen.getRows();
|
int oldRows = screen.getRows();
|
||||||
int oldCols = screen.getCols();
|
int oldCols = screen.getCols();
|
||||||
screen.erase(false);
|
screen.erase(false);
|
||||||
@@ -127,7 +136,11 @@ public class DataStreamProcessor {
|
|||||||
break;
|
break;
|
||||||
case CMD_EWA:
|
case CMD_EWA:
|
||||||
case SNA_CMD_EWA:
|
case SNA_CMD_EWA:
|
||||||
log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); {
|
programSymbolManager.commitStagedSymbols();
|
||||||
|
if (log.isLoggable(Level.FINE)) {
|
||||||
|
log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)");
|
||||||
|
}
|
||||||
|
{
|
||||||
int oldRows = screen.getRows();
|
int oldRows = screen.getRows();
|
||||||
int oldCols = screen.getCols();
|
int oldCols = screen.getCols();
|
||||||
screen.erase(true);
|
screen.erase(true);
|
||||||
@@ -140,18 +153,22 @@ public class DataStreamProcessor {
|
|||||||
break;
|
break;
|
||||||
case CMD_RB:
|
case CMD_RB:
|
||||||
case SNA_CMD_RB:
|
case SNA_CMD_RB:
|
||||||
|
programSymbolManager.commitStagedSymbols();
|
||||||
processReadBuffer();
|
processReadBuffer();
|
||||||
break;
|
break;
|
||||||
case CMD_RM:
|
case CMD_RM:
|
||||||
case SNA_CMD_RM:
|
case SNA_CMD_RM:
|
||||||
|
programSymbolManager.commitStagedSymbols();
|
||||||
processReadModified(false);
|
processReadModified(false);
|
||||||
break;
|
break;
|
||||||
case CMD_RMA:
|
case CMD_RMA:
|
||||||
case SNA_CMD_RMA:
|
case SNA_CMD_RMA:
|
||||||
|
programSymbolManager.commitStagedSymbols();
|
||||||
processReadModified(true);
|
processReadModified(true);
|
||||||
break;
|
break;
|
||||||
case CMD_EAU:
|
case CMD_EAU:
|
||||||
case SNA_CMD_EAU:
|
case SNA_CMD_EAU:
|
||||||
|
programSymbolManager.commitStagedSymbols();
|
||||||
log.info(">>> EAU: erasing all unprotected fields");
|
log.info(">>> EAU: erasing all unprotected fields");
|
||||||
screen.eraseAllUnprotected();
|
screen.eraseAllUnprotected();
|
||||||
break;
|
break;
|
||||||
@@ -170,9 +187,11 @@ public class DataStreamProcessor {
|
|||||||
// Translate EBCDIC to Unicode for display
|
// Translate EBCDIC to Unicode for display
|
||||||
screen.translateToUnicode();
|
screen.translateToUnicode();
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: dump non-empty screen lines
|
// Debug: dump non-empty screen lines
|
||||||
if (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();
|
||||||
int c = screen.getCols();
|
int c = screen.getCols();
|
||||||
StringBuilder dump = new StringBuilder();
|
StringBuilder dump = new StringBuilder();
|
||||||
@@ -205,7 +224,7 @@ public class DataStreamProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dump.length() > 0) {
|
if (dump.length() > 0) {
|
||||||
log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
|
log.fine("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -798,7 +817,6 @@ public class DataStreamProcessor {
|
|||||||
byte[] psData = new byte[fieldLen - 3];
|
byte[] psData = new byte[fieldLen - 3];
|
||||||
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
||||||
programSymbolManager.loadps(psData);
|
programSymbolManager.loadps(psData);
|
||||||
notifyScreenUpdated();
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
case org.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
||||||
@@ -810,7 +828,6 @@ public class DataStreamProcessor {
|
|||||||
byte[] psData = new byte[fieldLen - 4];
|
byte[] psData = new byte[fieldLen - 4];
|
||||||
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
||||||
programSymbolManager.loadps(psData);
|
programSymbolManager.loadps(psData);
|
||||||
notifyScreenUpdated();
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
||||||
@@ -946,7 +963,7 @@ public class DataStreamProcessor {
|
|||||||
if ((i + 1) % 32 == 0)
|
if ((i + 1) % 32 == 0)
|
||||||
sb.append("\n ");
|
sb.append("\n ");
|
||||||
}
|
}
|
||||||
log.info("Query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
|
log.warning(">>> SENDING Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
|
||||||
if (outputSender != null) {
|
if (outputSender != null) {
|
||||||
outputSender.send3270Data(qr);
|
outputSender.send3270Data(qr);
|
||||||
}
|
}
|
||||||
@@ -961,7 +978,7 @@ public class DataStreamProcessor {
|
|||||||
if ((i + 1) % 32 == 0)
|
if ((i + 1) % 32 == 0)
|
||||||
sb.append("\n ");
|
sb.append("\n ");
|
||||||
}
|
}
|
||||||
log.info("Requested query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
|
log.warning(">>> SENDING Requested Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
|
||||||
if (outputSender != null) {
|
if (outputSender != null) {
|
||||||
outputSender.send3270Data(qr);
|
outputSender.send3270Data(qr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class QueryReplyBuilder {
|
|||||||
private static final int Yr_3279_2 = 0x0002006f;
|
private static final int Yr_3279_2 = 0x0002006f;
|
||||||
|
|
||||||
private final ScreenBuffer screen;
|
private final ScreenBuffer screen;
|
||||||
private GraphicsMode graphicsMode = GraphicsMode.NONE;
|
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
|
||||||
|
|
||||||
// Base query reply codes (text mode)
|
// Base query reply codes (text mode)
|
||||||
private static final int[] SUPPORTED_QR_BASE = {
|
private static final int[] SUPPORTED_QR_BASE = {
|
||||||
@@ -103,26 +103,34 @@ public class QueryReplyBuilder {
|
|||||||
// Highlighting
|
// Highlighting
|
||||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||||
|
|
||||||
// Reply Modes
|
// Reply Modes (0x88)
|
||||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||||
|
|
||||||
// Distributed Data Management (DFT File Transfer)
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
// Save/Restore (0x8C)
|
||||||
|
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distributed Data Management (0x95)
|
||||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||||
|
|
||||||
// Implicit Partition
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
|
// Transparency (0x99)
|
||||||
|
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implicit Partition (0xA6)
|
||||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||||
|
|
||||||
// Vector Graphics QRs if enabled
|
// Vector Graphics QRs if enabled
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
|
||||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
appendQueryReply(out, QR_GRAPHICS, buildGraphics()); // 0xB0
|
||||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames());
|
appendQueryReply(out, QR_GIMAGE, buildGImage()); // 0xB1
|
||||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics());
|
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
|
||||||
appendQueryReply(out, QR_GIMAGE, buildGImage());
|
appendOemFmt(out); // 0xB3
|
||||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
|
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
|
||||||
appendOemFmt(out);
|
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6
|
||||||
appendQueryReply(out, QR_GCOLOR, buildGColor());
|
|
||||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
|
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
|
||||||
@@ -304,48 +312,48 @@ public class QueryReplyBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildCharsets() {
|
private byte[] buildCharsets() {
|
||||||
if (graphicsMode.isVectorGraphicsEnabled() || graphicsMode == GraphicsMode.NONE) {
|
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||||
// Standard 3179G / Base character sets (matches HOD QR_CHARSETS_S_STRING, 27 bytes total)
|
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
|
||||||
out.write(0x82); // flags: GE, CGCSGID present
|
|
||||||
out.write(0x00); // more flags
|
|
||||||
out.write(SW_3279_2); // SDW - default char width (9)
|
|
||||||
out.write(14); // SDH - default char height (14)
|
|
||||||
out.write(0x00); // LoadPS format (0x00)
|
|
||||||
out.write(0x00);
|
|
||||||
out.write(0x00);
|
|
||||||
out.write(0x00);
|
|
||||||
out.write(0x07); // DL = 7
|
|
||||||
// Set 0 (Base EBCDIC)
|
|
||||||
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4);
|
|
||||||
// Set 1 (APL/Text)
|
|
||||||
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
|
||||||
return out.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Programmed Symbols mode (3279 PS)
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||||
out.write(0x82); // flags: GE, CGCSGID present
|
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||||
out.write(0x00); // more flags
|
out.write(0x00); // more flags
|
||||||
out.write(SW_3279_2); // SDW (9)
|
out.write(SW_3279_2); // SDW (9)
|
||||||
out.write(14); // SDH (14)
|
out.write(SH_3279_2); // SDH (12)
|
||||||
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
||||||
out.write(0x00); // Load PS device type (high)
|
out.write(0x00); // Load PS device type (high)
|
||||||
out.write(0x00); // Load PS device type (low)
|
out.write(0x00); // Load PS device type (low)
|
||||||
out.write(0x00); // reserved
|
out.write(0x00); // reserved
|
||||||
out.write(0x07); // DL = 7 bytes per descriptor
|
out.write(0x07); // DL = 7 bytes per descriptor
|
||||||
// Descriptor 1 (SET 0): default character set
|
// Descriptor 1 (SET 0): default character set (non-loadable, single plane, CP037)
|
||||||
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4);
|
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
|
||||||
// Descriptor 2 (SET 1): APL/GE character set
|
// Descriptor 2 (SET 1): APL/GE character set
|
||||||
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||||
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3)
|
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - Flags = 0x80 (Loadable, single plane)
|
||||||
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7)
|
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) - Flags = 0xC0 (0x80 Loadable | 0x40 Triple-plane)
|
||||||
out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
out.write(0x04); out.write(0xc0); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
out.write(0x05); out.write(0xc0); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
out.write(0x06); out.write(0xc0); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
out.write(0x07); out.write(0xc0); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard 3179G / Base character sets (matches sf.c / HOD)
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
||||||
|
out.write(0x82); // flags: GE, CGCSGID present
|
||||||
|
out.write(0x00); // more flags
|
||||||
|
out.write(SW_3279_2); // SDW - default char width (9)
|
||||||
|
out.write(SH_3279_2); // SDH - default char height (12)
|
||||||
|
out.write(0x00); // LoadPS format (0x00)
|
||||||
|
out.write(0x00);
|
||||||
|
out.write(0x00);
|
||||||
|
out.write(0x00);
|
||||||
|
out.write(0x07); // DL = 7
|
||||||
|
// Set 0 (Base EBCDIC - Non-loadable, single plane, CP037)
|
||||||
|
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
|
||||||
|
// Set 1 (APL/Text)
|
||||||
|
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,10 +481,13 @@ public class QueryReplyBuilder {
|
|||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
out.write(i);
|
out.write(i);
|
||||||
java.awt.Color c = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
int argb = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||||
out.write(c.getRed());
|
int r = (argb >> 16) & 0xFF;
|
||||||
out.write(c.getGreen());
|
int g = (argb >> 8) & 0xFF;
|
||||||
out.write(c.getBlue());
|
int b = argb & 0xFF;
|
||||||
|
out.write(r);
|
||||||
|
out.write(g);
|
||||||
|
out.write(b);
|
||||||
out.write(0x00); // 6th byte in HOD color table
|
out.write(0x00); // 6th byte in HOD color table
|
||||||
}
|
}
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
|
|||||||
@@ -150,24 +150,35 @@ public final class GocaConstants {
|
|||||||
public static final int MIX_XOR = 4;
|
public static final int MIX_XOR = 4;
|
||||||
public static final int MIX_UNDER = 5;
|
public static final int MIX_UNDER = 5;
|
||||||
|
|
||||||
// Graphic Colors (IBM 3179G / HOD 16-color table)
|
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
|
||||||
public static final java.awt.Color[] GOCA_COLORS = new java.awt.Color[] {
|
public static final int[] GOCA_COLORS = new int[] {
|
||||||
new java.awt.Color(0, 255, 0), // 0: Default (Green)
|
0xFF00FF00, // 0: Default (Green)
|
||||||
new java.awt.Color(120, 144, 240), // 1: Blue
|
0xFF7890F0, // 1: Blue (120, 144, 240)
|
||||||
new java.awt.Color(255, 0, 0), // 2: Red
|
0xFFFF0000, // 2: Red (255, 0, 0)
|
||||||
new java.awt.Color(255, 0, 255), // 3: Pink / Magenta
|
0xFFFF00FF, // 3: Pink / Magenta (255, 0, 255)
|
||||||
new java.awt.Color(0, 255, 0), // 4: Green
|
0xFF00FF00, // 4: Green (0, 255, 0)
|
||||||
new java.awt.Color(0, 255, 255), // 5: Turquoise / Cyan
|
0xFF00FFFF, // 5: Turquoise / Cyan (0, 255, 255)
|
||||||
new java.awt.Color(255, 255, 0), // 6: Yellow
|
0xFFFFFF00, // 6: Yellow (255, 255, 0)
|
||||||
new java.awt.Color(255, 255, 255), // 7: Neutral White
|
0xFFFFFFFF, // 7: Neutral White (255, 255, 255)
|
||||||
new java.awt.Color(0, 0, 0), // 8: Black
|
0xFF000000, // 8: Black (0, 0, 0)
|
||||||
new java.awt.Color(0, 0, 128), // 9: Deep Blue
|
0xFF000080, // 9: Deep Blue (0, 0, 128)
|
||||||
new java.awt.Color(128, 0, 0), // 10: Orange / Dark Red
|
0xFF800000, // 10: Orange / Dark Red (128, 0, 0)
|
||||||
new java.awt.Color(128, 0, 128), // 11: Purple
|
0xFF800080, // 11: Purple (128, 0, 128)
|
||||||
new java.awt.Color(0, 128, 0), // 12: Pale Green
|
0xFF008000, // 12: Pale Green (0, 128, 0)
|
||||||
new java.awt.Color(0, 128, 128), // 13: Pale Cyan
|
0xFF008080, // 13: Pale Cyan (0, 128, 128)
|
||||||
new java.awt.Color(215, 151, 0), // 14: Mustard
|
0xFFD79700, // 14: Mustard (215, 151, 0)
|
||||||
new java.awt.Color(192, 192, 192), // 15: Grey / Light White
|
0xFFC0C0C0, // 15: Grey / Light White (192, 192, 192)
|
||||||
new java.awt.Color(73, 36, 0) // 16: Brown
|
0xFF492400 // 16: Brown (73, 36, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the 32-bit ARGB color value for a given GOCA color index (0-16).
|
||||||
|
* Returns Green (0xFF00FF00) if the index is out of range.
|
||||||
|
*/
|
||||||
|
public static int getGocaColorArgb(int colorIndex) {
|
||||||
|
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
|
||||||
|
return GOCA_COLORS[colorIndex];
|
||||||
|
}
|
||||||
|
return GOCA_COLORS[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package org.lib3270j.graphics;
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.awt.geom.AffineTransform;
|
|
||||||
import java.awt.geom.Path2D;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import org.lib3270j.charset.EbcdicTranslator;
|
||||||
@@ -22,14 +19,14 @@ public class GocaDecoder {
|
|||||||
// Drawing state
|
// Drawing state
|
||||||
private int curX = 0;
|
private int curX = 0;
|
||||||
private int curY = 0;
|
private int curY = 0;
|
||||||
private Color curColor = Color.GREEN;
|
private int curColor = GocaConstants.GOCA_COLORS[0];
|
||||||
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;
|
||||||
private int markerSize = 5;
|
private int markerSize = 5;
|
||||||
private Color markerColor = Color.GREEN;
|
private int markerColor = GocaConstants.GOCA_COLORS[0];
|
||||||
private int pattern = GocaConstants.PT_SOLID;
|
private int pattern = GocaConstants.PT_SOLID;
|
||||||
private Color fillColor = Color.GREEN;
|
private int fillColor = GocaConstants.GOCA_COLORS[0];
|
||||||
private int charDir = GocaConstants.CD_LR;
|
private int charDir = GocaConstants.CD_LR;
|
||||||
private double charAngle = 0.0;
|
private double charAngle = 0.0;
|
||||||
private int charWidth = 9;
|
private int charWidth = 9;
|
||||||
@@ -781,8 +778,22 @@ public class GocaDecoder {
|
|||||||
int code = data[pos + i] & 0xFF;
|
int code = data[pos + i] & 0xFF;
|
||||||
int px = plane.mapX(startX);
|
int px = plane.mapX(startX);
|
||||||
int py = plane.mapY(startY) - ch;
|
int py = plane.mapY(startY) - ch;
|
||||||
boolean drawn = programSymbolManager.drawSymbol(plane.getGraphics(), charSet, code, px, py, cw, ch, curColor, null);
|
ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code);
|
||||||
if (!drawn) {
|
if (slot != null) {
|
||||||
|
int[] rgb = slot.getRgbPixels(curColor, 0);
|
||||||
|
int symW = slot.getWidth();
|
||||||
|
int symH = slot.getHeight();
|
||||||
|
for (int dy = 0; dy < ch; dy++) {
|
||||||
|
int sy = (dy * symH) / ch;
|
||||||
|
for (int dx = 0; dx < cw; dx++) {
|
||||||
|
int sx = (dx * symW) / cw;
|
||||||
|
int pixelArgb = rgb[sy * symW + sx];
|
||||||
|
if ((pixelArgb >>> 24) != 0) {
|
||||||
|
plane.setPixel(px + dx, py + dy, pixelArgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
||||||
plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle);
|
plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle);
|
||||||
}
|
}
|
||||||
@@ -810,10 +821,7 @@ public class GocaDecoder {
|
|||||||
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
|
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color getColor(int colorIndex) {
|
private int getColor(int colorIndex) {
|
||||||
if (colorIndex >= 0 && colorIndex < GocaConstants.GOCA_COLORS.length) {
|
return GocaConstants.getGocaColorArgb(colorIndex);
|
||||||
return GocaConstants.GOCA_COLORS[colorIndex];
|
|
||||||
}
|
|
||||||
return Color.GREEN;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
package org.lib3270j.graphics;
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
import java.awt.BasicStroke;
|
import java.util.ArrayList;
|
||||||
import java.awt.Color;
|
import java.util.Arrays;
|
||||||
import java.awt.Graphics2D;
|
import java.util.Collections;
|
||||||
import java.awt.Polygon;
|
import java.util.List;
|
||||||
import java.awt.RenderingHints;
|
|
||||||
import java.awt.Stroke;
|
|
||||||
import java.awt.geom.AffineTransform;
|
|
||||||
import java.awt.geom.Arc2D;
|
|
||||||
import java.awt.geom.GeneralPath;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offscreen rendering surface for GOCA vector graphics.
|
* Offscreen rendering surface for GOCA vector graphics.
|
||||||
* Maintained as an ARGB BufferedImage that overlays the 3270 character cell matrix.
|
* Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix.
|
||||||
|
* Pure Java software rasterizer compatible with standard Java SE (Swing) and Android (Bitmap).
|
||||||
*/
|
*/
|
||||||
public class GraphicsPlane {
|
public class GraphicsPlane {
|
||||||
|
|
||||||
@@ -43,50 +38,45 @@ public class GraphicsPlane {
|
|||||||
|
|
||||||
private int canvasWidth = 800;
|
private int canvasWidth = 800;
|
||||||
private int canvasHeight = 600;
|
private int canvasHeight = 600;
|
||||||
private BufferedImage image;
|
private int[] rgbBuffer;
|
||||||
private Graphics2D g2d;
|
|
||||||
private boolean hasContent = false;
|
private boolean hasContent = false;
|
||||||
|
|
||||||
|
private int screenCols = 80;
|
||||||
|
private int screenRows = 24;
|
||||||
|
|
||||||
public GraphicsPlane(int width, int height) {
|
public GraphicsPlane(int width, int height) {
|
||||||
resize(width, height);
|
this.canvasWidth = Math.max(1, width);
|
||||||
|
this.canvasHeight = Math.max(1, height);
|
||||||
|
this.rgbBuffer = new int[canvasWidth * canvasHeight];
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void resize(int width, int height) {
|
public synchronized void resize(int width, int height) {
|
||||||
int w = Math.max(1, width);
|
int w = Math.max(1, width);
|
||||||
int h = Math.max(1, height);
|
int h = Math.max(1, height);
|
||||||
if (w == canvasWidth && h == canvasHeight && image != null) {
|
if (w == canvasWidth && h == canvasHeight && rgbBuffer != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int[] newBuffer = new int[w * h];
|
||||||
|
if (rgbBuffer != null && hasContent) {
|
||||||
|
// Scale existing content to new dimensions using nearest-neighbor
|
||||||
|
for (int dy = 0; dy < h; dy++) {
|
||||||
|
int sy = (dy * canvasHeight) / h;
|
||||||
|
for (int dx = 0; dx < w; dx++) {
|
||||||
|
int sx = (dx * canvasWidth) / w;
|
||||||
|
newBuffer[dy * w + dx] = rgbBuffer[sy * canvasWidth + sx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.canvasWidth = w;
|
this.canvasWidth = w;
|
||||||
this.canvasHeight = h;
|
this.canvasHeight = h;
|
||||||
|
this.rgbBuffer = newBuffer;
|
||||||
BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
|
||||||
Graphics2D newG2d = newImage.createGraphics();
|
|
||||||
newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
||||||
newG2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
|
||||||
|
|
||||||
if (image != null && hasContent) {
|
|
||||||
newG2d.drawImage(image, 0, 0, w, h, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.g2d != null) {
|
|
||||||
this.g2d.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.image = newImage;
|
|
||||||
this.g2d = newG2d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void clear() {
|
public synchronized void clear() {
|
||||||
if (image != null && g2d != null) {
|
if (rgbBuffer != null) {
|
||||||
BufferedImage newImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
|
Arrays.fill(rgbBuffer, 0);
|
||||||
Graphics2D newG2d = newImage.createGraphics();
|
|
||||||
newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
||||||
if (this.g2d != null) {
|
|
||||||
this.g2d.dispose();
|
|
||||||
}
|
|
||||||
this.image = newImage;
|
|
||||||
this.g2d = newG2d;
|
|
||||||
}
|
}
|
||||||
hasContent = false;
|
hasContent = false;
|
||||||
}
|
}
|
||||||
@@ -95,8 +85,8 @@ public class GraphicsPlane {
|
|||||||
return hasContent;
|
return hasContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized BufferedImage getImage() {
|
public synchronized int[] getRgbBuffer() {
|
||||||
return image;
|
return rgbBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCanvasWidth() {
|
public int getCanvasWidth() {
|
||||||
@@ -107,13 +97,6 @@ public class GraphicsPlane {
|
|||||||
return canvasHeight;
|
return canvasHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized Graphics2D getGraphics() {
|
|
||||||
return g2d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int screenCols = 80;
|
|
||||||
private int screenRows = 24;
|
|
||||||
|
|
||||||
public void setScreenDimensions(int cols, int rows) {
|
public 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;
|
||||||
@@ -148,35 +131,114 @@ public class GraphicsPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws an absolute or relative line.
|
* Safely plots a pixel at (x, y).
|
||||||
*/
|
*/
|
||||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, Color color, int lineType, int lineWidth) {
|
public synchronized void setPixel(int x, int y, int colorArgb) {
|
||||||
if (g2d == null) return;
|
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
||||||
g2d.setColor(color != null ? color : Color.WHITE);
|
rgbBuffer[y * canvasWidth + x] = colorArgb;
|
||||||
g2d.setStroke(createStroke(lineType, lineWidth));
|
|
||||||
g2d.drawLine(x1, y1, x2, y2);
|
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an absolute or relative line using Bresenham's algorithm with line styles and widths.
|
||||||
|
*/
|
||||||
|
public synchronized void drawLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
|
||||||
|
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
||||||
|
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
|
||||||
|
|
||||||
|
int dx = Math.abs(x2 - x1);
|
||||||
|
int dy = Math.abs(y2 - y1);
|
||||||
|
int sx = x1 < x2 ? 1 : -1;
|
||||||
|
int sy = y1 < y2 ? 1 : -1;
|
||||||
|
int err = dx - dy;
|
||||||
|
|
||||||
|
int curX = x1;
|
||||||
|
int curY = y1;
|
||||||
|
int stepIndex = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (shouldPlotLinePixel(stepIndex, lineType)) {
|
||||||
|
drawPixelWithThickness(curX, curY, color, thickness);
|
||||||
|
}
|
||||||
|
stepIndex++;
|
||||||
|
|
||||||
|
if (curX == x2 && curY == y2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int e2 = 2 * err;
|
||||||
|
if (e2 > -dy) {
|
||||||
|
err -= dy;
|
||||||
|
curX += sx;
|
||||||
|
}
|
||||||
|
if (e2 < dx) {
|
||||||
|
err += dx;
|
||||||
|
curY += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldPlotLinePixel(int step, int lineType) {
|
||||||
|
switch (lineType) {
|
||||||
|
case GocaConstants.LT_DOT:
|
||||||
|
return (step % 4) < 2;
|
||||||
|
case GocaConstants.LT_SHORTDASH:
|
||||||
|
return (step % 6) < 4;
|
||||||
|
case GocaConstants.LT_DASHDOT:
|
||||||
|
int m12 = step % 12;
|
||||||
|
return m12 < 6 || (m12 >= 8 && m12 < 10);
|
||||||
|
case GocaConstants.LT_DOUBLEDOT:
|
||||||
|
int m10 = step % 10;
|
||||||
|
return m10 < 2 || (m10 >= 4 && m10 < 6);
|
||||||
|
case GocaConstants.LT_LONGDASH:
|
||||||
|
return (step % 11) < 8;
|
||||||
|
case GocaConstants.LT_DASHDOUBLEDOT:
|
||||||
|
int m18 = step % 18;
|
||||||
|
return m18 < 8 || (m18 >= 10 && m18 < 12) || (m18 >= 14 && m18 < 16);
|
||||||
|
case GocaConstants.LT_SOLID:
|
||||||
|
case GocaConstants.LT_DEFAULT:
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawPixelWithThickness(int x, int y, int color, int thickness) {
|
||||||
|
if (thickness <= 1) {
|
||||||
|
setPixel(x, y, color);
|
||||||
|
} else {
|
||||||
|
for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) {
|
||||||
|
for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) {
|
||||||
|
setPixel(x + dx, y + dy, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a full or partial arc / ellipse.
|
* Draws a full or partial arc / ellipse.
|
||||||
*/
|
*/
|
||||||
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
|
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
|
||||||
Color color, int lineType, int lineWidth, boolean isFull) {
|
int colorArgb, int lineType, int lineWidth, boolean isFull) {
|
||||||
if (g2d == null) return;
|
if (rx <= 0) rx = 1;
|
||||||
g2d.setColor(color != null ? color : Color.WHITE);
|
if (ry <= 0) ry = 1;
|
||||||
g2d.setStroke(createStroke(lineType, lineWidth));
|
|
||||||
|
|
||||||
double x = cx - rx;
|
int numSteps = Math.max(24, Math.max(rx, ry) * 4);
|
||||||
double y = cy - ry;
|
double startRad = Math.toRadians(startAngleDeg);
|
||||||
double w = rx * 2.0;
|
double sweepRad = isFull ? (2.0 * Math.PI) : Math.toRadians(sweepAngleDeg);
|
||||||
double h = ry * 2.0;
|
double stepRad = sweepRad / numSteps;
|
||||||
|
|
||||||
if (isFull) {
|
int prevX = (int) Math.round(cx + rx * Math.cos(startRad));
|
||||||
g2d.drawOval((int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h));
|
int prevY = (int) Math.round(cy - ry * Math.sin(startRad));
|
||||||
} else {
|
|
||||||
Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngleDeg, sweepAngleDeg, Arc2D.OPEN);
|
for (int i = 1; i <= numSteps; i++) {
|
||||||
g2d.draw(arc);
|
double angle = startRad + i * stepRad;
|
||||||
|
int nextX = (int) Math.round(cx + rx * Math.cos(angle));
|
||||||
|
int nextY = (int) Math.round(cy - ry * Math.sin(angle));
|
||||||
|
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
|
||||||
|
prevX = nextX;
|
||||||
|
prevY = nextY;
|
||||||
}
|
}
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
}
|
||||||
@@ -184,53 +246,101 @@ public class GraphicsPlane {
|
|||||||
/**
|
/**
|
||||||
* Draws a Fillet (spline / curve approximation across control points).
|
* Draws a Fillet (spline / curve approximation across control points).
|
||||||
*/
|
*/
|
||||||
public synchronized void drawFillet(int[] px, int[] py, int numPoints, Color color, int lineType, int lineWidth) {
|
public synchronized void drawFillet(int[] px, int[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
|
||||||
if (g2d == null || numPoints < 2) return;
|
if (px == null || py == null || numPoints < 2) return;
|
||||||
g2d.setColor(color != null ? color : Color.WHITE);
|
|
||||||
g2d.setStroke(createStroke(lineType, lineWidth));
|
|
||||||
|
|
||||||
GeneralPath path = new GeneralPath();
|
|
||||||
path.moveTo(px[0], py[0]);
|
|
||||||
|
|
||||||
if (numPoints == 2) {
|
if (numPoints == 2) {
|
||||||
path.lineTo(px[1], py[1]);
|
drawLine(px[0], py[0], px[1], py[1], colorArgb, lineType, lineWidth);
|
||||||
} else {
|
return;
|
||||||
for (int i = 1; i < numPoints - 1; i++) {
|
|
||||||
double midX = (px[i] + px[i + 1]) / 2.0;
|
|
||||||
double midY = (py[i] + py[i + 1]) / 2.0;
|
|
||||||
path.quadTo(px[i], py[i], midX, midY);
|
|
||||||
}
|
|
||||||
path.lineTo(px[numPoints - 1], py[numPoints - 1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g2d.draw(path);
|
int prevX = px[0];
|
||||||
|
int prevY = py[0];
|
||||||
|
|
||||||
|
for (int i = 0; i < numPoints - 1; i++) {
|
||||||
|
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
|
||||||
|
double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0;
|
||||||
|
double p1x = px[i];
|
||||||
|
double p1y = py[i];
|
||||||
|
double p2x = (i == numPoints - 2) ? px[numPoints - 1] : (px[i] + px[i + 1]) / 2.0;
|
||||||
|
double p2y = (i == numPoints - 2) ? py[numPoints - 1] : (py[i] + py[i + 1]) / 2.0;
|
||||||
|
|
||||||
|
int steps = 20;
|
||||||
|
for (int s = 1; s <= steps; s++) {
|
||||||
|
double t = (double) s / steps;
|
||||||
|
double oneMinusT = 1.0 - t;
|
||||||
|
double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x;
|
||||||
|
double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y;
|
||||||
|
int nextX = (int) Math.round(bx);
|
||||||
|
int nextY = (int) Math.round(by);
|
||||||
|
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
|
||||||
|
prevX = nextX;
|
||||||
|
prevY = nextY;
|
||||||
|
}
|
||||||
|
}
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fills a closed polygon area with a solid color or hatching pattern.
|
* Fills a closed polygon area with a solid color or hatching pattern.
|
||||||
*/
|
*/
|
||||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, Color fillColor, int pattern,
|
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
|
||||||
boolean drawBoundary, Color boundaryColor, int lineType, int lineWidth) {
|
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) {
|
||||||
if (g2d == null || numPoints < 3) return;
|
if (px == null || py == null || numPoints < 3) return;
|
||||||
|
|
||||||
Polygon poly = new Polygon(px, py, numPoints);
|
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
|
||||||
|
|
||||||
if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) {
|
if (pattern != GocaConstants.PT_EMPTY) {
|
||||||
g2d.setColor(fillColor != null ? fillColor : Color.WHITE);
|
// Find polygon vertical bounds
|
||||||
g2d.fill(poly);
|
int minY = py[0];
|
||||||
} else if (pattern != GocaConstants.PT_EMPTY) {
|
int maxY = py[0];
|
||||||
// Fill with pattern texture
|
for (int i = 1; i < numPoints; i++) {
|
||||||
BufferedImage patImg = createPatternTexture(pattern, fillColor != null ? fillColor : Color.WHITE);
|
if (py[i] < minY) minY = py[i];
|
||||||
java.awt.TexturePaint tp = new java.awt.TexturePaint(patImg, new java.awt.Rectangle(0, 0, 8, 8));
|
if (py[i] > maxY) maxY = py[i];
|
||||||
g2d.setPaint(tp);
|
}
|
||||||
g2d.fill(poly);
|
minY = Math.max(0, minY);
|
||||||
|
maxY = Math.min(canvasHeight - 1, maxY);
|
||||||
|
|
||||||
|
List<Integer> nodeX = new ArrayList<>();
|
||||||
|
byte[] patRows = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0];
|
||||||
|
|
||||||
|
for (int y = minY; y <= maxY; y++) {
|
||||||
|
nodeX.clear();
|
||||||
|
int j = numPoints - 1;
|
||||||
|
for (int i = 0; i < numPoints; i++) {
|
||||||
|
if ((py[i] < y && py[j] >= y) || (py[j] < y && py[i] >= y)) {
|
||||||
|
int x = px[i] + (int) Math.round((double) (y - py[i]) / (py[j] - py[i]) * (px[j] - px[i]));
|
||||||
|
nodeX.add(x);
|
||||||
|
}
|
||||||
|
j = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (drawBoundary && boundaryColor != null) {
|
Collections.sort(nodeX);
|
||||||
g2d.setColor(boundaryColor);
|
|
||||||
g2d.setStroke(createStroke(lineType, lineWidth));
|
for (int i = 0; i < nodeX.size(); i += 2) {
|
||||||
g2d.draw(poly);
|
if (i + 1 >= nodeX.size()) break;
|
||||||
|
int leftX = Math.max(0, nodeX.get(i));
|
||||||
|
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
|
||||||
|
|
||||||
|
for (int x = leftX; x <= rightX; x++) {
|
||||||
|
if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) {
|
||||||
|
setPixel(x, y, fill);
|
||||||
|
} else {
|
||||||
|
int b = patRows[y & 7] & 0xFF;
|
||||||
|
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||||
|
setPixel(x, y, fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawBoundary && boundaryColorArgb != 0) {
|
||||||
|
for (int i = 0; i < numPoints; i++) {
|
||||||
|
int next = (i + 1) % numPoints;
|
||||||
|
drawLine(px[i], py[i], px[next], py[next], boundaryColorArgb, lineType, lineWidth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
}
|
||||||
@@ -238,67 +348,92 @@ public class GraphicsPlane {
|
|||||||
/**
|
/**
|
||||||
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle).
|
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle).
|
||||||
*/
|
*/
|
||||||
public synchronized void drawMarker(int x, int y, int markerType, int size, Color color) {
|
public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) {
|
||||||
if (g2d == null) return;
|
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
||||||
g2d.setColor(color != null ? color : Color.WHITE);
|
|
||||||
g2d.setStroke(new BasicStroke(1.5f));
|
|
||||||
|
|
||||||
int s = Math.max(3, size > 0 ? size : 5);
|
int s = Math.max(3, size > 0 ? size : 5);
|
||||||
|
|
||||||
switch (markerType) {
|
switch (markerType) {
|
||||||
case GocaConstants.MK_CROSS: // x
|
case GocaConstants.MK_CROSS: // x
|
||||||
g2d.drawLine(x - s, y - s, x + s, y + s);
|
drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
g2d.drawLine(x - s, y + s, x + s, y - s);
|
drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_PLUS: // +
|
case GocaConstants.MK_PLUS: // +
|
||||||
case GocaConstants.MK_DEFAULT:
|
case GocaConstants.MK_DEFAULT:
|
||||||
g2d.drawLine(x - s, y, x + s, y);
|
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
g2d.drawLine(x, y - s, x, y + s);
|
drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_DIAMOND: // <>
|
case GocaConstants.MK_DIAMOND: // <>
|
||||||
g2d.drawPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4);
|
drawLine(x, y - s, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
|
drawLine(x + s, y, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
|
drawLine(x, y + s, x - s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
|
drawLine(x - s, y, x, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_SQUARE: // []
|
case GocaConstants.MK_SQUARE: // []
|
||||||
g2d.drawRect(x - s, y - s, s * 2, s * 2);
|
drawLine(x - s, y - s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
|
drawLine(x + s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
|
drawLine(x + s, y + s, x - s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
|
drawLine(x - s, y + s, x - s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_6STAR: // 6-point star
|
case GocaConstants.MK_6STAR: // 6-point star
|
||||||
g2d.drawLine(x - s, y, x + s, y);
|
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
g2d.drawLine(x - s / 2, y - s, x + s / 2, y + s);
|
drawLine(x - s / 2, y - s, x + s / 2, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
g2d.drawLine(x - s / 2, y + s, x + s / 2, y - s);
|
drawLine(x - s / 2, y + s, x + s / 2, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_8STAR: // 8-point star
|
case GocaConstants.MK_8STAR: // 8-point star
|
||||||
g2d.drawLine(x - s, y, x + s, y);
|
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
g2d.drawLine(x, y - s, x, y + s);
|
drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
g2d.drawLine(x - s, y - s, x + s, y + s);
|
drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
g2d.drawLine(x - s, y + s, x + s, y - s);
|
drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_SDIAMOND: // solid diamond
|
case GocaConstants.MK_SDIAMOND: // solid diamond
|
||||||
g2d.fillPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4);
|
fillArea(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4,
|
||||||
|
color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_SSQUARE: // solid square
|
case GocaConstants.MK_SSQUARE: // solid square
|
||||||
g2d.fillRect(x - s, y - s, s * 2, s * 2);
|
fillArea(new int[]{x - s, x + s, x + s, x - s}, new int[]{y - s, y - s, y + s, y + s}, 4,
|
||||||
|
color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_DOT: // dot
|
case GocaConstants.MK_DOT: // dot
|
||||||
g2d.fillOval(x - 2, y - 2, 4, 4);
|
for (int dy = -2; dy <= 2; dy++) {
|
||||||
|
for (int dx = -2; dx <= 2; dx++) {
|
||||||
|
if (dx * dx + dy * dy <= 4) {
|
||||||
|
setPixel(x + dx, y + dy, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case GocaConstants.MK_CIRCLE: // circle
|
case GocaConstants.MK_CIRCLE: // circle
|
||||||
g2d.drawOval(x - s, y - s, s * 2, s * 2);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
g2d.drawOval(x - s, y - s, s * 2, s * 2);
|
drawArc(x, y, s, s, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final int[] VSS_OFFSETS = new int[256];
|
||||||
|
static {
|
||||||
|
Arrays.fill(VSS_OFFSETS, -1);
|
||||||
|
int sym = VectorSymbolData.VSS_SYMBOL_START; // 33
|
||||||
|
if (sym < 256) {
|
||||||
|
VSS_OFFSETS[sym] = 0;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < VectorSymbolData.vss_data.length; i++) {
|
||||||
|
if (VectorSymbolData.vss_data[i] == VectorSymbolData.END_DEFAULT) { // 0xFF
|
||||||
|
sym++;
|
||||||
|
if (sym < 256 && i + 1 < VectorSymbolData.vss_data.length) {
|
||||||
|
VSS_OFFSETS[sym] = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws stroked vector text using IBM Vector Symbol Set (VSS).
|
* Draws stroked vector text using IBM Vector Symbol Set (VSS).
|
||||||
*/
|
*/
|
||||||
public synchronized void drawVectorText(int x, int y, String text, Color color,
|
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
||||||
int cellWidth, int cellHeight, int dir, double angle) {
|
int cellWidth, int cellHeight, int dir, double angle) {
|
||||||
if (g2d == null || text == null || text.isEmpty()) return;
|
if (text == null || text.isEmpty()) return;
|
||||||
g2d.setColor(color != null ? color : Color.WHITE);
|
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
||||||
g2d.setStroke(new BasicStroke(1.2f));
|
|
||||||
|
|
||||||
int curX = x;
|
int curX = x;
|
||||||
int curY = y;
|
int curY = y;
|
||||||
@@ -307,7 +442,7 @@ public class GraphicsPlane {
|
|||||||
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
for (int i = 0; i < text.length(); i++) {
|
||||||
char c = text.charAt(i);
|
char c = text.charAt(i);
|
||||||
drawVssChar(curX, curY, c, cw, ch);
|
drawVssChar(curX, curY, c, color, cw, ch);
|
||||||
|
|
||||||
switch (dir) {
|
switch (dir) {
|
||||||
case GocaConstants.CD_TB: curY += ch; break;
|
case GocaConstants.CD_TB: curY += ch; break;
|
||||||
@@ -323,24 +458,54 @@ public class GraphicsPlane {
|
|||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawVssChar(int x, int y, char c, int cw, int ch) {
|
private void drawVssChar(int x, int y, char c, int color, int cw, int ch) {
|
||||||
int idx = (int) c;
|
int code = (int) c;
|
||||||
if (idx < VectorSymbolData.VSS_SYMBOL_START || idx > VectorSymbolData.VSS_SYMBOL_END) {
|
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
|
||||||
// Draw simple bounding box or space
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Fallback vector stroke rendering
|
int offset = VSS_OFFSETS[code];
|
||||||
g2d.drawString(String.valueOf(c), x, y + ch);
|
if (offset < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ptr = offset;
|
||||||
|
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||||
|
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||||
|
if (order == 0xC1) {
|
||||||
|
int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF;
|
||||||
|
int numPoints = byteLen / 4;
|
||||||
|
int dataPtr = ptr + 2;
|
||||||
|
|
||||||
|
if (numPoints >= 2) {
|
||||||
|
int prevVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
|
||||||
|
int prevVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
|
||||||
|
int prevPx = x + (int) Math.round(((double) prevVx / VectorSymbolData.VSS_WIDTH) * cw);
|
||||||
|
int prevPy = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - prevVy) / VectorSymbolData.VSS_HEIGHT) * ch);
|
||||||
|
|
||||||
|
for (int p = 1; p < numPoints; p++) {
|
||||||
|
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||||
|
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||||
|
int px = x + (int) Math.round(((double) vx / VectorSymbolData.VSS_WIDTH) * cw);
|
||||||
|
int py = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch);
|
||||||
|
|
||||||
|
drawLine(prevPx, prevPy, px, py, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||||
|
prevPx = px;
|
||||||
|
prevPy = py;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ptr += 2 + byteLen;
|
||||||
|
} else {
|
||||||
|
ptr++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws raw image pixel bitmap.
|
* Draws raw image pixel bitmap.
|
||||||
*/
|
*/
|
||||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, Color fgColor) {
|
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
|
||||||
if (g2d == null || imageData == null || width <= 0 || height <= 0) return;
|
if (imageData == null || width <= 0 || height <= 0) return;
|
||||||
|
int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF;
|
||||||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
|
||||||
int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF;
|
|
||||||
|
|
||||||
for (int row = 0; row < height; row++) {
|
for (int row = 0; row < height; row++) {
|
||||||
for (int col = 0; col < width; col++) {
|
for (int col = 0; col < width; col++) {
|
||||||
@@ -349,52 +514,11 @@ public class GraphicsPlane {
|
|||||||
if (byteIdx < imageData.length) {
|
if (byteIdx < imageData.length) {
|
||||||
boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0;
|
boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0;
|
||||||
if (bit) {
|
if (bit) {
|
||||||
img.setRGB(col, row, fgRgb);
|
setPixel(x + col, y + row, fgColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
g2d.drawImage(img, x, y, null);
|
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Stroke createStroke(int lineType, int lineWidth) {
|
|
||||||
float width = (lineWidth == GocaConstants.LW_THICK) ? 2.5f : 1.2f;
|
|
||||||
|
|
||||||
switch (lineType) {
|
|
||||||
case GocaConstants.LT_DOT:
|
|
||||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f);
|
|
||||||
case GocaConstants.LT_SHORTDASH:
|
|
||||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{4.0f, 2.0f}, 0.0f);
|
|
||||||
case GocaConstants.LT_DASHDOT:
|
|
||||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{6.0f, 2.0f, 2.0f, 2.0f}, 0.0f);
|
|
||||||
case GocaConstants.LT_DOUBLEDOT:
|
|
||||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f, 2.0f, 4.0f}, 0.0f);
|
|
||||||
case GocaConstants.LT_LONGDASH:
|
|
||||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 3.0f}, 0.0f);
|
|
||||||
case GocaConstants.LT_DASHDOUBLEDOT:
|
|
||||||
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f}, 0.0f);
|
|
||||||
case GocaConstants.LT_SOLID:
|
|
||||||
case GocaConstants.LT_DEFAULT:
|
|
||||||
default:
|
|
||||||
return new BasicStroke(width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private BufferedImage createPatternTexture(int patternIndex, Color color) {
|
|
||||||
BufferedImage pat = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB);
|
|
||||||
byte[] patternRows = (patternIndex >= 0 && patternIndex < PATTERN_DATA.length) ? PATTERN_DATA[patternIndex] : PATTERN_DATA[0];
|
|
||||||
int rgb = color.getRGB();
|
|
||||||
|
|
||||||
for (int r = 0; r < 8; r++) {
|
|
||||||
int b = patternRows[r] & 0xFF;
|
|
||||||
for (int c = 0; c < 8; c++) {
|
|
||||||
if (((b >> (7 - c)) & 1) != 0) {
|
|
||||||
pat.setRGB(c, r, rgb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pat;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.lib3270j.graphics;
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -19,12 +17,31 @@ public class ProgramSymbolManager {
|
|||||||
public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; // RWS 4..7 (Sets 2..5)
|
public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; // RWS 4..7 (Sets 2..5)
|
||||||
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
|
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
|
||||||
|
|
||||||
|
private int defaultCellWidth = 9;
|
||||||
|
private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12)
|
||||||
|
|
||||||
|
public void setDefaultCellDimensions(int width, int height) {
|
||||||
|
this.defaultCellWidth = (width > 0) ? width : 9;
|
||||||
|
this.defaultCellHeight = (height > 0) ? height : 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDefaultCellWidth() {
|
||||||
|
return defaultCellWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDefaultCellHeight() {
|
||||||
|
return defaultCellHeight;
|
||||||
|
}
|
||||||
|
|
||||||
private final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
private final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||||
|
private final ProgramSymbolSet[] stagingSets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||||
|
private final ProgramSymbolSet[] lcidMap = new ProgramSymbolSet[256];
|
||||||
|
|
||||||
public ProgramSymbolManager() {
|
public ProgramSymbolManager() {
|
||||||
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||||
boolean isTriple = (i >= NUMBER_SINGLE_PLANE_PS_SETS && i < NUMBER_SINGLE_PLANE_PS_SETS + NUMBER_TRIPLE_PLANE_PS_SETS);
|
boolean isTriple = (i >= NUMBER_SINGLE_PLANE_PS_SETS && i < NUMBER_SINGLE_PLANE_PS_SETS + NUMBER_TRIPLE_PLANE_PS_SETS);
|
||||||
sets[i] = new ProgramSymbolSet(isTriple);
|
sets[i] = new ProgramSymbolSet(isTriple);
|
||||||
|
stagingSets[i] = new ProgramSymbolSet(isTriple);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,32 +49,67 @@ public class ProgramSymbolManager {
|
|||||||
* Resets all symbol sets.
|
* Resets all symbol sets.
|
||||||
*/
|
*/
|
||||||
public synchronized void clearAll() {
|
public synchronized void clearAll() {
|
||||||
for (ProgramSymbolSet set : sets) {
|
Arrays.fill(lcidMap, null);
|
||||||
set.clear();
|
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||||
set.setLcid(0);
|
sets[i].clear();
|
||||||
|
sets[i].setLcid(0);
|
||||||
|
stagingSets[i].clear();
|
||||||
|
stagingSets[i].setLcid(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits all staged multi-plane symbol sets to the active presentation sets atomically.
|
||||||
|
*/
|
||||||
|
public synchronized void commitStagedSymbols() {
|
||||||
|
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||||
|
int lcid = stagingSets[i].getLcid();
|
||||||
|
if (lcid > 0) {
|
||||||
|
ProgramSymbolSet staged = stagingSets[i];
|
||||||
|
ProgramSymbolSet active = sets[i];
|
||||||
|
active.setLcid(lcid);
|
||||||
|
for (int slot = 0; slot < ProgramSymbolSet.NUM_SLOTS; slot++) {
|
||||||
|
active.setSlot(slot, staged.getSlot(slot));
|
||||||
|
}
|
||||||
|
if (lcid < 256) {
|
||||||
|
lcidMap[lcid] = active;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE).
|
* Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE).
|
||||||
*/
|
*/
|
||||||
public synchronized ProgramSymbolSet getSymbolSet(int lcid) {
|
public ProgramSymbolSet getSymbolSet(int lcid) {
|
||||||
if (lcid <= 0) {
|
if (lcid <= 0 || lcid >= 256) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (ProgramSymbolSet set : sets) {
|
ProgramSymbolSet set = lcidMap[lcid];
|
||||||
if (set.getLcid() == lcid) {
|
if (set == null) {
|
||||||
|
for (ProgramSymbolSet s : stagingSets) {
|
||||||
|
if (s.getLcid() == lcid) return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 synchronized ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
|
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
|
||||||
ProgramSymbolSet set = getSymbolSet(lcid);
|
if (lcid <= 0 || lcid >= 256) {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -65,21 +117,6 @@ public class ProgramSymbolManager {
|
|||||||
return set.getSlot(index);
|
return set.getSlot(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a programmed symbol character cell if defined.
|
|
||||||
* Returns true if symbol was drawn, false if not found.
|
|
||||||
*/
|
|
||||||
public synchronized boolean drawSymbol(Graphics2D g2d, int lcid, int codePoint,
|
|
||||||
int x, int y, int cellWidth, int cellHeight,
|
|
||||||
Color fgColor, Color bgColor) {
|
|
||||||
ProgramSymbolSet.SymbolSlot slot = getSymbol(lcid, codePoint);
|
|
||||||
if (slot != null) {
|
|
||||||
slot.draw(g2d, x, y, cellWidth, cellHeight, fgColor, bgColor);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload.
|
* Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload.
|
||||||
*/
|
*/
|
||||||
@@ -115,12 +152,12 @@ public class ProgramSymbolManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgramSymbolSet set = sets[setIndex];
|
|
||||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||||
|
ProgramSymbolSet set = isTriplePlane ? stagingSets[setIndex] : sets[setIndex];
|
||||||
|
|
||||||
int extHeaderLen = 0;
|
int extHeaderLen = 0;
|
||||||
int cellWidth = 9;
|
int cellWidth = defaultCellWidth;
|
||||||
int cellHeight = 16;
|
int cellHeight = defaultCellHeight;
|
||||||
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
||||||
|
|
||||||
if (hasExtHeader && data.length > 4) {
|
if (hasExtHeader && data.length > 4) {
|
||||||
@@ -138,10 +175,10 @@ public class ProgramSymbolManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clearAll) {
|
|
||||||
set.clear();
|
|
||||||
}
|
|
||||||
set.setLcid(lcid);
|
set.setLcid(lcid);
|
||||||
|
if (!isTriplePlane && lcid > 0 && lcid < 256) {
|
||||||
|
lcidMap[lcid] = set;
|
||||||
|
}
|
||||||
|
|
||||||
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
|
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
|
||||||
int remaining = data.length - offset;
|
int remaining = data.length - offset;
|
||||||
@@ -159,12 +196,10 @@ public class ProgramSymbolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||||
byte[] pixelData;
|
byte[] pixelData = new byte[cellWidth * cellHeight];
|
||||||
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
||||||
if (existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||||
pixelData = existing.getPixelData();
|
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
||||||
} else {
|
|
||||||
pixelData = new byte[cellWidth * cellHeight];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadFormat == 1) {
|
if (loadFormat == 1) {
|
||||||
@@ -180,10 +215,18 @@ public class ProgramSymbolManager {
|
|||||||
remaining -= bytesPerSymbol;
|
remaining -= bytesPerSymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
if (clearAll) {
|
||||||
|
for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) {
|
||||||
|
set.clearSlot(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger.isLoggable(Level.FINE)) {
|
||||||
|
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
||||||
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
||||||
cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint)));
|
cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unpacks Format 1 (9x16) symbol slice bit-pattern.
|
* Unpacks Format 1 (9x16) symbol slice bit-pattern.
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
package org.lib3270j.graphics;
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.awt.Image;
|
|
||||||
import java.awt.RenderingHints;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
||||||
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
|
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
|
||||||
@@ -67,9 +61,15 @@ public class ProgramSymbolSet {
|
|||||||
private final int height;
|
private final int height;
|
||||||
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
||||||
private final boolean isTriplePlane;
|
private final boolean isTriplePlane;
|
||||||
private BufferedImage cachedImage;
|
private int[] cachedRgbArray;
|
||||||
private int cachedFgRgb = -1;
|
private int cachedFgRgb = -1;
|
||||||
private int cachedBgRgb = -1;
|
private int cachedBgRgb = -1;
|
||||||
|
private java.awt.image.BufferedImage cachedImage;
|
||||||
|
private java.awt.image.BufferedImage cachedScaledImage;
|
||||||
|
private int cachedTargetW = 0;
|
||||||
|
private int cachedTargetH = 0;
|
||||||
|
private int cachedScaledFgRgb = -1;
|
||||||
|
private int cachedScaledBgRgb = -1;
|
||||||
|
|
||||||
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
||||||
this.width = width > 0 ? width : 9;
|
this.width = width > 0 ? width : 9;
|
||||||
@@ -95,28 +95,80 @@ public class ProgramSymbolSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders this symbol to a BufferedImage.
|
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
||||||
|
* Enables unscaled 1:1 hardware blitting in Java2D.
|
||||||
*/
|
*/
|
||||||
public synchronized BufferedImage getImage(Color fgColor, Color bgColor) {
|
public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||||
int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF;
|
if (targetW <= 0 || targetH <= 0) {
|
||||||
int bgRgb = bgColor != null ? bgColor.getRGB() : 0x00000000;
|
return getImage(fgArgb, bgArgb);
|
||||||
|
}
|
||||||
|
if (targetW == width && targetH == height) {
|
||||||
|
return getImage(fgArgb, bgArgb);
|
||||||
|
}
|
||||||
|
if (cachedScaledImage != null && cachedTargetW == targetW && cachedTargetH == targetH
|
||||||
|
&& cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) {
|
||||||
|
return cachedScaledImage;
|
||||||
|
}
|
||||||
|
int[] srcRgb = getRgbPixels(fgArgb, bgArgb);
|
||||||
|
java.awt.image.BufferedImage scaled = new java.awt.image.BufferedImage(targetW, targetH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||||
|
int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData();
|
||||||
|
|
||||||
if (cachedImage != null && cachedFgRgb == fgRgb && cachedBgRgb == bgRgb) {
|
for (int dy = 0; dy < targetH; dy++) {
|
||||||
|
int sy = dy * height / targetH;
|
||||||
|
int srcRowOffset = sy * width;
|
||||||
|
int dstRowOffset = dy * targetW;
|
||||||
|
for (int dx = 0; dx < targetW; dx++) {
|
||||||
|
int sx = dx * width / targetW;
|
||||||
|
dstRgb[dstRowOffset + dx] = srcRgb[srcRowOffset + sx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedScaledImage = scaled;
|
||||||
|
this.cachedTargetW = targetW;
|
||||||
|
this.cachedTargetH = targetH;
|
||||||
|
this.cachedScaledFgRgb = fgArgb;
|
||||||
|
this.cachedScaledBgRgb = bgArgb;
|
||||||
|
return scaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes and returns the cached BufferedImage for this symbol glyph.
|
||||||
|
* Eliminates per-cell heap allocations during high frame rate rendering.
|
||||||
|
*/
|
||||||
|
public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) {
|
||||||
|
if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||||
return cachedImage;
|
return cachedImage;
|
||||||
|
}
|
||||||
|
int[] rgb = getRgbPixels(fgArgb, bgArgb);
|
||||||
|
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||||
|
int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||||
|
System.arraycopy(rgb, 0, imgData, 0, rgb.length);
|
||||||
|
this.cachedImage = img;
|
||||||
|
this.cachedFgRgb = fgArgb;
|
||||||
|
this.cachedBgRgb = bgArgb;
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes and returns the 32-bit ARGB pixel array for this symbol.
|
||||||
|
* The returned array has length (width * height).
|
||||||
|
*/
|
||||||
|
public synchronized int[] getRgbPixels(int fgArgb, int bgArgb) {
|
||||||
|
if (cachedRgbArray != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||||
|
return cachedRgbArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
|
||||||
int[] rgbArray = new int[width * height];
|
int[] rgbArray = new int[width * height];
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
int idx = y * width + x;
|
int idx = y * width + x;
|
||||||
int val = (idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0;
|
int val = (pixelData != null && idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0;
|
||||||
|
|
||||||
if (val == 0) {
|
if (val == 0) {
|
||||||
rgbArray[idx] = bgRgb;
|
rgbArray[idx] = bgArgb;
|
||||||
} else if (!isTriplePlane) {
|
} else if (!isTriplePlane) {
|
||||||
rgbArray[idx] = fgRgb;
|
rgbArray[idx] = fgArgb;
|
||||||
} else {
|
} else {
|
||||||
// Triple-Plane RGB composite:
|
// Triple-Plane RGB composite:
|
||||||
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
||||||
@@ -128,21 +180,10 @@ public class ProgramSymbolSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img.setRGB(0, 0, width, height, rgbArray, 0, width);
|
this.cachedRgbArray = rgbArray;
|
||||||
this.cachedImage = img;
|
this.cachedFgRgb = fgArgb;
|
||||||
this.cachedFgRgb = fgRgb;
|
this.cachedBgRgb = bgArgb;
|
||||||
this.cachedBgRgb = bgRgb;
|
return rgbArray;
|
||||||
return img;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws the symbol directly onto a Graphics2D surface with aspect scaling.
|
|
||||||
*/
|
|
||||||
public void draw(Graphics2D g2d, int x, int y, int cellWidth, int cellHeight, Color fgColor, Color bgColor) {
|
|
||||||
BufferedImage img = getImage(fgColor, bgColor);
|
|
||||||
if (img != null) {
|
|
||||||
g2d.drawImage(img, x, y, cellWidth, cellHeight, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,6 +207,7 @@ public class InputProcessor {
|
|||||||
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
||||||
screen.setCursorAddress(nextRowAddr);
|
screen.setCursorAddress(nextRowAddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
setKeyboardLocked(false);
|
setKeyboardLocked(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -309,6 +311,7 @@ public class InputProcessor {
|
|||||||
addr -= screen.getCols();
|
addr -= screen.getCols();
|
||||||
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorDown() {
|
public void cursorDown() {
|
||||||
@@ -316,18 +319,21 @@ public class InputProcessor {
|
|||||||
addr += screen.getCols();
|
addr += screen.getCols();
|
||||||
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorLeft() {
|
public void cursorLeft() {
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
addr = screen.decrementAddress(addr);
|
addr = screen.decrementAddress(addr);
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorRight() {
|
public void cursorRight() {
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
addr = screen.incrementAddress(addr);
|
addr = screen.incrementAddress(addr);
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorHome() {
|
public void cursorHome() {
|
||||||
@@ -336,11 +342,13 @@ public class InputProcessor {
|
|||||||
} else {
|
} else {
|
||||||
screen.setCursorAddress(0);
|
screen.setCursorAddress(0);
|
||||||
}
|
}
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tab() {
|
public void tab() {
|
||||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLastAid() { return lastAid; }
|
public int getLastAid() { return lastAid; }
|
||||||
@@ -348,6 +356,7 @@ public class InputProcessor {
|
|||||||
|
|
||||||
public void setCursorAddress(int baddr) {
|
public void setCursorAddress(int baddr) {
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backTab() {
|
public void backTab() {
|
||||||
@@ -361,6 +370,7 @@ public class InputProcessor {
|
|||||||
if (screen.getCell(addr).isFieldAttribute()) {
|
if (screen.getCell(addr).isFieldAttribute()) {
|
||||||
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
||||||
screen.setCursorAddress(screen.incrementAddress(addr));
|
screen.setCursorAddress(screen.incrementAddress(addr));
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,6 +387,7 @@ public class InputProcessor {
|
|||||||
ea.ucs4 = 0;
|
ea.ucs4 = 0;
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
@@ -398,6 +409,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteChar() {
|
public void deleteChar() {
|
||||||
@@ -426,6 +438,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backspace() {
|
public void backspace() {
|
||||||
@@ -439,6 +452,7 @@ public class InputProcessor {
|
|||||||
if (keyboardLocked) return;
|
if (keyboardLocked) return;
|
||||||
screen.eraseAllUnprotected();
|
screen.eraseAllUnprotected();
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
|
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
|
||||||
@@ -455,6 +469,7 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
screen.setCursorAddress(target);
|
screen.setCursorAddress(target);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert Duplicate (DUP) code and advance to next field. */
|
/** Insert Duplicate (DUP) code and advance to next field. */
|
||||||
@@ -472,6 +487,8 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
tab();
|
tab();
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert Field Mark (FM) code. */
|
/** Insert Field Mark (FM) code. */
|
||||||
@@ -495,6 +512,7 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attention key (sends Telnet IP). */
|
/** Attention key (sends Telnet IP). */
|
||||||
@@ -571,6 +589,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(i).ucs4 = 0;
|
screen.getCell(i).ucs4 = 0;
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +627,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return fieldLen;
|
return fieldLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,12 @@ public class ScreenBuffer {
|
|||||||
private byte defaultGr = 0x00;
|
private byte defaultGr = 0x00;
|
||||||
private byte defaultCs = 0x00;
|
private byte defaultCs = 0x00;
|
||||||
private byte defaultIc = 0x00;
|
private byte defaultIc = 0x00;
|
||||||
|
|
||||||
private final EbcdicTranslator translator;
|
private final EbcdicTranslator translator;
|
||||||
|
private final Object renderLock = new Object();
|
||||||
|
|
||||||
|
public Object getRenderLock() {
|
||||||
|
return renderLock;
|
||||||
|
}
|
||||||
|
|
||||||
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
|
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
|
||||||
this.translator = translator;
|
this.translator = translator;
|
||||||
@@ -56,6 +60,7 @@ public class ScreenBuffer {
|
|||||||
defaultFA.ic = 1;
|
defaultFA.ic = 1;
|
||||||
|
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void allocateBuffers() {
|
private void allocateBuffers() {
|
||||||
@@ -79,6 +84,50 @@ public class ScreenBuffer {
|
|||||||
return buffer[addr];
|
return buffer[addr];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ExtendedAttribute[] displaySnapshot;
|
||||||
|
private int displayRows;
|
||||||
|
private int displayCols;
|
||||||
|
private int displayCursorAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically creates a snapshot of the current presentation buffer for tear-free rendering.
|
||||||
|
* Takes ~2 microseconds and eliminates mutual thread contention with the UI thread.
|
||||||
|
*/
|
||||||
|
public synchronized void updateDisplaySnapshot() {
|
||||||
|
int size = rows * cols;
|
||||||
|
if (displaySnapshot == null || displaySnapshot.length < size) {
|
||||||
|
displaySnapshot = new ExtendedAttribute[size];
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
displaySnapshot[i] = new ExtendedAttribute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
displaySnapshot[i].copyFrom(buffer[i]);
|
||||||
|
}
|
||||||
|
this.displayRows = rows;
|
||||||
|
this.displayCols = cols;
|
||||||
|
this.displayCursorAddress = cursorAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized ExtendedAttribute getDisplayCell(int addr) {
|
||||||
|
if (displaySnapshot == null || addr < 0 || addr >= displayRows * displayCols) {
|
||||||
|
return getCell(addr);
|
||||||
|
}
|
||||||
|
return displaySnapshot[addr];
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getDisplayRows() {
|
||||||
|
return displayRows > 0 ? displayRows : rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getDisplayCols() {
|
||||||
|
return displayCols > 0 ? displayCols : cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getDisplayCursorAddress() {
|
||||||
|
return displayRows > 0 ? displayCursorAddress : cursorAddress;
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Dimension accessors ==========
|
// ========== Dimension accessors ==========
|
||||||
public int getRows() { return rows; }
|
public int getRows() { return rows; }
|
||||||
public int getCols() { return cols; }
|
public int getCols() { return cols; }
|
||||||
@@ -91,7 +140,7 @@ public class ScreenBuffer {
|
|||||||
public boolean isScreenAlt() { return screenAlt; }
|
public boolean isScreenAlt() { return screenAlt; }
|
||||||
|
|
||||||
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
|
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
|
||||||
public void setAlternateDimensions(int newAltRows, int newAltCols) {
|
public synchronized void setAlternateDimensions(int newAltRows, int newAltCols) {
|
||||||
if (newAltRows == altRows && newAltCols == altCols) return;
|
if (newAltRows == altRows && newAltCols == altCols) return;
|
||||||
this.altRows = newAltRows;
|
this.altRows = newAltRows;
|
||||||
this.altCols = newAltCols;
|
this.altCols = newAltCols;
|
||||||
@@ -101,11 +150,15 @@ public class ScreenBuffer {
|
|||||||
this.maxCols = Math.max(maxCols, newAltCols);
|
this.maxCols = Math.max(maxCols, newAltCols);
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
}
|
}
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Cursor ==========
|
// ========== Cursor ==========
|
||||||
public int getCursorAddress() { return cursorAddress; }
|
public int getCursorAddress() { return cursorAddress; }
|
||||||
public void setCursorAddress(int addr) { this.cursorAddress = addr; }
|
public synchronized void setCursorAddress(int addr) {
|
||||||
|
this.cursorAddress = addr;
|
||||||
|
this.displayCursorAddress = addr;
|
||||||
|
}
|
||||||
public int getCursorRow() { return cursorAddress / cols; }
|
public int getCursorRow() { return cursorAddress / cols; }
|
||||||
public int getCursorCol() { return cursorAddress % cols; }
|
public int getCursorCol() { return cursorAddress % cols; }
|
||||||
|
|
||||||
@@ -120,20 +173,22 @@ public class ScreenBuffer {
|
|||||||
/**
|
/**
|
||||||
* Perform an erase, optionally using the alternate screen size.
|
* Perform an erase, optionally using the alternate screen size.
|
||||||
*/
|
*/
|
||||||
public void erase(boolean alt) {
|
public synchronized void erase(boolean alt) {
|
||||||
clear();
|
clear();
|
||||||
int newRows = alt ? altRows : defRows;
|
int newRows = alt ? altRows : defRows;
|
||||||
int newCols = alt ? altCols : defCols;
|
int newCols = alt ? altCols : defCols;
|
||||||
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
||||||
|
updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rows = newRows;
|
rows = newRows;
|
||||||
cols = newCols;
|
cols = newCols;
|
||||||
screenAlt = alt;
|
screenAlt = alt;
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear the entire buffer. */
|
/** Clear the entire buffer. */
|
||||||
public void clear() {
|
public synchronized void clear() {
|
||||||
for (ExtendedAttribute ea : buffer) {
|
for (ExtendedAttribute ea : buffer) {
|
||||||
ea.clear();
|
ea.clear();
|
||||||
}
|
}
|
||||||
@@ -148,12 +203,13 @@ public class ScreenBuffer {
|
|||||||
defaultCs = 0x00;
|
defaultCs = 0x00;
|
||||||
defaultIc = 0x00;
|
defaultIc = 0x00;
|
||||||
replyMode = SF_SRM_FIELD;
|
replyMode = SF_SRM_FIELD;
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erase all unprotected fields.
|
* Erase all unprotected fields.
|
||||||
*/
|
*/
|
||||||
public void eraseAllUnprotected() {
|
public synchronized void eraseAllUnprotected() {
|
||||||
int size = rows * cols;
|
int size = rows * cols;
|
||||||
boolean inUnprotected = false;
|
boolean inUnprotected = false;
|
||||||
|
|
||||||
@@ -181,6 +237,7 @@ public class ScreenBuffer {
|
|||||||
// Move cursor to first unprotected field
|
// Move cursor to first unprotected field
|
||||||
cursorAddress = findNextUnprotected(0);
|
cursorAddress = findNextUnprotected(0);
|
||||||
screenChanged = true;
|
screenChanged = true;
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Field attribute navigation ==========
|
// ========== Field attribute navigation ==========
|
||||||
@@ -274,7 +331,7 @@ public class ScreenBuffer {
|
|||||||
|
|
||||||
// ========== Setters for model reconfiguration ==========
|
// ========== Setters for model reconfiguration ==========
|
||||||
|
|
||||||
public void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
|
public synchronized void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
|
||||||
int altRows, int altCols) {
|
int altRows, int altCols) {
|
||||||
this.maxRows = maxRows;
|
this.maxRows = maxRows;
|
||||||
this.maxCols = maxCols;
|
this.maxCols = maxCols;
|
||||||
@@ -285,6 +342,7 @@ public class ScreenBuffer {
|
|||||||
this.rows = defRows;
|
this.rows = defRows;
|
||||||
this.cols = defCols;
|
this.cols = defCols;
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void translateToUnicode() {
|
public void translateToUnicode() {
|
||||||
|
|||||||
@@ -149,9 +149,7 @@ public class TelnetConnection {
|
|||||||
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < n; i++) {
|
fsm.feedBytes(buf, 0, n);
|
||||||
fsm.feedByte(buf[i] & 0xFF);
|
|
||||||
}
|
|
||||||
fsm.endOfNetworkData();
|
fsm.endOfNetworkData();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
|
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ public class TelnetFSM {
|
|||||||
private int eXmitSeq;
|
private int eXmitSeq;
|
||||||
private int responseRequired = RSF_NO_RESPONSE;
|
private int responseRequired = RSF_NO_RESPONSE;
|
||||||
private boolean deferredWillTtype;
|
private boolean deferredWillTtype;
|
||||||
|
private boolean tn3270eDeviceTypeSent;
|
||||||
|
|
||||||
// Connection references
|
// Connection references
|
||||||
private TelnetConnection connection;
|
private TelnetConnection connection;
|
||||||
@@ -103,6 +104,7 @@ public class TelnetFSM {
|
|||||||
java.util.Arrays.fill(hisOpts, false);
|
java.util.Arrays.fill(hisOpts, false);
|
||||||
java.util.Arrays.fill(eFuncs, false);
|
java.util.Arrays.fill(eFuncs, false);
|
||||||
tn3270eNegotiated = false;
|
tn3270eNegotiated = false;
|
||||||
|
tn3270eDeviceTypeSent = false;
|
||||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||||
tn3270eBound = false;
|
tn3270eBound = false;
|
||||||
eXmitSeq = 0;
|
eXmitSeq = 0;
|
||||||
@@ -118,6 +120,37 @@ public class TelnetFSM {
|
|||||||
changeState(ConnectionState.TELNET_PENDING);
|
changeState(ConnectionState.TELNET_PENDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feed a bulk buffer of bytes from the network into the FSM.
|
||||||
|
*/
|
||||||
|
public void feedBytes(byte[] buf, int offset, int len) {
|
||||||
|
int end = offset + len;
|
||||||
|
int i = offset;
|
||||||
|
while (i < end) {
|
||||||
|
if (state == TNS_DATA) {
|
||||||
|
int start = i;
|
||||||
|
while (i < end && (buf[i] & 0xFF) != IAC) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i > start) {
|
||||||
|
if (connectionState == ConnectionState.TELNET_PENDING) {
|
||||||
|
changeState(ConnectionState.CONNECTED_NVT);
|
||||||
|
}
|
||||||
|
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||||
|
ibuf.write(buf, start, i - start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < end) {
|
||||||
|
state = TNS_IAC;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
feedByte(buf[i] & 0xFF);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed a single byte from the network into the FSM.
|
* Feed a single byte from the network into the FSM.
|
||||||
*/
|
*/
|
||||||
@@ -172,8 +205,8 @@ public class TelnetFSM {
|
|||||||
changeState(ConnectionState.CONNECTED_NVT);
|
changeState(ConnectionState.CONNECTED_NVT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states)
|
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
|
||||||
if (connectionState.is3270() || connectionState.isTn3270e()) {
|
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||||
ibuf.write(c);
|
ibuf.write(c);
|
||||||
}
|
}
|
||||||
// NVT data would go to NVT processor (not implemented in initial version)
|
// NVT data would go to NVT processor (not implemented in initial version)
|
||||||
@@ -189,7 +222,7 @@ public class TelnetFSM {
|
|||||||
break;
|
break;
|
||||||
case EOR: // End of record — process accumulated 3270 data
|
case EOR: // End of record — process accumulated 3270 data
|
||||||
log.fine("RCVD EOR");
|
log.fine("RCVD EOR");
|
||||||
if (connectionState.is3270() || connectionState.isTn3270e()) {
|
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||||
processEndOfRecord();
|
processEndOfRecord();
|
||||||
}
|
}
|
||||||
ibuf.reset();
|
ibuf.reset();
|
||||||
@@ -235,7 +268,9 @@ public class TelnetFSM {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case TELOPT_TN3270E:
|
case TELOPT_TN3270E:
|
||||||
if (!hisOpts[opt]) {
|
if (!config.isTn3270eEnabled()) {
|
||||||
|
sendCommand(DONT, opt);
|
||||||
|
} else if (!hisOpts[opt]) {
|
||||||
hisOpts[opt] = true;
|
hisOpts[opt] = true;
|
||||||
sendCommand(DO, opt);
|
sendCommand(DO, opt);
|
||||||
}
|
}
|
||||||
@@ -278,7 +313,7 @@ public class TelnetFSM {
|
|||||||
case TELOPT_TTYPE:
|
case TELOPT_TTYPE:
|
||||||
if (!myOpts[opt]) {
|
if (!myOpts[opt]) {
|
||||||
myOpts[opt] = true;
|
myOpts[opt] = true;
|
||||||
if (hisOpts[TELOPT_TN3270E]) {
|
if (config.isTn3270eEnabled() && hisOpts[TELOPT_TN3270E]) {
|
||||||
// Defer TTYPE response until TN3270E negotiation completes
|
// Defer TTYPE response until TN3270E negotiation completes
|
||||||
deferredWillTtype = true;
|
deferredWillTtype = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -288,11 +323,16 @@ public class TelnetFSM {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case TELOPT_TN3270E:
|
case TELOPT_TN3270E:
|
||||||
if (!myOpts[opt]) {
|
if (!config.isTn3270eEnabled()) {
|
||||||
|
sendCommand(WONT, opt);
|
||||||
|
} else if (!myOpts[opt]) {
|
||||||
myOpts[opt] = true;
|
myOpts[opt] = true;
|
||||||
sendCommand(WILL, opt);
|
sendCommand(WILL, opt);
|
||||||
// Start TN3270E sub-negotiation: send device type request
|
// Start TN3270E sub-negotiation: send device type request
|
||||||
|
if (!tn3270eDeviceTypeSent) {
|
||||||
sendTN3270EDeviceTypeRequest();
|
sendTN3270EDeviceTypeRequest();
|
||||||
|
tn3270eDeviceTypeSent = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -401,7 +441,7 @@ public class TelnetFSM {
|
|||||||
out.write(IAC);
|
out.write(IAC);
|
||||||
out.write(SE);
|
out.write(SE);
|
||||||
sendBytes(out.toByteArray());
|
sendBytes(out.toByteArray());
|
||||||
log.info("SENT SB TTYPE IS " + termType + " SE");
|
log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +468,10 @@ public class TelnetFSM {
|
|||||||
switch (op) {
|
switch (op) {
|
||||||
case OP_SEND:
|
case OP_SEND:
|
||||||
// Host asks us to send device-type request
|
// Host asks us to send device-type request
|
||||||
|
if (!tn3270eDeviceTypeSent) {
|
||||||
sendTN3270EDeviceTypeRequest();
|
sendTN3270EDeviceTypeRequest();
|
||||||
|
tn3270eDeviceTypeSent = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OP_DEVICE_TYPE:
|
case OP_DEVICE_TYPE:
|
||||||
@@ -475,7 +518,7 @@ public class TelnetFSM {
|
|||||||
out.write(IAC);
|
out.write(IAC);
|
||||||
out.write(SE);
|
out.write(SE);
|
||||||
sendBytes(out.toByteArray());
|
sendBytes(out.toByteArray());
|
||||||
log.info("SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
|
log.warning(">>> SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
|
||||||
(config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE");
|
(config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,14 +531,19 @@ public class TelnetFSM {
|
|||||||
pos++;
|
pos++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if REJECT
|
||||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
|
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
|
||||||
// Rejection
|
|
||||||
pos++;
|
pos++;
|
||||||
int reason = -1;
|
int reason = (pos < data.length) ? (data[pos] & 0xFF) : REASON_UNSUPPORTED_REQ;
|
||||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
|
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
|
||||||
pos++;
|
// Try fallback model 2 if we were requesting something else
|
||||||
if (pos < data.length) {
|
if (config.getModel() != TerminalModel.IBM_3278_2 &&
|
||||||
reason = data[pos] & 0xFF;
|
config.getModel() != TerminalModel.IBM_3279_2) {
|
||||||
|
log.warning("TN3270E device-type rejected (" +
|
||||||
|
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
|
||||||
|
config.setModel(TerminalModel.IBM_3278_2);
|
||||||
|
sendTN3270EDeviceTypeRequest();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
|
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
|
||||||
@@ -563,33 +611,86 @@ public class TelnetFSM {
|
|||||||
log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE");
|
log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTN3270EFunctions(byte[] data) {
|
private void sendTN3270EFunctionsIs() {
|
||||||
// Parse: TN3270E FUNCTIONS IS [func...]
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
int pos = 2; // Skip TN3270E, FUNCTIONS
|
out.write(IAC);
|
||||||
|
out.write(SB);
|
||||||
|
out.write(TELOPT_TN3270E);
|
||||||
|
out.write(OP_FUNCTIONS);
|
||||||
|
out.write(OP_IS);
|
||||||
|
|
||||||
if (pos < data.length && (data[pos] & 0xFF) == OP_IS) {
|
StringBuilder funcNames = new StringBuilder();
|
||||||
pos++; // Skip IS
|
for (int i = 0; i < eFuncs.length; i++) {
|
||||||
|
if (eFuncs[i]) {
|
||||||
|
out.write(i);
|
||||||
|
if (funcNames.length() > 0) funcNames.append(" ");
|
||||||
|
funcNames.append(TN3270EConstants.functionName(i));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The remaining bytes are the agreed-upon functions
|
out.write(IAC);
|
||||||
java.util.Arrays.fill(eFuncs, false);
|
out.write(SE);
|
||||||
StringBuilder funcNames = new StringBuilder();
|
sendBytes(out.toByteArray());
|
||||||
|
log.info("SENT SB TN3270E FUNCTIONS IS " + funcNames + " SE");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTN3270EFunctions(byte[] data) {
|
||||||
|
// Parse: TN3270E FUNCTIONS REQUEST [func...] or TN3270E FUNCTIONS IS [func...]
|
||||||
|
int pos = 1;
|
||||||
|
boolean isRequest = false;
|
||||||
|
|
||||||
|
while (pos < data.length) {
|
||||||
|
int b = data[pos] & 0xFF;
|
||||||
|
if (b == OP_FUNCTIONS) {
|
||||||
|
pos++;
|
||||||
|
} else if (b == OP_REQUEST) {
|
||||||
|
isRequest = true;
|
||||||
|
pos++;
|
||||||
|
break;
|
||||||
|
} else if (b == OP_IS) {
|
||||||
|
isRequest = false;
|
||||||
|
pos++;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The remaining bytes are the proposed/agreed functions
|
||||||
|
boolean[] hostFuncs = new boolean[8];
|
||||||
while (pos < data.length) {
|
while (pos < data.length) {
|
||||||
int func = data[pos] & 0xFF;
|
int func = data[pos] & 0xFF;
|
||||||
if (func <= FUNC_SNA_SENSE) {
|
if (func <= FUNC_SNA_SENSE) {
|
||||||
eFuncs[func] = true;
|
hostFuncs[func] = true;
|
||||||
if (funcNames.length() > 0) funcNames.append(" ");
|
|
||||||
funcNames.append(TN3270EConstants.functionName(func));
|
|
||||||
}
|
}
|
||||||
pos++;
|
pos++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRequest) {
|
||||||
|
// Host sent FUNCTIONS REQUEST -> We reply with FUNCTIONS IS (intersection of functions)
|
||||||
|
for (int i = 0; i < eFuncs.length; i++) {
|
||||||
|
eFuncs[i] = eFuncs[i] && hostFuncs[i];
|
||||||
|
}
|
||||||
|
sendTN3270EFunctionsIs();
|
||||||
|
} else {
|
||||||
|
// Host sent FUNCTIONS IS -> Accept host's agreed function list
|
||||||
|
for (int i = 0; i < eFuncs.length; i++) {
|
||||||
|
eFuncs[i] = hostFuncs[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tn3270eNegotiated = true;
|
tn3270eNegotiated = true;
|
||||||
log.info("TN3270E functions IS: " + funcNames);
|
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
|
||||||
log.info("TN3270E negotiation complete");
|
log.info("TN3270E negotiation complete");
|
||||||
|
|
||||||
// Move to CONNECTED_UNBOUND or CONNECTED_SSCP
|
// RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered
|
||||||
|
// to be bound immediately upon completion of the FUNCTIONS negotiation.
|
||||||
|
if (eFuncs[FUNC_BIND_IMAGE]) {
|
||||||
changeState(ConnectionState.CONNECTED_UNBOUND);
|
changeState(ConnectionState.CONNECTED_UNBOUND);
|
||||||
|
} else {
|
||||||
|
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||||
|
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||||
|
}
|
||||||
|
|
||||||
// Notify listeners
|
// Notify listeners
|
||||||
for (ConnectionListener l : connectionListeners) {
|
for (ConnectionListener l : connectionListeners) {
|
||||||
@@ -597,12 +698,29 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getNegotiatedFunctionNames() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < eFuncs.length; i++) {
|
||||||
|
if (eFuncs[i]) {
|
||||||
|
if (sb.length() > 0) sb.append(" ");
|
||||||
|
sb.append(TN3270EConstants.functionName(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.length() > 0 ? sb.toString() : "<none>";
|
||||||
|
}
|
||||||
|
|
||||||
// ========== End of Record processing ==========
|
// ========== End of Record processing ==========
|
||||||
|
|
||||||
private void processEndOfRecord() {
|
private void processEndOfRecord() {
|
||||||
byte[] data = ibuf.toByteArray();
|
byte[] data = ibuf.toByteArray();
|
||||||
|
ibuf.reset();
|
||||||
if (data.length == 0) return;
|
if (data.length == 0) return;
|
||||||
|
|
||||||
|
if (connectionState == ConnectionState.TELNET_PENDING && !tn3270eNegotiated) {
|
||||||
|
log.info("Received EOR during TELNET_PENDING - transitioning to plain TN3270 mode");
|
||||||
|
changeState(ConnectionState.CONNECTED_3270);
|
||||||
|
}
|
||||||
|
|
||||||
if (tn3270eNegotiated) {
|
if (tn3270eNegotiated) {
|
||||||
// TN3270E mode: data starts with 5-byte header
|
// TN3270E mode: data starts with 5-byte header
|
||||||
processTN3270ERecord(data);
|
processTN3270ERecord(data);
|
||||||
@@ -701,11 +819,11 @@ public class TelnetFSM {
|
|||||||
bindRa = screenBuffer.getMaxRows();
|
bindRa = screenBuffer.getMaxRows();
|
||||||
bindCa = screenBuffer.getMaxCols();
|
bindCa = screenBuffer.getMaxCols();
|
||||||
break;
|
break;
|
||||||
case 0x7e:
|
case 0x7E:
|
||||||
// Both default and alternate = specified values
|
// Both default and alternate = specified values
|
||||||
bindRa = bindRd; bindCa = bindCd;
|
bindRa = bindRd; bindCa = bindCd;
|
||||||
break;
|
break;
|
||||||
case 0x7f:
|
case 0x7F:
|
||||||
// Default and alternate are both specified separately
|
// Default and alternate are both specified separately
|
||||||
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
|
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
|
||||||
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
|
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
|
||||||
@@ -767,7 +885,20 @@ public class TelnetFSM {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
// Check if the host sent a raw 3270 data stream (e.g. 0xF5 EraseWrite, 0x7E EW Alternate, 0xF1 Write, etc.)
|
||||||
|
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
|
||||||
|
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
|
||||||
|
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 || dataType == 0x05 ||
|
||||||
|
dataType == 0x0D || dataType == 0x01 || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
|
||||||
|
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
|
||||||
|
") in TN3270E mode — automatically switching to plain TN3270 mode");
|
||||||
|
tn3270eNegotiated = false;
|
||||||
|
changeState(ConnectionState.CONNECTED_3270);
|
||||||
|
dsProcessor.processRecord(data, 0, data.length, true);
|
||||||
|
notifyScreenUpdate();
|
||||||
|
} else {
|
||||||
log.info("Unhandled TN3270E data type: " + dataType);
|
log.info("Unhandled TN3270E data type: " + dataType);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -790,7 +921,7 @@ public class TelnetFSM {
|
|||||||
if (connectionState != ConnectionState.TELNET_PENDING) return;
|
if (connectionState != ConnectionState.TELNET_PENDING) return;
|
||||||
|
|
||||||
// For TN3270E, we wait for TN3270E negotiation to complete
|
// For TN3270E, we wait for TN3270E negotiation to complete
|
||||||
if (myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
|
if (config.isTn3270eEnabled() && myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
|
||||||
return; // TN3270E in progress
|
return; // TN3270E in progress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,27 @@ public class QueryReplyBuilderTest {
|
|||||||
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBuildAllQueryRepliesDefaultNone() {
|
public void testBuildAllQueryRepliesDefaultBoth() {
|
||||||
assertEquals(GraphicsMode.NONE, qrBuilder.getGraphicsMode());
|
assertEquals(GraphicsMode.BOTH, qrBuilder.getGraphicsMode());
|
||||||
|
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||||
|
assertNotNull(replies);
|
||||||
|
assertTrue(replies.length > 0);
|
||||||
|
assertEquals((byte) AID_SF, replies[0]);
|
||||||
|
|
||||||
|
// In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must be present
|
||||||
|
boolean hasB0 = false;
|
||||||
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||||
|
hasB0 = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(hasB0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildAllQueryRepliesExplicitNone() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
|
||||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||||
assertNotNull(replies);
|
assertNotNull(replies);
|
||||||
assertTrue(replies.length > 0);
|
assertTrue(replies.length > 0);
|
||||||
@@ -54,6 +73,52 @@ public class QueryReplyBuilderTest {
|
|||||||
assertTrue(hasB4);
|
assertTrue(hasB4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildAllQueryRepliesWithBoth() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||||
|
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||||
|
assertNotNull(replies);
|
||||||
|
|
||||||
|
// Vector Graphics (0xB0) must be present and Charsets must have LoadPS (0x0A)
|
||||||
|
boolean hasB0 = false;
|
||||||
|
boolean hasCharsetsWithLoadPs = false;
|
||||||
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||||
|
hasB0 = true;
|
||||||
|
}
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
||||||
|
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
|
||||||
|
hasCharsetsWithLoadPs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(hasB0);
|
||||||
|
assertTrue(hasCharsetsWithLoadPs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildAllQueryRepliesWithProgrammedSymbols() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
|
||||||
|
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||||
|
assertNotNull(replies);
|
||||||
|
|
||||||
|
// Charsets with LoadPS (0x0A) must be present, and QR_GRAPHICS must NOT be present
|
||||||
|
boolean hasB0 = false;
|
||||||
|
boolean hasCharsetsWithLoadPs = false;
|
||||||
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||||
|
hasB0 = true;
|
||||||
|
}
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
||||||
|
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
|
||||||
|
hasCharsetsWithLoadPs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertFalse(hasB0);
|
||||||
|
assertTrue(hasCharsetsWithLoadPs);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBuildFilteredQueryRepliesWithSupportedCodes() {
|
public void testBuildFilteredQueryRepliesWithSupportedCodes() {
|
||||||
byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA };
|
byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA };
|
||||||
@@ -110,4 +175,61 @@ public class QueryReplyBuilderTest {
|
|||||||
assertEquals(80, altCols);
|
assertEquals(80, altCols);
|
||||||
assertEquals(43, altRows);
|
assertEquals(43, altRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProgrammedSymbolsDescriptorsSingleAndTriplePlane() {
|
||||||
|
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
|
||||||
|
byte[] requested = new byte[] { (byte) QR_CHARSETS };
|
||||||
|
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||||
|
|
||||||
|
assertNotNull(replies);
|
||||||
|
int offset = -1;
|
||||||
|
for (int i = 0; i < replies.length - 3; i++) {
|
||||||
|
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
||||||
|
offset = i + 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(offset >= 0, "QR_CHARSETS must be present");
|
||||||
|
|
||||||
|
// QR_CHARSETS payload structure:
|
||||||
|
// Flags (2 bytes), SDW (1 byte), SDH (1 byte), Form (1 byte), DevType (2 bytes), Res (1 byte), DL (1 byte)
|
||||||
|
// DL is at offset + 8, and is 7 bytes per descriptor.
|
||||||
|
int dl = replies[offset + 8] & 0xFF;
|
||||||
|
assertEquals(7, dl);
|
||||||
|
|
||||||
|
int descOffset = offset + 9;
|
||||||
|
|
||||||
|
// Descriptor 1: SET 0 (Base) -> flags 0x10
|
||||||
|
assertEquals(0x00, replies[descOffset] & 0xFF);
|
||||||
|
assertEquals(0x10, replies[descOffset + 1] & 0xFF);
|
||||||
|
|
||||||
|
// Descriptor 2: SET 1 (APL) -> flags 0x00
|
||||||
|
assertEquals(0x01, replies[descOffset + 7] & 0xFF);
|
||||||
|
assertEquals(0x00, replies[descOffset + 7 + 1] & 0xFF);
|
||||||
|
|
||||||
|
// Descriptor 3: PSA (Single plane) -> flags 0x80 (Loadable, single-plane)
|
||||||
|
assertEquals(0x02, replies[descOffset + 14] & 0xFF);
|
||||||
|
assertEquals(0x80, replies[descOffset + 14 + 1] & 0xFF);
|
||||||
|
|
||||||
|
// Descriptor 4: PSB (Single plane) -> flags 0x80 (Loadable, single-plane)
|
||||||
|
assertEquals(0x03, replies[descOffset + 21] & 0xFF);
|
||||||
|
assertEquals(0x80, replies[descOffset + 21 + 1] & 0xFF);
|
||||||
|
|
||||||
|
// Descriptor 5: PSC (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||||
|
assertEquals(0x04, replies[descOffset + 28] & 0xFF);
|
||||||
|
assertEquals(0xC0, replies[descOffset + 28 + 1] & 0xFF);
|
||||||
|
|
||||||
|
// Descriptor 6: PSD (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||||
|
assertEquals(0x05, replies[descOffset + 35] & 0xFF);
|
||||||
|
assertEquals(0xC0, replies[descOffset + 35 + 1] & 0xFF);
|
||||||
|
|
||||||
|
// Descriptor 7: PSE (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||||
|
assertEquals(0x06, replies[descOffset + 42] & 0xFF);
|
||||||
|
assertEquals(0xC0, replies[descOffset + 42 + 1] & 0xFF);
|
||||||
|
|
||||||
|
// Descriptor 8: PSF (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||||
|
assertEquals(0x07, replies[descOffset + 49] & 0xFF);
|
||||||
|
assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ public class GraphicsModeTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testConnectionConfigDefault() {
|
public void testConnectionConfigDefault() {
|
||||||
ConnectionConfig config = new ConnectionConfig();
|
ConnectionConfig config = new ConnectionConfig();
|
||||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
|
||||||
|
|
||||||
config.setGraphicsMode(GraphicsMode.BOTH);
|
|
||||||
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
|
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
|
||||||
|
|
||||||
|
config.setGraphicsMode(GraphicsMode.NONE);
|
||||||
|
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||||
|
|
||||||
config.setGraphicsMode(null);
|
config.setGraphicsMode(null);
|
||||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package org.lib3270j.graphics;
|
package org.lib3270j.graphics;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
public class ProgramSymbolManagerTest {
|
public class ProgramSymbolManagerTest {
|
||||||
@@ -54,7 +51,7 @@ public class ProgramSymbolManagerTest {
|
|||||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||||
assertNotNull(slot);
|
assertNotNull(slot);
|
||||||
assertEquals(9, slot.getWidth());
|
assertEquals(9, slot.getWidth());
|
||||||
assertEquals(16, slot.getHeight());
|
assertEquals(12, slot.getHeight());
|
||||||
assertFalse(slot.isTriplePlane());
|
assertFalse(slot.isTriplePlane());
|
||||||
|
|
||||||
// Verify pixel data (Row 0 Col 0 is 1)
|
// Verify pixel data (Row 0 Col 0 is 1)
|
||||||
@@ -62,12 +59,36 @@ public class ProgramSymbolManagerTest {
|
|||||||
assertNotNull(pixels);
|
assertNotNull(pixels);
|
||||||
assertEquals(1, pixels[0]); // Row 0 Col 0 is 1
|
assertEquals(1, pixels[0]); // Row 0 Col 0 is 1
|
||||||
|
|
||||||
// Test rendering into BufferedImage
|
// Test rendering into 32-bit ARGB pixels
|
||||||
BufferedImage canvas = new BufferedImage(80, 24, BufferedImage.TYPE_INT_ARGB);
|
int fg = 0xFF00FF00; // Green
|
||||||
Graphics2D g2 = canvas.createGraphics();
|
int bg = 0xFF000000; // Black
|
||||||
boolean drawn = manager.drawSymbol(g2, 0x40, 0x41, 0, 0, 10, 20, Color.GREEN, Color.BLACK);
|
int[] rgb = slot.getRgbPixels(fg, bg);
|
||||||
g2.dispose();
|
assertNotNull(rgb);
|
||||||
assertTrue(drawn);
|
assertEquals(9 * 12, rgb.length);
|
||||||
|
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testExplicitCellDimensions() {
|
||||||
|
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||||
|
manager.setDefaultCellDimensions(9, 16);
|
||||||
|
|
||||||
|
byte[] payload = new byte[4 + 18];
|
||||||
|
payload[0] = 0x01; // Format 1
|
||||||
|
payload[1] = 0x40; // LCID 0x40
|
||||||
|
payload[2] = 0x41; // Code point 0x41
|
||||||
|
payload[3] = 0x02; // RWS 2
|
||||||
|
payload[4] = (byte) 0x80;
|
||||||
|
payload[5] = (byte) 0x00;
|
||||||
|
for (int r = 0; r < 16; r++) {
|
||||||
|
payload[6 + r] = (byte) 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.loadps(payload);
|
||||||
|
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||||
|
assertNotNull(slot);
|
||||||
|
assertEquals(9, slot.getWidth());
|
||||||
|
assertEquals(16, slot.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -104,16 +125,18 @@ public class ProgramSymbolManagerTest {
|
|||||||
byte[] pixels = slot.getPixelData();
|
byte[] pixels = slot.getPixelData();
|
||||||
assertEquals(7, pixels[0] & 0xFF);
|
assertEquals(7, pixels[0] & 0xFF);
|
||||||
|
|
||||||
// Render image
|
// Render ARGB pixels
|
||||||
BufferedImage img = slot.getImage(Color.WHITE, Color.BLACK);
|
int[] rgb = slot.getRgbPixels(0xFFFFFFFF, 0xFF000000);
|
||||||
assertNotNull(img);
|
assertNotNull(rgb);
|
||||||
|
|
||||||
// Pixel (0,0) should be white composite (Red=255, Green=255, Blue=255)
|
// Pixel (0,0) should be white composite (0xFFFFFFFF)
|
||||||
int rgb = img.getRGB(0, 0);
|
int pixelArgb = rgb[0];
|
||||||
Color pixelColor = new Color(rgb, true);
|
int r = (pixelArgb >> 16) & 0xFF;
|
||||||
assertEquals(255, pixelColor.getRed());
|
int g = (pixelArgb >> 8) & 0xFF;
|
||||||
assertEquals(255, pixelColor.getGreen());
|
int b = pixelArgb & 0xFF;
|
||||||
assertEquals(255, pixelColor.getBlue());
|
assertEquals(255, r);
|
||||||
|
assertEquals(255, g);
|
||||||
|
assertEquals(255, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -44,10 +44,91 @@ public class InputProcessorTest {
|
|||||||
|
|
||||||
assertEquals('A', screen.getCell(1).ucs4);
|
assertEquals('A', screen.getCell(1).ucs4);
|
||||||
assertEquals('B', screen.getCell(2).ucs4);
|
assertEquals('B', screen.getCell(2).ucs4);
|
||||||
|
assertEquals('A', screen.getDisplayCell(1).ucs4);
|
||||||
|
assertEquals('B', screen.getDisplayCell(2).ucs4);
|
||||||
|
|
||||||
input.eraseInput();
|
input.eraseInput();
|
||||||
|
|
||||||
assertEquals(0, screen.getCell(1).ec);
|
assertEquals(0, screen.getCell(1).ec);
|
||||||
assertEquals(0, screen.getCell(2).ec);
|
assertEquals(0, screen.getCell(2).ec);
|
||||||
|
assertEquals(0, screen.getDisplayCell(1).ec);
|
||||||
|
assertEquals(0, screen.getDisplayCell(2).ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTypeCharacterUpdatesDisplaySnapshot() {
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCellFA(0, (byte) FA_PRINTABLE); // Unprotected field starting at 1
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
screen.updateDisplaySnapshot(); // Initial snapshot
|
||||||
|
|
||||||
|
// Type "logon"
|
||||||
|
input.typeCharacter('l');
|
||||||
|
input.typeCharacter('o');
|
||||||
|
input.typeCharacter('g');
|
||||||
|
input.typeCharacter('o');
|
||||||
|
input.typeCharacter('n');
|
||||||
|
|
||||||
|
// Verify underlying buffer
|
||||||
|
assertEquals('l', screen.getCell(1).ucs4);
|
||||||
|
assertEquals('o', screen.getCell(2).ucs4);
|
||||||
|
assertEquals('g', screen.getCell(3).ucs4);
|
||||||
|
assertEquals('o', screen.getCell(4).ucs4);
|
||||||
|
assertEquals('n', screen.getCell(5).ucs4);
|
||||||
|
|
||||||
|
// Verify display snapshot (what TerminalPanel renders)
|
||||||
|
assertEquals('l', screen.getDisplayCell(1).ucs4);
|
||||||
|
assertEquals('o', screen.getDisplayCell(2).ucs4);
|
||||||
|
assertEquals('g', screen.getDisplayCell(3).ucs4);
|
||||||
|
assertEquals('o', screen.getDisplayCell(4).ucs4);
|
||||||
|
assertEquals('n', screen.getDisplayCell(5).ucs4);
|
||||||
|
assertEquals(6, screen.getDisplayCursorAddress());
|
||||||
|
assertEquals(6, screen.getCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCursorMovementUpdatesDisplayCursorAddress() {
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCursorAddress(10);
|
||||||
|
assertEquals(10, screen.getDisplayCursorAddress());
|
||||||
|
|
||||||
|
input.cursorRight();
|
||||||
|
assertEquals(11, screen.getDisplayCursorAddress());
|
||||||
|
|
||||||
|
input.cursorLeft();
|
||||||
|
assertEquals(10, screen.getDisplayCursorAddress());
|
||||||
|
|
||||||
|
screen.setCursorAddress(45);
|
||||||
|
assertEquals(45, screen.getDisplayCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeleteAndEraseEofUpdateDisplaySnapshot() {
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCellFA(0, (byte) FA_PRINTABLE);
|
||||||
|
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
input.typeCharacter('A');
|
||||||
|
input.typeCharacter('B');
|
||||||
|
input.typeCharacter('C');
|
||||||
|
assertEquals('A', screen.getDisplayCell(1).ucs4);
|
||||||
|
assertEquals('B', screen.getDisplayCell(2).ucs4);
|
||||||
|
assertEquals('C', screen.getDisplayCell(3).ucs4);
|
||||||
|
|
||||||
|
// Backspace over 'C'
|
||||||
|
input.backspace();
|
||||||
|
assertEquals(3, screen.getCursorAddress());
|
||||||
|
assertEquals(3, screen.getDisplayCursorAddress());
|
||||||
|
assertEquals(0, screen.getDisplayCell(3).ucs4);
|
||||||
|
|
||||||
|
// Erase EOF from position 2 ('B')
|
||||||
|
screen.setCursorAddress(2);
|
||||||
|
input.eraseEof();
|
||||||
|
assertEquals(0, screen.getDisplayCell(2).ucs4);
|
||||||
|
assertEquals('A', screen.getDisplayCell(1).ucs4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package org.lib3270j.telnet;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.lib3270j.ConnectionConfig;
|
||||||
|
import org.lib3270j.ConnectionState;
|
||||||
|
import org.lib3270j.TerminalModel;
|
||||||
|
import org.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import org.lib3270j.datastream.DataStreamProcessor;
|
||||||
|
import org.lib3270j.protocol.TelnetConstants;
|
||||||
|
import org.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class TelnetFSMTest {
|
||||||
|
|
||||||
|
private ConnectionConfig config;
|
||||||
|
private ScreenBuffer screenBuffer;
|
||||||
|
private DataStreamProcessor dsProcessor;
|
||||||
|
private TelnetFSM fsm;
|
||||||
|
private MockConnection connection;
|
||||||
|
|
||||||
|
private static class MockConnection extends TelnetConnection {
|
||||||
|
final List<byte[]> sentData = new ArrayList<>();
|
||||||
|
|
||||||
|
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||||
|
super(config, fsm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void sendRaw(byte[] data) {
|
||||||
|
sentData.add(data.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void sendRaw(byte[] data, int offset, int length) {
|
||||||
|
byte[] b = new byte[length];
|
||||||
|
System.arraycopy(data, offset, b, 0, length);
|
||||||
|
sentData.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||||
|
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
|
||||||
|
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
|
||||||
|
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||||
|
connection = new MockConnection(config, fsm);
|
||||||
|
fsm.setConnection(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void feedBytes(int... bytes) {
|
||||||
|
for (int b : bytes) {
|
||||||
|
fsm.feedByte(b & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHostPrefixParsingPlainTn3270() {
|
||||||
|
ConnectionConfig c1 = ConnectionConfig.parseHostString("P:mainframe.example.com", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertFalse(c1.isTn3270eEnabled(), "P: prefix should disable TN3270E");
|
||||||
|
assertFalse(c1.isUseTls());
|
||||||
|
assertEquals("mainframe.example.com", c1.getHost());
|
||||||
|
|
||||||
|
ConnectionConfig c2 = ConnectionConfig.parseHostString("plain:zos.net:2323", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertFalse(c2.isTn3270eEnabled());
|
||||||
|
assertEquals("zos.net", c2.getHost());
|
||||||
|
assertEquals(2323, c2.getPort());
|
||||||
|
|
||||||
|
ConnectionConfig c3 = ConnectionConfig.parseHostString("L:P:secure.mainframe.com:992", 0, TerminalModel.IBM_3279_4);
|
||||||
|
assertTrue(c3.isUseTls(), "L: should enable TLS");
|
||||||
|
assertFalse(c3.isTn3270eEnabled(), "P: should disable TN3270E");
|
||||||
|
assertEquals("secure.mainframe.com", c3.getHost());
|
||||||
|
assertEquals(992, c3.getPort());
|
||||||
|
|
||||||
|
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertFalse(c4.isTn3270eEnabled());
|
||||||
|
assertEquals("vm.ibm.com", c4.getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlainTn3270NegotiationRejectsTn3270E() {
|
||||||
|
config.setTn3270eEnabled(false);
|
||||||
|
fsm.onConnected();
|
||||||
|
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
|
||||||
|
|
||||||
|
// Host offers DO TN3270E -> client must reply WONT TN3270E
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
assertFalse(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||||
|
|
||||||
|
// Host offers WILL TN3270E -> client must reply DONT TN3270E
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
assertFalse(fsm.getHisOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||||
|
|
||||||
|
// Host negotiates standard 3270 options: DO BINARY, WILL BINARY, DO EOR, WILL EOR, DO TTYPE
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_BINARY);
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_BINARY);
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_EOR);
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_EOR);
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
|
||||||
|
|
||||||
|
// Client should have transitioned to CONNECTED_3270
|
||||||
|
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
|
||||||
|
|
||||||
|
// Host asks for TTYPE
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
|
||||||
|
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
|
||||||
|
|
||||||
|
// Verify sent packets contain WONT TN3270E, DONT TN3270E, and TTYPE IS
|
||||||
|
boolean foundWontTn3270e = false;
|
||||||
|
boolean foundTtypeIs = false;
|
||||||
|
for (byte[] pkt : connection.sentData) {
|
||||||
|
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||||
|
(pkt[1] & 0xFF) == TelnetConstants.WONT && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E) {
|
||||||
|
foundWontTn3270e = true;
|
||||||
|
}
|
||||||
|
if (pkt.length >= 4 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||||
|
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TTYPE) {
|
||||||
|
foundTtypeIs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(foundWontTn3270e, "Expected WONT TN3270E response");
|
||||||
|
assertTrue(foundTtypeIs, "Expected TTYPE IS response");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTn3270eFunctionsNegotiationWithoutBindImageTransitionsToConnected() {
|
||||||
|
config.setTn3270eEnabled(true);
|
||||||
|
fsm.onConnected();
|
||||||
|
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
|
||||||
|
|
||||||
|
// Host offers DO TN3270E -> client accepts with WILL TN3270E
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
assertTrue(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||||
|
|
||||||
|
// Host responds with DEVICE-TYPE IS IBM-3279-4-E CONNECT LU01
|
||||||
|
byte[] devTypeIs = new byte[]{
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||||
|
0x02, 0x04, // OP_DEVICE_TYPE, OP_IS
|
||||||
|
'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||||
|
0x01, // OP_CONNECT
|
||||||
|
'L', 'U', '0', '1',
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||||
|
};
|
||||||
|
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
|
||||||
|
|
||||||
|
// Host responds with FUNCTIONS REQUEST <empty> (e.g. pytn3270 / RFC 2355 server)
|
||||||
|
byte[] funcsReq = new byte[]{
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||||
|
0x03, 0x07, // OP_FUNCTIONS, OP_REQUEST
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||||
|
};
|
||||||
|
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
|
||||||
|
|
||||||
|
// Per RFC 2355: Client must reply with FUNCTIONS IS, and without BIND-IMAGE, session is bound immediately
|
||||||
|
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState(),
|
||||||
|
"Expected CONNECTED_TN3270E when BIND-IMAGE is not negotiated");
|
||||||
|
|
||||||
|
// Verify that client sent SB TN3270E FUNCTIONS IS SE
|
||||||
|
boolean foundFunctionsIs = false;
|
||||||
|
for (byte[] pkt : connection.sentData) {
|
||||||
|
if (pkt.length >= 5 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||||
|
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E &&
|
||||||
|
(pkt[3] & 0xFF) == 0x03 && (pkt[4] & 0xFF) == 0x04) {
|
||||||
|
foundFunctionsIs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(foundFunctionsIs, "Expected FUNCTIONS IS reply from client upon receiving FUNCTIONS REQUEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlainDataStreamFallbackInTn3270eMode() {
|
||||||
|
config.setTn3270eEnabled(true);
|
||||||
|
fsm.onConnected();
|
||||||
|
|
||||||
|
// Negotiate TN3270E with empty functions
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
byte[] devTypeIs = new byte[]{
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||||
|
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||||
|
};
|
||||||
|
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
|
||||||
|
|
||||||
|
byte[] funcsReq = new byte[]{
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||||
|
0x03, 0x07, (byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||||
|
};
|
||||||
|
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
|
||||||
|
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
|
||||||
|
|
||||||
|
// Now host sends raw 3270 data stream without 5-byte TN3270E header (e.g. EraseWrite 0xF5)
|
||||||
|
// 0xF5 = EraseWrite, 0xC3 = WCC, 0x11 = SBA, 0x40 0x40 = Pos 0, "HELLO"
|
||||||
|
EbcdicTranslator trans = new EbcdicTranslator();
|
||||||
|
String msg = "HELLO";
|
||||||
|
byte[] rawStream = new byte[3 + 2 + msg.length() + 2];
|
||||||
|
rawStream[0] = (byte) 0xF5; // EraseWrite
|
||||||
|
rawStream[1] = (byte) 0xC3; // WCC
|
||||||
|
rawStream[2] = 0x11; // SBA
|
||||||
|
rawStream[3] = 0x40; rawStream[4] = 0x40; // addr 0
|
||||||
|
for (int i = 0; i < msg.length(); i++) {
|
||||||
|
rawStream[5 + i] = (byte) trans.unicodeToEbcdic(msg.charAt(i));
|
||||||
|
}
|
||||||
|
rawStream[rawStream.length - 2] = (byte) TelnetConstants.IAC;
|
||||||
|
rawStream[rawStream.length - 1] = (byte) TelnetConstants.EOR;
|
||||||
|
|
||||||
|
for (byte b : rawStream) fsm.feedByte(b & 0xFF);
|
||||||
|
|
||||||
|
// State must have automatically switched to CONNECTED_3270 and screen updated
|
||||||
|
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
|
||||||
|
assertEquals('H', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
|
||||||
|
assertEquals('E', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
|
||||||
|
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
|
||||||
|
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
|
||||||
|
assertEquals('O', trans.ebcdicToUnicode(screenBuffer.getCellEC(4)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user