Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
bcfd4ba2e0
|
|||
|
2584f4289f
|
|||
|
3526e682a1
|
|||
|
7eebd2107c
|
|||
|
36b93df986
|
|||
|
dbe0cdaa89
|
|||
|
b5a4978b24
|
|||
| d2bdb9bb0a | |||
| e28b7dc30a | |||
| d9df1052a1 | |||
| 0837de8db3 | |||
| 42eef519a6 | |||
|
b57d8f960d
|
|||
|
09600260b5
|
|||
|
00c7b0770c
|
|||
|
1212b2cceb
|
|||
|
bf5e4410e6
|
|||
|
7e658a6c20
|
|||
|
c32521fd4b
|
|||
|
d7d2f8bad5
|
|||
|
f0fc1b6e70
|
|||
| 10a31476c2 |
@@ -1,3 +1,9 @@
|
||||
j3270.log.*
|
||||
*.txt
|
||||
*.py
|
||||
*.pcap
|
||||
build/*
|
||||
*.log
|
||||
.DS_Store
|
||||
*.jar
|
||||
build
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# j3270
|
||||
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||
[](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.
|
||||
|
||||
@@ -24,6 +24,19 @@ Requires **Java 11 or higher** (Java 11, 17, 21+):
|
||||
# Launch interactive GUI
|
||||
java -jar j3270.jar
|
||||
|
||||
# Connect to TLS/SSL mainframe with custom port
|
||||
java -jar j3270.jar -s mainframe.example.com 992
|
||||
# Or using standard x3270 L: prefix
|
||||
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
|
||||
java -jar j3270.jar --tls --insecure mainframe.example.com 992
|
||||
|
||||
# Launch with custom configuration file
|
||||
java -jar j3270.jar -c config.ini
|
||||
|
||||
@@ -35,7 +48,11 @@ java -jar j3270.jar -c config.ini
|
||||
|
||||
## ✨ 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**:
|
||||
- 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.
|
||||
- Optional unverified/insecure mode for testing and headless scripting.
|
||||
- **IND$FILE File Transfer**: Full support for both **CUT** and **DFT (DDM)** high-speed structured field transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS.
|
||||
- **z/VM & Line-Mode Support**: Proper SSCP-LU and unformatted line handling (`CP TERM CONMODE 3270` supported).
|
||||
- **APL & Graphic Escape**: Comprehensive box-drawing and math symbol character rendering.
|
||||
@@ -59,7 +76,7 @@ The project includes self-contained, portable build scripts:
|
||||
# Build executable JAR in 2 seconds:
|
||||
sh ./build_all.sh
|
||||
|
||||
# Run all 25 automated unit tests in ~130ms:
|
||||
# Run all 57 automated unit tests in ~500ms:
|
||||
sh ./test_all.sh
|
||||
```
|
||||
|
||||
@@ -73,38 +90,13 @@ The resulting standalone JAR is created at `build/j3270.jar`.
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ How to Tag & Publish Releases
|
||||
|
||||
Releases can be published automatically to Gitea via Git tags:
|
||||
|
||||
```bash
|
||||
# 1. Create a version tag
|
||||
git tag -a v0.1.0 -m "Release v0.1.0"
|
||||
|
||||
# 2. Push the tag to Gitea
|
||||
git push origin v0.1.0
|
||||
```
|
||||
|
||||
Pushing any `v*` tag triggers the automated release workflow (`.gitea/workflows/release.yaml`), which runs tests, builds the JAR, and publishes a new Gitea Release with `j3270.jar` attached automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Topics & Discoverability
|
||||
|
||||
Suggested keywords/topics for repository indexing:
|
||||
`tn3270` · `tn3270e` · `ibm-3270` · `mainframe` · `terminal-emulator` · `x3270` · `ind-file` · `file-transfer` · `ebcdic` · `z-vm` · `z-os` · `cms` · `tso` · `cics` · `java` · `desktop-app`
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
|
||||
|
||||
---
|
||||
|
||||
## 📜 Acknowledgements
|
||||
|
||||
- [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
|
||||
- Gemini 3.1 Pro
|
||||
- Gemini 3.7 Flash
|
||||
- Gemma4 12B and 26B
|
||||
- GPT-OSS 120B
|
||||
- Qwen3 4B and 32B
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Fixed Bugs
|
||||
- Complete failure when using `cp term conmode 3270` under VM:
|
||||
Fixed in InputProcessor by sending raw line-mode EBCDIC character data in SSCP-LU mode instead of 3270 AID headers and 1920-byte buffer dumps, and correctly processing subsequent 3270 stream transitions upon CONMODE 3270.
|
||||
|
||||
- Unable to start a 2nd transfer after first completed ("A transfer is already in progress"):
|
||||
Fixed in FTDft by signaling completion upon handling `TR_CLOSE_REQ` / host completion messages, and adding state reset in FileTransfer.
|
||||
|
||||
- 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.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
allprojects {
|
||||
group = 'org.j3270'
|
||||
group = 'haus.nightmare.j3270'
|
||||
version = '0.1.0'
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -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.
+58
-1
@@ -77,7 +77,7 @@ fi
|
||||
|
||||
# 4. Package executable JAR
|
||||
echo "Packaging j3270.jar..."
|
||||
printf "Manifest-Version: 1.0\nMain-Class: org.pubvm.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF"
|
||||
printf "Manifest-Version: 1.0\nMain-Class: haus.nightmare.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF"
|
||||
|
||||
"$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
|
||||
-C "$BUILD_DIR/lib3270j" . \
|
||||
@@ -87,3 +87,60 @@ echo "=== Build Complete ==="
|
||||
echo "Executable JAR created: $BUILD_DIR/j3270.jar"
|
||||
ls -lh "$BUILD_DIR/j3270.jar"
|
||||
|
||||
# 5. Compile and run tests if requested
|
||||
if [ "$1" = "test" ] || [ "$1" = "check" ]; then
|
||||
echo "=== Running Tests ==="
|
||||
TEST_BUILD_DIR="$BUILD_DIR/test-classes"
|
||||
mkdir -p "$TEST_BUILD_DIR"
|
||||
JUNIT_CP=$(find "$HOME/.gradle/caches/modules-2/files-2.1/" -name "*.jar" 2>/dev/null | grep -E "junit|opentest|apiguardian" | tr "\n" ":" || true)
|
||||
FULL_CP="$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$JUNIT_CP"
|
||||
|
||||
find "$SCRIPT_DIR/lib3270j/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
|
||||
if [ -d "$SCRIPT_DIR/j3270/src/test/java" ]; then
|
||||
find "$SCRIPT_DIR/j3270/src/test/java" -name "*.java" >> "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -s "$BUILD_DIR/test_sources.txt" ] && [ -n "$JUNIT_CP" ]; then
|
||||
echo "Compiling tests..."
|
||||
"$JAVAC_BIN" -cp "$FULL_CP" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt"
|
||||
rm -f "$BUILD_DIR/test_sources.txt"
|
||||
|
||||
echo "Executing tests with JUnit Platform..."
|
||||
cat << 'EOF' > "$BUILD_DIR/TestRunner.java"
|
||||
import org.junit.platform.launcher.*;
|
||||
import org.junit.platform.launcher.core.*;
|
||||
import org.junit.platform.launcher.listeners.*;
|
||||
import org.junit.platform.engine.discovery.*;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.*;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
public class TestRunner {
|
||||
public static void main(String[] args) {
|
||||
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
|
||||
.selectors(
|
||||
selectPackage("haus.nightmare.lib3270j")
|
||||
)
|
||||
.build();
|
||||
|
||||
Launcher launcher = LauncherFactory.create();
|
||||
SummaryGeneratingListener listener = new SummaryGeneratingListener();
|
||||
launcher.registerTestExecutionListeners(listener);
|
||||
launcher.execute(request);
|
||||
|
||||
TestExecutionSummary summary = listener.getSummary();
|
||||
summary.printTo(new PrintWriter(System.out));
|
||||
summary.printFailuresTo(new PrintWriter(System.err));
|
||||
|
||||
if (summary.getTotalFailureCount() > 0) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
"$JAVAC_BIN" -cp "$FULL_CP:$TEST_BUILD_DIR" -d "$BUILD_DIR" "$BUILD_DIR/TestRunner.java"
|
||||
JAVA_BIN_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/java}"
|
||||
JAVA_BIN_EXEC="${JAVA_BIN_EXEC:-java}"
|
||||
"$JAVA_BIN_EXEC" -cp "$FULL_CP:$TEST_BUILD_DIR:$BUILD_DIR" TestRunner
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec "$(dirname "$0")/build_all.sh"
|
||||
exec "$(dirname "$0")/build_all.sh" "$@"
|
||||
|
||||
+2
-2
@@ -7,12 +7,12 @@ dependencies {
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'org.pubvm.j3270.J3270App'
|
||||
mainClass = 'haus.nightmare.j3270.J3270App'
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Main-Class': 'org.pubvm.j3270.J3270App',
|
||||
attributes 'Main-Class': 'haus.nightmare.j3270.J3270App',
|
||||
'Implementation-Title': 'j3270',
|
||||
'Implementation-Version': archiveVersion
|
||||
}
|
||||
|
||||
+156
-53
@@ -1,13 +1,13 @@
|
||||
package org.pubvm.j3270;
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import org.lib3270j.*;
|
||||
import org.lib3270j.listener.ConnectionListener;
|
||||
import org.lib3270j.listener.ScreenUpdateListener;
|
||||
import org.pubvm.j3270.ui.ConnectDialog;
|
||||
import org.pubvm.j3270.ui.StatusBar;
|
||||
import org.pubvm.j3270.ui.TerminalPanel;
|
||||
import org.pubvm.j3270.ft.FileTransfer;
|
||||
import org.pubvm.j3270.ft.FileTransferDialog;
|
||||
import haus.nightmare.lib3270j.*;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||
import haus.nightmare.j3270.ui.StatusBar;
|
||||
import haus.nightmare.j3270.ui.TerminalPanel;
|
||||
import haus.nightmare.j3270.ft.FileTransfer;
|
||||
import haus.nightmare.j3270.ft.FileTransferDialog;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
@@ -34,6 +34,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
private int lastPort = 23;
|
||||
|
||||
private FileTransfer fileTransfer;
|
||||
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
public J3270App() {
|
||||
super("j3270 — Java TN3270 Terminal Emulator");
|
||||
@@ -119,7 +120,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
// Actions menu
|
||||
JMenu actionsMenu = createMenu("Actions");
|
||||
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_L, () -> {
|
||||
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
|
||||
if (client != null)
|
||||
client.sendClear();
|
||||
terminalPanel.repaint();
|
||||
@@ -130,6 +131,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
terminalPanel.repaint();
|
||||
}));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
|
||||
terminalPanel.toggleLightPen();
|
||||
}, true));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
@@ -148,18 +153,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
return menu;
|
||||
}
|
||||
|
||||
private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) {
|
||||
JMenuItem item = new JMenuItem(text);
|
||||
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
|
||||
JMenuItem item = new JMenuItem(name);
|
||||
item.setBackground(new Color(40, 40, 40));
|
||||
item.setForeground(new Color(200, 200, 200));
|
||||
if (acceleratorKey > 0) {
|
||||
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey,
|
||||
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
|
||||
if (mnemonic > 0) {
|
||||
int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
|
||||
}
|
||||
item.addActionListener(e -> action.run());
|
||||
return item;
|
||||
}
|
||||
|
||||
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action) {
|
||||
return createMenuItem(name, mnemonic, action, false);
|
||||
}
|
||||
|
||||
private void showFileTransferDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
||||
@@ -227,8 +236,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
private void showConnectDialog() {
|
||||
ConnectDialog dialog = new ConnectDialog(this);
|
||||
dialog.setInitialHost(lastHost);
|
||||
dialog.setInitialPort(lastPort);
|
||||
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||
int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||
dialog.setInitialHost(initialHost);
|
||||
dialog.setInitialPort(initialPort);
|
||||
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
||||
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed()) {
|
||||
@@ -240,7 +253,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
|
||||
private void showSettingsDialog() {
|
||||
org.pubvm.j3270.ui.SettingsDialog dialog = new org.pubvm.j3270.ui.SettingsDialog(this);
|
||||
haus.nightmare.j3270.ui.SettingsDialog dialog = new haus.nightmare.j3270.ui.SettingsDialog(this);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
@@ -253,20 +266,30 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
lastPort = config.getPort();
|
||||
|
||||
// Save for auto-connect
|
||||
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||
|
||||
// Disconnect existing connection
|
||||
disconnect();
|
||||
|
||||
// Wire interactive certificate verifier if none configured
|
||||
if (config.getCertificateVerifier() == null) {
|
||||
config.setCertificateVerifier((chain, authType, exception) ->
|
||||
haus.nightmare.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
|
||||
);
|
||||
}
|
||||
|
||||
client = new Telnet3270Client(config);
|
||||
client.addConnectionListener(this);
|
||||
client.addScreenUpdateListener(this);
|
||||
|
||||
terminalPanel.setClient(client);
|
||||
statusBar.setClient(client);
|
||||
statusBar.setClient(client, terminalPanel);
|
||||
|
||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
|
||||
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
||||
|
||||
// Resize window to match model
|
||||
terminalPanel.guardedPack();
|
||||
@@ -297,7 +320,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
client.disconnect();
|
||||
client = null;
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null);
|
||||
statusBar.setClient(null, terminalPanel);
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
}
|
||||
@@ -349,20 +372,23 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// During an active file transfer, let the CUT/DFT handler drive
|
||||
// keyboard state. In x3270, ft_cut_data() runs before WCC
|
||||
// keyboard-restore is applied — the keyboard stays locked for the
|
||||
// entire CUT transfer.
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.onScreenUpdated();
|
||||
}
|
||||
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
if (screenUpdatePending.compareAndSet(false, true)) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
screenUpdatePending.set(false);
|
||||
// During an active file transfer, let the CUT/DFT handler drive
|
||||
// keyboard state. In x3270, ft_cut_data() runs before WCC
|
||||
// keyboard-restore is applied — the keyboard stays locked for the
|
||||
// entire CUT transfer.
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.onScreenUpdated();
|
||||
}
|
||||
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -426,11 +452,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean debug = false;
|
||||
boolean cliTls = false;
|
||||
boolean cliNoVerifyCert = false;
|
||||
Boolean cliTn3270e = null;
|
||||
haus.nightmare.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
||||
String configFile = null;
|
||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
|
||||
debug = true;
|
||||
} else if ("--tls".equals(args[i]) || "--ssl".equals(args[i]) || "-s".equals(args[i])) {
|
||||
cliTls = true;
|
||||
} else if ("--no-verify-cert".equals(args[i]) || "--insecure".equals(args[i]) || "-k".equals(args[i])) {
|
||||
cliNoVerifyCert = true;
|
||||
} else if ("--no-tn3270e".equals(args[i]) || "--plain-tn3270".equals(args[i]) || "--plain".equals(args[i]) || "-P".equals(args[i]) || "-p".equals(args[i]) || "--non-e".equals(args[i])) {
|
||||
cliTn3270e = false;
|
||||
} else if ("--tn3270e".equals(args[i])) {
|
||||
cliTn3270e = true;
|
||||
} else if (args[i].startsWith("--graphics=")) {
|
||||
cliGraphicsMode = haus.nightmare.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("-")) {
|
||||
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
|
||||
} else if ("--no-graphics".equals(args[i])) {
|
||||
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
|
||||
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
||||
configFile = args[++i];
|
||||
} else {
|
||||
@@ -438,21 +482,50 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
}
|
||||
|
||||
// Configure logging over the absolute root logger
|
||||
// Configure logging for application packages
|
||||
Level logLevel = debug ? Level.ALL : Level.INFO;
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
}
|
||||
globalRoot.setLevel(debug ? Level.ALL : Level.WARNING);
|
||||
ConsoleHandler handler = new ConsoleHandler();
|
||||
handler.setLevel(Level.ALL);
|
||||
handler.setFormatter(new SimpleFormatter());
|
||||
globalRoot.addHandler(handler);
|
||||
globalRoot.setLevel(Level.ALL);
|
||||
|
||||
java.util.logging.Filter appFilter = record -> {
|
||||
String name = record.getLoggerName();
|
||||
return name != null && (name.startsWith("haus.nightmare.lib3270j") || name.startsWith("haus.nightmare.j3270"));
|
||||
};
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(logLevel);
|
||||
consoleHandler.setFormatter(new SimpleFormatter());
|
||||
consoleHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(consoleHandler);
|
||||
|
||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||
|
||||
try {
|
||||
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||
@Override
|
||||
public synchronized void publish(java.util.logging.LogRecord record) {
|
||||
super.publish(record);
|
||||
flush();
|
||||
}
|
||||
};
|
||||
fileHandler.setLevel(Level.ALL);
|
||||
fileHandler.setFormatter(new SimpleFormatter());
|
||||
fileHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(fileHandler);
|
||||
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
||||
} catch (Exception e) {
|
||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Load INI config file if specified
|
||||
if (configFile != null) {
|
||||
try {
|
||||
org.pubvm.j3270.config.Settings.loadFromIniFile(configFile);
|
||||
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to load config file: " + configFile);
|
||||
System.err.println(e.getMessage());
|
||||
@@ -471,18 +544,23 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
||||
System.setProperty("apple.awt.application.name", "j3270");
|
||||
|
||||
final boolean finalTls = cliTls;
|
||||
final boolean finalNoVerify = cliNoVerifyCert;
|
||||
final Boolean finalTn3270e = cliTn3270e;
|
||||
final haus.nightmare.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
J3270App app = new J3270App();
|
||||
app.setVisible(true);
|
||||
|
||||
// If host:port given on command line, connect directly
|
||||
if (!remainingArgs.isEmpty()) {
|
||||
String host = remainingArgs.get(0);
|
||||
int port = 23;
|
||||
String hostArg = remainingArgs.get(0);
|
||||
int port = finalTls ? 992 : 23;
|
||||
if (remainingArgs.size() >= 2) {
|
||||
try {
|
||||
port = Integer.parseInt(remainingArgs.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
@@ -490,23 +568,48 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
try {
|
||||
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
||||
model = TerminalModel.forModel(modelNum, true);
|
||||
} catch (Exception e) {
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, model);
|
||||
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
|
||||
if (finalTls) {
|
||||
config.setUseTls(true);
|
||||
}
|
||||
if (finalNoVerify) {
|
||||
config.setTlsVerifyCert(false);
|
||||
}
|
||||
if (finalTn3270e != null) {
|
||||
config.setTn3270eEnabled(finalTn3270e);
|
||||
}
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
app.connect(config);
|
||||
} else {
|
||||
org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings
|
||||
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
|
||||
.getStartupBehavior();
|
||||
switch (behavior) {
|
||||
case SHOW_CONNECT:
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
break;
|
||||
case AUTO_CONNECT:
|
||||
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
|
||||
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
||||
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
|
||||
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert();
|
||||
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e();
|
||||
if (host != null && !host.isEmpty()) {
|
||||
app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4));
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||
config.setTlsVerifyCert(verify);
|
||||
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
app.connect(config);
|
||||
} else {
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
}
|
||||
+76
-4
@@ -1,4 +1,4 @@
|
||||
package org.pubvm.j3270.config;
|
||||
package haus.nightmare.j3270.config;
|
||||
|
||||
import java.util.prefs.Preferences;
|
||||
import java.awt.Color;
|
||||
@@ -62,6 +62,39 @@ public class Settings {
|
||||
prefs.putInt("autoConnectPort", port);
|
||||
}
|
||||
|
||||
public static boolean getAutoConnectTls() {
|
||||
return prefs.getBoolean("autoConnectTls", false);
|
||||
}
|
||||
|
||||
public static void setAutoConnectTls(boolean tls) {
|
||||
prefs.putBoolean("autoConnectTls", tls);
|
||||
}
|
||||
|
||||
public static boolean getAutoConnectVerifyCert() {
|
||||
return prefs.getBoolean("autoConnectVerifyCert", true);
|
||||
}
|
||||
|
||||
public static void setAutoConnectVerifyCert(boolean 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 haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
|
||||
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
|
||||
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||
}
|
||||
|
||||
public static void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||
prefs.put("graphicsMode", (mode != null ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH).name());
|
||||
}
|
||||
|
||||
public static Color getColorOverride(int index, Color defaultColor) {
|
||||
String hex = prefs.get("color_" + index, null);
|
||||
try {
|
||||
@@ -194,15 +227,38 @@ public class Settings {
|
||||
break;
|
||||
|
||||
case "behavior":
|
||||
case "connection":
|
||||
switch (key) {
|
||||
case "startupBehavior":
|
||||
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "autoConnectHost": setAutoConnectHost(value); break;
|
||||
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
|
||||
case "autoConnectHost":
|
||||
case "host":
|
||||
setAutoConnectHost(value);
|
||||
break;
|
||||
case "autoConnectPort":
|
||||
case "port":
|
||||
setAutoConnectPort(Integer.parseInt(value));
|
||||
break;
|
||||
case "autoConnectTls":
|
||||
case "tls":
|
||||
case "ssl":
|
||||
case "useTls":
|
||||
setAutoConnectTls(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "autoConnectVerifyCert":
|
||||
case "verifyCert":
|
||||
case "tlsVerifyCert":
|
||||
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "autoConnectTn3270e":
|
||||
case "tn3270e":
|
||||
case "enableTn3270e":
|
||||
setAutoConnectTn3270e(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||
default:
|
||||
log.warning("Unknown behavior key: " + key);
|
||||
log.warning("Unknown behavior/connection key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -222,6 +278,14 @@ public class Settings {
|
||||
setKeyBinding(key, value);
|
||||
break;
|
||||
|
||||
case "graphics":
|
||||
if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) {
|
||||
setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(value));
|
||||
} else {
|
||||
log.warning("Unknown graphics key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Allow bare keys outside any section — treat as raw prefs
|
||||
log.fine("Setting raw preference: " + key + " = " + value);
|
||||
@@ -256,10 +320,18 @@ public class Settings {
|
||||
if (acHost != null && !acHost.isEmpty()) {
|
||||
w.println("autoConnectHost = " + acHost);
|
||||
w.println("autoConnectPort = " + getAutoConnectPort());
|
||||
w.println("autoConnectTls = " + getAutoConnectTls());
|
||||
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
|
||||
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
|
||||
}
|
||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||
w.println();
|
||||
|
||||
// [graphics]
|
||||
w.println("[graphics]");
|
||||
w.println("graphicsMode = " + getGraphicsMode().name());
|
||||
w.println();
|
||||
|
||||
// [colors]
|
||||
w.println("[colors]");
|
||||
// Host colors 0-15
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
package org.pubvm.j3270.ft;
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import org.lib3270j.ft.FTConfig;
|
||||
import org.lib3270j.ft.FTConstants.FTState;
|
||||
import org.lib3270j.ft.FTCut;
|
||||
import org.lib3270j.ft.FTDft;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConstants.FTState;
|
||||
import haus.nightmare.lib3270j.ft.FTCut;
|
||||
import haus.nightmare.lib3270j.ft.FTDft;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package org.pubvm.j3270.ft;
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import org.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
+86
-4
@@ -1,7 +1,7 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import org.lib3270j.ConnectionConfig;
|
||||
import org.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
@@ -14,7 +14,11 @@ public class ConnectDialog extends JDialog {
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JComboBox<TerminalModel> modelCombo;
|
||||
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
||||
private JTextField luField;
|
||||
private JCheckBox tlsCheckBox;
|
||||
private JCheckBox verifyCertCheckBox;
|
||||
private JCheckBox tn3270eCheckBox;
|
||||
private boolean confirmed;
|
||||
private ConnectionConfig result;
|
||||
|
||||
@@ -93,6 +97,67 @@ public class ConnectDialog extends JDialog {
|
||||
luField = createDarkField(12);
|
||||
mainPanel.add(luField, gbc);
|
||||
|
||||
// Graphics Mode
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.weightx = 0;
|
||||
JLabel graphicsLabel = new JLabel("Graphics:");
|
||||
graphicsLabel.setForeground(fg);
|
||||
graphicsLabel.setFont(labelFont);
|
||||
mainPanel.add(graphicsLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
graphicsCombo.setBackground(new Color(45, 45, 45));
|
||||
graphicsCombo.setForeground(fg);
|
||||
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
mainPanel.add(graphicsCombo, gbc);
|
||||
|
||||
// TLS / SSL Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 5;
|
||||
gbc.weightx = 1.0;
|
||||
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
|
||||
tlsCheckBox.setBackground(new Color(30, 30, 30));
|
||||
tlsCheckBox.setForeground(fg);
|
||||
tlsCheckBox.setFont(labelFont);
|
||||
tlsCheckBox.setFocusPainted(false);
|
||||
tlsCheckBox.addActionListener(e -> {
|
||||
boolean isTls = tlsCheckBox.isSelected();
|
||||
verifyCertCheckBox.setEnabled(isTls);
|
||||
String currentPort = portField.getText().trim();
|
||||
if (isTls && "23".equals(currentPort)) {
|
||||
portField.setText("992");
|
||||
} else if (!isTls && "992".equals(currentPort)) {
|
||||
portField.setText("23");
|
||||
}
|
||||
});
|
||||
mainPanel.add(tlsCheckBox, gbc);
|
||||
|
||||
// Verify Certificate Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 6;
|
||||
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
|
||||
verifyCertCheckBox.setBackground(new Color(30, 30, 30));
|
||||
verifyCertCheckBox.setForeground(new Color(160, 160, 160));
|
||||
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
verifyCertCheckBox.setSelected(true);
|
||||
verifyCertCheckBox.setEnabled(false);
|
||||
verifyCertCheckBox.setFocusPainted(false);
|
||||
mainPanel.add(verifyCertCheckBox, gbc);
|
||||
|
||||
// TN3270E Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 7;
|
||||
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
|
||||
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
|
||||
tn3270eCheckBox.setForeground(fg);
|
||||
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
||||
tn3270eCheckBox.setFocusPainted(false);
|
||||
mainPanel.add(tn3270eCheckBox, gbc);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||
@@ -116,7 +181,7 @@ public class ConnectDialog extends JDialog {
|
||||
buttonPanel.add(connectBtn);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridy = 8;
|
||||
gbc.gridwidth = 2;
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
|
||||
@@ -158,6 +223,10 @@ public class ConnectDialog extends JDialog {
|
||||
if (!lu.isEmpty()) {
|
||||
result.setLuName(lu);
|
||||
}
|
||||
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
||||
result.setUseTls(tlsCheckBox.isSelected());
|
||||
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
||||
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
@@ -178,4 +247,17 @@ public class ConnectDialog extends JDialog {
|
||||
public void setInitialPort(int port) {
|
||||
portField.setText(String.valueOf(port));
|
||||
}
|
||||
|
||||
public void setInitialTls(boolean tls) {
|
||||
tlsCheckBox.setSelected(tls);
|
||||
verifyCertCheckBox.setEnabled(tls);
|
||||
}
|
||||
|
||||
public void setInitialVerifyCert(boolean verify) {
|
||||
verifyCertCheckBox.setSelected(verify);
|
||||
}
|
||||
|
||||
public void setInitialTn3270e(boolean tn3270e) {
|
||||
tn3270eCheckBox.setSelected(tn3270e);
|
||||
}
|
||||
}
|
||||
+29
-12
@@ -1,9 +1,9 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import org.pubvm.j3270.J3270App;
|
||||
import org.pubvm.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
@@ -31,6 +31,7 @@ public class SettingsDialog extends JDialog {
|
||||
// Appearance tab
|
||||
private JComboBox<String> fontBox;
|
||||
private JSpinner fontSizeSpinner;
|
||||
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
|
||||
|
||||
// Behavior tab
|
||||
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
||||
@@ -264,8 +265,21 @@ public class SettingsDialog extends JDialog {
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(fontSizeSpinner, gbc);
|
||||
|
||||
// Fill remaining space
|
||||
// Graphics Mode
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
JLabel graphicsLabel = new JLabel("Graphics Mode:");
|
||||
panel.add(graphicsLabel, gbc);
|
||||
|
||||
graphicsModeBox = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||
graphicsModeBox.setSelectedItem(Settings.getGraphicsMode());
|
||||
gbc.gridx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(graphicsModeBox, gbc);
|
||||
|
||||
// Fill remaining space
|
||||
gbc.gridy = 3;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(Box.createGlue(), gbc);
|
||||
|
||||
@@ -356,7 +370,7 @@ public class SettingsDialog extends JDialog {
|
||||
hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors"));
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Color init = org.pubvm.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
|
||||
Color init = haus.nightmare.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
|
||||
tempHostColors[i] = init;
|
||||
JPanel cb = new JPanel();
|
||||
cb.setName("colorSwatch");
|
||||
@@ -388,7 +402,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
for (int i = 0; i < monoKeys.length; i++) {
|
||||
String mk = monoKeys[i];
|
||||
Color init = org.pubvm.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]);
|
||||
Color init = haus.nightmare.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]);
|
||||
tempMonoColors.put(mk, init);
|
||||
|
||||
JPanel p = new JPanel(new BorderLayout());
|
||||
@@ -452,7 +466,7 @@ public class SettingsDialog extends JDialog {
|
||||
for(String act : actions) {
|
||||
String def = act;
|
||||
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example
|
||||
String current = org.pubvm.j3270.config.Settings.getKeyBinding(act, def);
|
||||
String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def);
|
||||
tempKeyBindings.put(act, current);
|
||||
keymapModel.addRow(new Object[]{act, current});
|
||||
}
|
||||
@@ -464,13 +478,13 @@ public class SettingsDialog extends JDialog {
|
||||
defaultBinding = "shift F" + (i - 12);
|
||||
}
|
||||
String pf = "PF"+i;
|
||||
String cur = org.pubvm.j3270.config.Settings.getKeyBinding(pf, defaultBinding);
|
||||
String cur = haus.nightmare.j3270.config.Settings.getKeyBinding(pf, defaultBinding);
|
||||
tempKeyBindings.put(pf, cur);
|
||||
keymapModel.addRow(new Object[]{pf, cur});
|
||||
}
|
||||
for(int i=1;i<=3;i++){
|
||||
String pa = "PA"+i;
|
||||
String curPa = org.pubvm.j3270.config.Settings.getKeyBinding(pa, "alt "+i);
|
||||
String curPa = haus.nightmare.j3270.config.Settings.getKeyBinding(pa, "alt "+i);
|
||||
tempKeyBindings.put(pa, curPa);
|
||||
keymapModel.addRow(new Object[]{pa, curPa});
|
||||
}
|
||||
@@ -611,6 +625,9 @@ public class SettingsDialog extends JDialog {
|
||||
Settings.setFontFamily(fontFam);
|
||||
}
|
||||
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
|
||||
if (graphicsModeBox.getSelectedItem() != null) {
|
||||
Settings.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem());
|
||||
}
|
||||
|
||||
// Apply Behavior
|
||||
Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem();
|
||||
@@ -631,15 +648,15 @@ public class SettingsDialog extends JDialog {
|
||||
// Propagate visual changes to the app
|
||||
// Save Colors
|
||||
for (int i=0; i<16; i++) {
|
||||
org.pubvm.j3270.config.Settings.setColorOverride(i, tempHostColors[i]);
|
||||
haus.nightmare.j3270.config.Settings.setColorOverride(i, tempHostColors[i]);
|
||||
}
|
||||
for (Map.Entry<String, Color> entry : tempMonoColors.entrySet()) {
|
||||
org.pubvm.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue());
|
||||
haus.nightmare.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
// Save Keybindings
|
||||
for (Map.Entry<String, String> entry : tempKeyBindings.entrySet()) {
|
||||
org.pubvm.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue());
|
||||
haus.nightmare.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
parentApp.getTerminalPanel().reloadSettings();
|
||||
+57
-5
@@ -1,8 +1,8 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import org.lib3270j.ConnectionState;
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
@@ -15,12 +15,15 @@ import java.awt.*;
|
||||
public class StatusBar extends JPanel {
|
||||
|
||||
private final JLabel connectionStatus;
|
||||
private final JLabel tlsStatus;
|
||||
private final JLabel cursorPosition;
|
||||
private final JLabel luName;
|
||||
private final JLabel lockStatus;
|
||||
private final JLabel modelInfo;
|
||||
private final JButton lpButton;
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
|
||||
// OIA colors
|
||||
private static final Color OIA_BG = new Color(20, 20, 20);
|
||||
@@ -37,17 +40,35 @@ public class StatusBar extends JPanel {
|
||||
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
|
||||
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
||||
luName = createLabel("", oiaFont, OIA_FG);
|
||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
|
||||
|
||||
lpButton = new JButton("LightPen: OFF");
|
||||
lpButton.setFont(oiaFont);
|
||||
lpButton.setForeground(OIA_FG);
|
||||
lpButton.setBackground(OIA_BG);
|
||||
lpButton.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
|
||||
lpButton.setFocusable(false);
|
||||
lpButton.addActionListener(e -> {
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.toggleLightPen();
|
||||
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
|
||||
}
|
||||
});
|
||||
|
||||
add(Box.createHorizontalStrut(6));
|
||||
add(connectionStatus);
|
||||
add(Box.createHorizontalStrut(10));
|
||||
add(tlsStatus);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(luName);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(lockStatus);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(lpButton);
|
||||
add(Box.createHorizontalGlue());
|
||||
add(modelInfo);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
@@ -62,8 +83,13 @@ public class StatusBar extends JPanel {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.setOnLightPenToggle(this::updateStatus);
|
||||
}
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
@@ -110,6 +136,26 @@ public class StatusBar extends JPanel {
|
||||
break;
|
||||
}
|
||||
|
||||
// TLS Status
|
||||
if (state.isConnected() && client.getConfig().isUseTls()) {
|
||||
boolean verified = client.getConfig().isTlsVerifyCert();
|
||||
javax.net.ssl.SSLSession session = client.getSslSession();
|
||||
String cipher = session != null ? session.getCipherSuite() : "TLS";
|
||||
String protocol = session != null ? session.getProtocol() : "TLS";
|
||||
if (verified) {
|
||||
tlsStatus.setText("🔒 TLS");
|
||||
tlsStatus.setForeground(OIA_FG);
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
|
||||
} else {
|
||||
tlsStatus.setText("🔓 TLS (Unverified)");
|
||||
tlsStatus.setForeground(new Color(255, 180, 80));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
|
||||
}
|
||||
} else {
|
||||
tlsStatus.setText("");
|
||||
tlsStatus.setToolTipText(null);
|
||||
}
|
||||
|
||||
// LU name
|
||||
String lu = "";
|
||||
if (client.getConnectionState().isTn3270e()) {
|
||||
@@ -136,5 +182,11 @@ public class StatusBar extends JPanel {
|
||||
int row = sb.getCursorRow() + 1;
|
||||
int col = sb.getCursorCol() + 1;
|
||||
cursorPosition.setText(String.format("%03d/%03d", row, col));
|
||||
|
||||
// Light Pen indicator — sync with actual state
|
||||
if (terminalPanel != null) {
|
||||
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
|
||||
lpButton.setForeground(terminalPanel.isLightPenMode() ? new Color(255, 255, 80) : OIA_FG);
|
||||
}
|
||||
}
|
||||
}
|
||||
+350
-79
@@ -1,15 +1,16 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Custom JPanel that renders the 3270 screen buffer.
|
||||
@@ -22,6 +23,7 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
// Font and cell dimensions
|
||||
private Font terminalFont;
|
||||
private Font boldTerminalFont;
|
||||
private int cellWidth;
|
||||
private int cellHeight;
|
||||
private int fontAscent;
|
||||
@@ -47,12 +49,23 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
// ========== Selection / Copy-Paste state ==========
|
||||
private boolean blockSelectMode = false;
|
||||
|
||||
private static final String[] CHAR_STRINGS = new String[128];
|
||||
static {
|
||||
for (int i = 0; i < 128; i++) {
|
||||
CHAR_STRINGS[i] = String.valueOf((char) i);
|
||||
}
|
||||
}
|
||||
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
|
||||
private int selectionStartRow = -1, selectionStartCol = -1;
|
||||
private int selectionEndRow = -1, selectionEndCol = -1;
|
||||
private boolean isDragging = false;
|
||||
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
|
||||
|
||||
// ========== Resize guard ==========
|
||||
private boolean lightPenMode = false;
|
||||
private long lastGraphicsUpdateCount = -1;
|
||||
private java.awt.image.BufferedImage cachedGraphicsImage = null;
|
||||
private boolean resizeGuard = false;
|
||||
|
||||
/**
|
||||
@@ -61,7 +74,7 @@ public class TerminalPanel extends JPanel {
|
||||
*/
|
||||
private int getRenderOffsetX() {
|
||||
int termCols = 80;
|
||||
if (client != null) termCols = client.getScreenBuffer().getCols();
|
||||
if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
|
||||
int gridW = termCols * cellWidth;
|
||||
int extra = getWidth() - gridW;
|
||||
return Math.max(padding, extra / 2);
|
||||
@@ -69,10 +82,11 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
/**
|
||||
* Compute the vertical render offset to center the grid within the panel.
|
||||
* Must use getDisplayRows() to match paintComponent's iteration.
|
||||
*/
|
||||
private int getRenderOffsetY() {
|
||||
int termRows = 24;
|
||||
if (client != null) termRows = client.getScreenBuffer().getRows();
|
||||
if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
|
||||
int gridH = termRows * cellHeight;
|
||||
int extra = getHeight() - gridH;
|
||||
return Math.max(padding, extra / 2);
|
||||
@@ -121,7 +135,7 @@ public class TerminalPanel extends JPanel {
|
||||
setupFont();
|
||||
setupCursorBlink();
|
||||
|
||||
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
||||
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
|
||||
|
||||
// Handle mouse clicks to position cursor, selection, and grab focus
|
||||
MouseAdapter mouseHandler = new MouseAdapter() {
|
||||
@@ -139,16 +153,27 @@ public class TerminalPanel extends JPanel {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
int displayCols = sb.getDisplayCols();
|
||||
int displayRows = sb.getDisplayRows();
|
||||
int col = (e.getX() - ox) / cellWidth;
|
||||
int row = (e.getY() - oy) / cellHeight;
|
||||
col = Math.max(0, Math.min(col, sb.getCols() - 1));
|
||||
row = Math.max(0, Math.min(row, sb.getRows() - 1));
|
||||
col = Math.max(0, Math.min(col, displayCols - 1));
|
||||
row = Math.max(0, Math.min(row, displayRows - 1));
|
||||
|
||||
// Start selection
|
||||
selectionStartRow = row;
|
||||
selectionStartCol = col;
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
// IMPORTANT ARCHITECTURE NOTE:
|
||||
// 1. In GDDM Graphic Cursor Mode (3179G / GOCA), mouse events represent graphic light-pen touches.
|
||||
// Text drag-selection is explicitly suppressed so blue selection boxes do not artifact over graphics.
|
||||
// 2. In Text Light Pen mode (Alt+L on 3270 formatted screens), clicks toggle selectable fields.
|
||||
// 3. In standard alphanumeric mode, click-drag selects text for copy-paste.
|
||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
||||
if (isGraphic || lightPenMode) {
|
||||
clearSelection();
|
||||
} else {
|
||||
selectionStartRow = row;
|
||||
selectionStartCol = col;
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
}
|
||||
isDragging = true;
|
||||
repaint();
|
||||
}
|
||||
@@ -157,30 +182,114 @@ public class TerminalPanel extends JPanel {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (isDragging && client != null && client.getConnectionState().isFullSession()) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
int col = (e.getX() - ox) / cellWidth;
|
||||
int row = (e.getY() - oy) / cellHeight;
|
||||
col = Math.max(0, Math.min(col, sb.getCols() - 1));
|
||||
row = Math.max(0, Math.min(row, sb.getRows() - 1));
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
||||
if (isGraphic && client.getGraphicsPlane() != null) {
|
||||
int gridW = sb.getDisplayCols() * cellWidth;
|
||||
int gridH = sb.getDisplayRows() * cellHeight;
|
||||
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
||||
int gHeight = client.getGraphicsPlane().getCanvasHeight();
|
||||
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
|
||||
int py = (gridH > 0 && gHeight > 0) ? (int) Math.round((double) (e.getY() - oy) * gHeight / gridH) : (e.getY() - oy);
|
||||
px = Math.max(0, Math.min(px, gWidth - 1));
|
||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
||||
} else if (!lightPenMode) {
|
||||
int col = (e.getX() - ox) / cellWidth;
|
||||
int row = (e.getY() - oy) / cellHeight;
|
||||
col = Math.max(0, Math.min(col, sb.getDisplayCols() - 1));
|
||||
row = Math.max(0, Math.min(row, sb.getDisplayRows() - 1));
|
||||
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
if (client != null && client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int gridW = sb.getDisplayCols() * cellWidth;
|
||||
int gridH = sb.getDisplayRows() * cellHeight;
|
||||
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
||||
int gHeight = client.getGraphicsPlane().getCanvasHeight();
|
||||
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
|
||||
int py = (gridH > 0 && gHeight > 0) ? (int) Math.round((double) (e.getY() - oy) * gHeight / gridH) : (e.getY() - oy);
|
||||
px = Math.max(0, Math.min(px, gWidth - 1));
|
||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
// If start == end, treat as a click (position cursor, clear selection)
|
||||
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
int displayCols = sb.getDisplayCols();
|
||||
int displayRows = sb.getDisplayRows();
|
||||
int col = (e.getX() - ox) / cellWidth;
|
||||
int row = (e.getY() - oy) / cellHeight;
|
||||
col = Math.max(0, Math.min(col, displayCols - 1));
|
||||
row = Math.max(0, Math.min(row, displayRows - 1));
|
||||
int clickAddr = row * displayCols + col;
|
||||
|
||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null;
|
||||
|
||||
if (isGraphic) {
|
||||
clearSelection();
|
||||
int gridW = displayCols * cellWidth;
|
||||
int gridH = displayRows * cellHeight;
|
||||
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
||||
int gHeight = client.getGraphicsPlane().getCanvasHeight();
|
||||
int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox);
|
||||
int py = (gridH > 0 && gHeight > 0) ? (int) Math.round((double) (e.getY() - oy) * gHeight / gridH) : (e.getY() - oy);
|
||||
px = Math.max(0, Math.min(px, gWidth - 1));
|
||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
||||
|
||||
int gx = client.getGocaDecoder().getGraphicCursorX();
|
||||
int gy = client.getGocaDecoder().getGraphicCursorY();
|
||||
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
|
||||
System.err.println(String.format(
|
||||
"TerminalPanel.mouseReleased: mouse=(%d, %d) offset=(%d, %d) grid=(%dx%d) px=(%d, %d) goca=(%d, %d) btn=%d",
|
||||
e.getX(), e.getY(), ox, oy, gridW, gridH, px, py, gx, gy, button
|
||||
));
|
||||
|
||||
client.getInputProcessor().sendGraphicMouseAid(
|
||||
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
||||
button,
|
||||
e.isShiftDown(),
|
||||
e.isControlDown()
|
||||
);
|
||||
refreshScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (lightPenMode) {
|
||||
clearSelection();
|
||||
sb.setCursorAddress(clickAddr);
|
||||
boolean result = client.getInputProcessor().lightPenSelect(clickAddr);
|
||||
if (result) {
|
||||
refreshScreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
|
||||
sb.setCursorAddress(clickAddr);
|
||||
clearSelection();
|
||||
refreshScreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
repaint();
|
||||
@@ -329,7 +438,7 @@ public class TerminalPanel extends JPanel {
|
||||
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
||||
blockModeItem.addActionListener(ev -> {
|
||||
blockSelectMode = blockModeItem.isSelected();
|
||||
org.pubvm.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
|
||||
haus.nightmare.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
|
||||
});
|
||||
popup.add(blockModeItem);
|
||||
|
||||
@@ -381,8 +490,8 @@ public class TerminalPanel extends JPanel {
|
||||
int termRows = 24;
|
||||
if (client != null) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
termCols = sb.getCols();
|
||||
termRows = sb.getRows();
|
||||
termCols = sb.getDisplayCols();
|
||||
termRows = sb.getDisplayRows();
|
||||
}
|
||||
|
||||
// Use minimal padding for the fit calculation
|
||||
@@ -407,7 +516,7 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
if (bestSize != currentFontSize) {
|
||||
currentFontSize = bestSize;
|
||||
org.pubvm.j3270.config.Settings.setFontSize(bestSize);
|
||||
haus.nightmare.j3270.config.Settings.setFontSize(bestSize);
|
||||
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
||||
updateCellSize();
|
||||
}
|
||||
@@ -464,7 +573,7 @@ public class TerminalPanel extends JPanel {
|
||||
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
||||
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
||||
for (String key : navKeys) {
|
||||
String binding = org.pubvm.j3270.config.Settings.getKeyBinding(key, key);
|
||||
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding(key, key);
|
||||
bindKeyToMap(im, key, binding);
|
||||
}
|
||||
|
||||
@@ -477,7 +586,7 @@ public class TerminalPanel extends JPanel {
|
||||
} else {
|
||||
defaultBinding = "shift F" + (i - 12);
|
||||
}
|
||||
String binding = org.pubvm.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
|
||||
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
|
||||
bindKeyToMap(im, "PF" + i, binding);
|
||||
}
|
||||
|
||||
@@ -485,25 +594,26 @@ public class TerminalPanel extends JPanel {
|
||||
String pa1Def = "alt 1";
|
||||
String pa2Def = "alt 2";
|
||||
String pa3Def = "alt 3";
|
||||
bindKeyToMap(im, "PA1", org.pubvm.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
|
||||
bindKeyToMap(im, "PA2", org.pubvm.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
|
||||
bindKeyToMap(im, "PA3", org.pubvm.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
|
||||
bindKeyToMap(im, "PA1", haus.nightmare.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
|
||||
bindKeyToMap(im, "PA2", haus.nightmare.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
|
||||
bindKeyToMap(im, "PA3", haus.nightmare.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
|
||||
|
||||
// Clear
|
||||
bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
||||
bindKeyToMap(im, "CLEAR", haus.nightmare.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
||||
|
||||
// Extended 3270 functions
|
||||
bindKeyToMap(im, "ERASE_INPUT", org.pubvm.j3270.config.Settings.getKeyBinding("ERASE_INPUT", "alt E"));
|
||||
bindKeyToMap(im, "NEWLINE", org.pubvm.j3270.config.Settings.getKeyBinding("NEWLINE", "shift ENTER"));
|
||||
bindKeyToMap(im, "DUP", org.pubvm.j3270.config.Settings.getKeyBinding("DUP", "alt D"));
|
||||
bindKeyToMap(im, "FIELD_MARK", org.pubvm.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
|
||||
bindKeyToMap(im, "ATTN", org.pubvm.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
|
||||
bindKeyToMap(im, "SYSREQ", org.pubvm.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
||||
bindKeyToMap(im, "ERASE_INPUT", haus.nightmare.j3270.config.Settings.getKeyBinding("ERASE_INPUT", "alt E"));
|
||||
bindKeyToMap(im, "NEWLINE", haus.nightmare.j3270.config.Settings.getKeyBinding("NEWLINE", "shift ENTER"));
|
||||
bindKeyToMap(im, "DUP", haus.nightmare.j3270.config.Settings.getKeyBinding("DUP", "alt D"));
|
||||
bindKeyToMap(im, "FIELD_MARK", haus.nightmare.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
|
||||
bindKeyToMap(im, "ATTN", haus.nightmare.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
|
||||
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
||||
|
||||
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
|
||||
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN");
|
||||
|
||||
// Create actions for all bound keys
|
||||
am.put("j3270-ENTER", createAction(this::handleEnter));
|
||||
@@ -532,6 +642,7 @@ public class TerminalPanel extends JPanel {
|
||||
// Copy/Paste actions
|
||||
am.put("j3270-COPY", createAction(this::copySelection));
|
||||
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
|
||||
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
final int pf = i;
|
||||
@@ -714,6 +825,9 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
|
||||
private void refreshScreen() {
|
||||
if (client != null) {
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
}
|
||||
repaint();
|
||||
// Notify parent to update status bar too
|
||||
Container parent = getParent();
|
||||
@@ -727,8 +841,8 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
|
||||
private void setupFont() {
|
||||
currentFontSize = org.pubvm.j3270.config.Settings.getFontSize();
|
||||
String fontFamily = org.pubvm.j3270.config.Settings.getFontFamily();
|
||||
currentFontSize = haus.nightmare.j3270.config.Settings.getFontSize();
|
||||
String fontFamily = haus.nightmare.j3270.config.Settings.getFontFamily();
|
||||
|
||||
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
||||
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
||||
@@ -740,13 +854,13 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
private void setupColors() {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
hostColors[i] = org.pubvm.j3270.config.Settings.getColorOverride(i, DEFAULT_HOST_COLORS[i]);
|
||||
hostColors[i] = haus.nightmare.j3270.config.Settings.getColorOverride(i, DEFAULT_HOST_COLORS[i]);
|
||||
}
|
||||
monoNormal = org.pubvm.j3270.config.Settings.getMonoColorOverride("NORMAL", DEFAULT_MONO_NORMAL);
|
||||
monoIntensify = org.pubvm.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY);
|
||||
monoProtected = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED);
|
||||
monoProtectedHigh = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH);
|
||||
bgColor = org.pubvm.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR);
|
||||
monoNormal = haus.nightmare.j3270.config.Settings.getMonoColorOverride("NORMAL", DEFAULT_MONO_NORMAL);
|
||||
monoIntensify = haus.nightmare.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY);
|
||||
monoProtected = haus.nightmare.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED);
|
||||
monoProtectedHigh = haus.nightmare.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH);
|
||||
bgColor = haus.nightmare.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR);
|
||||
setBackground(bgColor);
|
||||
}
|
||||
|
||||
@@ -754,14 +868,14 @@ public class TerminalPanel extends JPanel {
|
||||
setupColors();
|
||||
setupKeyBindings();
|
||||
setupFont();
|
||||
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
||||
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setFontSize(int size) {
|
||||
currentFontSize = size;
|
||||
org.pubvm.j3270.config.Settings.setFontSize(size);
|
||||
haus.nightmare.j3270.config.Settings.setFontSize(size);
|
||||
terminalFont = terminalFont.deriveFont((float) size);
|
||||
updateCellSize();
|
||||
resizeGuard = true;
|
||||
@@ -779,6 +893,7 @@ public class TerminalPanel extends JPanel {
|
||||
cellHeight = fm.getHeight();
|
||||
fontAscent = fm.getAscent();
|
||||
fontDescent = fm.getDescent();
|
||||
boldTerminalFont = terminalFont.deriveFont(Font.BOLD);
|
||||
}
|
||||
|
||||
private void setupCursorBlink() {
|
||||
@@ -791,6 +906,63 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
if (client != null) {
|
||||
setupGraphicsPlaneRenderer();
|
||||
updateCellSize();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupGraphicsPlaneRenderer() {
|
||||
if (client != null && client.getGraphicsPlane() != null) {
|
||||
client.getGraphicsPlane().setTextRenderer((plane, x, y, text, colorArgb, cw, ch, dir, angle) -> {
|
||||
int[] rgb = plane.getRgbBuffer();
|
||||
int pw = plane.getCanvasWidth();
|
||||
int ph = plane.getCanvasHeight();
|
||||
if (rgb == null || pw <= 0 || ph <= 0) return;
|
||||
|
||||
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(
|
||||
pw, ph, java.awt.image.BufferedImage.TYPE_INT_ARGB
|
||||
);
|
||||
img.setRGB(0, 0, pw, ph, rgb, 0, pw);
|
||||
|
||||
Graphics2D g2 = img.createGraphics();
|
||||
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||
|
||||
int fontSize = (int) Math.round(ch * 0.95);
|
||||
if (fontSize < 10) fontSize = 10;
|
||||
Font f = (boldTerminalFont != null ? boldTerminalFont : terminalFont).deriveFont((float) fontSize);
|
||||
g2.setFont(f);
|
||||
FontMetrics fm = g2.getFontMetrics();
|
||||
int ascent = fm.getAscent();
|
||||
|
||||
g2.setColor(new Color(colorArgb, true));
|
||||
|
||||
double curX = x;
|
||||
double curY = y;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
String s = text.substring(i, i + 1);
|
||||
int charW = fm.stringWidth(s);
|
||||
int drawX = (int) Math.round(curX + Math.max(0, (cw - charW) / 2.0));
|
||||
int drawY = (int) Math.round(curY + ascent + Math.max(0, (ch - fm.getHeight()) / 2.0));
|
||||
g2.drawString(s, drawX, drawY);
|
||||
|
||||
switch (dir) {
|
||||
case GocaConstants.CD_TB: curY += ch; break;
|
||||
case GocaConstants.CD_RL: curX -= cw; break;
|
||||
case GocaConstants.CD_BT: curY -= ch; break;
|
||||
case GocaConstants.CD_LR:
|
||||
case GocaConstants.CD_DEFAULT:
|
||||
default:
|
||||
curX += cw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
g2.dispose();
|
||||
img.getRGB(0, 0, pw, ph, rgb, 0, pw);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -800,8 +972,8 @@ public class TerminalPanel extends JPanel {
|
||||
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
|
||||
// When the host switches to alternate screen, onScreenSizeChanged fires
|
||||
// and the frame re-packs.
|
||||
int displayCols = sb.getCols();
|
||||
int displayRows = sb.getRows();
|
||||
int displayCols = sb.getDisplayCols();
|
||||
int displayRows = sb.getDisplayRows();
|
||||
return new Dimension(displayCols * cellWidth + padding * 2,
|
||||
displayRows * cellHeight + padding * 2);
|
||||
}
|
||||
@@ -817,7 +989,12 @@ public class TerminalPanel extends JPanel {
|
||||
|
||||
Graphics2D g2 = (Graphics2D) g;
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
||||
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||
|
||||
// Clear entire panel with background color
|
||||
g2.setColor(bgColor);
|
||||
@@ -828,10 +1005,32 @@ public class TerminalPanel extends JPanel {
|
||||
int oy = getRenderOffsetY();
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getRows();
|
||||
int cols = sb.getCols();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
boolean isColorModel = client.getConfig().getModel().isColor();
|
||||
|
||||
// Draw Vector Graphics Plane under text if present
|
||||
if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) {
|
||||
int gridW = cols * cellWidth;
|
||||
int gridH = rows * cellHeight;
|
||||
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
||||
int gHeight = client.getGraphicsPlane().getCanvasHeight();
|
||||
int[] rgb = client.getGraphicsPlane().getRgbBuffer();
|
||||
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
||||
long currentUpdateCount = client.getGraphicsPlane().getUpdateCount();
|
||||
if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) {
|
||||
cachedGraphicsImage = new java.awt.image.BufferedImage(gWidth, gHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
cachedGraphicsImage.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
|
||||
lastGraphicsUpdateCount = currentUpdateCount;
|
||||
}
|
||||
if (gWidth == gridW && gHeight == gridH) {
|
||||
g2.drawImage(cachedGraphicsImage, ox, oy, null);
|
||||
} else {
|
||||
g2.drawImage(cachedGraphicsImage, ox, oy, gridW, gridH, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track current field attribute for monochrome color decisions
|
||||
byte currentFA = 0;
|
||||
ExtendedAttribute currentFieldEa = null;
|
||||
@@ -839,7 +1038,7 @@ public class TerminalPanel extends JPanel {
|
||||
for (int row = 0; row < rows; row++) {
|
||||
for (int col = 0; col < cols; col++) {
|
||||
int baddr = row * cols + col;
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
ExtendedAttribute ea = sb.getDisplayCell(baddr);
|
||||
|
||||
int x = ox + col * cellWidth;
|
||||
int y = oy + row * cellHeight;
|
||||
@@ -854,10 +1053,7 @@ public class TerminalPanel extends JPanel {
|
||||
if (ea.isFieldAttribute()) {
|
||||
currentFA = ea.fa;
|
||||
currentFieldEa = ea;
|
||||
// Field attributes display as blanks
|
||||
g2.setColor(this.bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
// Selection highlight on field attribute cells too
|
||||
// Selection highlight on field attribute cells
|
||||
if (isCellSelected(row, col)) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
@@ -889,10 +1085,17 @@ public class TerminalPanel extends JPanel {
|
||||
if (faIsHigh(currentFA & 0xFF))
|
||||
bold = true;
|
||||
|
||||
// Handle invisible fields (zero intensity)
|
||||
// Handle invisible fields (zero intensity / password fields)
|
||||
// Modern UX: render '*' for typed characters so user sees length/digit count
|
||||
if (faIsZero(currentFA & 0xFF)) {
|
||||
g2.setColor(this.bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? boldTerminalFont : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
g2.drawString("*", x, y + fontAscent);
|
||||
}
|
||||
|
||||
if (isCellSelected(row, col)) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
@@ -907,17 +1110,39 @@ public class TerminalPanel extends JPanel {
|
||||
bgColor = tmp;
|
||||
}
|
||||
|
||||
// Draw background
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
// Draw background only if different from default panel bgColor or if inverted
|
||||
if (!bgColor.equals(this.bgColor) || reverse) {
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
|
||||
// Draw character
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
||||
// Draw character or Programmed Symbol
|
||||
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
|
||||
boolean drawnAsPs = false;
|
||||
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
|
||||
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
|
||||
if (slot != null) {
|
||||
int symBg = (!bgColor.equals(this.bgColor) || reverse) ? bgColor.getRGB() : 0;
|
||||
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), symBg);
|
||||
if (img != null) {
|
||||
g2.drawImage(img, x, y, null);
|
||||
drawnAsPs = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!drawnAsPs) {
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? boldTerminalFont : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
if (ch < 128) {
|
||||
g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent);
|
||||
} else {
|
||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw underline
|
||||
@@ -935,19 +1160,47 @@ public class TerminalPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
// Draw Graphic Cursor (Light-Pen / interactive graphics pointer) if active
|
||||
if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
|
||||
int gocaX = client.getGocaDecoder().getGraphicCursorX();
|
||||
int gocaY = client.getGocaDecoder().getGraphicCursorY();
|
||||
int canvasPx = client.getGraphicsPlane().mapX(gocaX);
|
||||
int canvasPy = client.getGraphicsPlane().mapY(gocaY);
|
||||
int gridW = cols * cellWidth;
|
||||
int gridH = rows * cellHeight;
|
||||
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
||||
int gHeight = client.getGraphicsPlane().getCanvasHeight();
|
||||
int px = ox + (gWidth > 0 ? (int)Math.round((double)canvasPx * gridW / gWidth) : canvasPx);
|
||||
int py = oy + (gHeight > 0 ? (int)Math.round((double)canvasPy * gridH / gHeight) : canvasPy);
|
||||
|
||||
g2.setColor(Color.WHITE);
|
||||
g2.setXORMode(Color.BLACK);
|
||||
// Draw a crosshair cursor for the light-pen / graphic cursor
|
||||
g2.drawLine(px - 6, py, px + 6, py);
|
||||
g2.drawLine(px, py - 6, px, py + 6);
|
||||
g2.setPaintMode();
|
||||
}
|
||||
|
||||
// Draw 3270 text cursor
|
||||
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int curAddr = sb.getDisplayCursorAddress();
|
||||
int curRow = curAddr / cols;
|
||||
int curCol = curAddr % cols;
|
||||
int cx = ox + curCol * cellWidth;
|
||||
int cy = oy + curRow * cellHeight;
|
||||
|
||||
g2.setColor(new Color(255, 255, 255, 180));
|
||||
g2.setColor(CURSOR_COLOR);
|
||||
g2.setXORMode(bgColor);
|
||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||
g2.setPaintMode();
|
||||
}
|
||||
|
||||
// Draw Light Pen mode indicator
|
||||
if (lightPenMode) {
|
||||
g2.setFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
|
||||
g2.setColor(new Color(50, 255, 50));
|
||||
g2.drawString("LP", ox + 2, oy + rows * cellHeight + 14);
|
||||
}
|
||||
}
|
||||
|
||||
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
||||
@@ -988,4 +1241,22 @@ public class TerminalPanel extends JPanel {
|
||||
if (blinkTimer != null)
|
||||
blinkTimer.stop();
|
||||
}
|
||||
private Runnable onLightPenToggle;
|
||||
|
||||
public void setOnLightPenToggle(Runnable callback) {
|
||||
this.onLightPenToggle = callback;
|
||||
}
|
||||
|
||||
public void toggleLightPen() {
|
||||
this.lightPenMode = !this.lightPenMode;
|
||||
System.out.println("Light Pen mode: " + (this.lightPenMode ? "ON" : "OFF"));
|
||||
if (onLightPenToggle != null) {
|
||||
onLightPenToggle.run();
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
public boolean isLightPenMode() {
|
||||
return this.lightPenMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Modal dialog for inspecting and confirming untrusted TLS/SSL server certificates.
|
||||
*/
|
||||
public class UntrustedCertificateDialog extends JDialog {
|
||||
|
||||
private boolean accepted = false;
|
||||
|
||||
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
super(parent, "Untrusted SSL/TLS Certificate", true);
|
||||
buildUI(host, port, chain, exception);
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
setResizable(false);
|
||||
}
|
||||
|
||||
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
mainPanel.setBackground(new Color(30, 30, 30));
|
||||
|
||||
// Header
|
||||
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
|
||||
headerPanel.setBackground(new Color(30, 30, 30));
|
||||
JLabel iconLabel = new JLabel("⚠️");
|
||||
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
|
||||
headerPanel.add(iconLabel);
|
||||
|
||||
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
|
||||
titleBox.setBackground(new Color(30, 30, 30));
|
||||
JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
|
||||
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
|
||||
titleLabel.setForeground(new Color(255, 180, 80));
|
||||
titleBox.add(titleLabel);
|
||||
|
||||
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
|
||||
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
subtitleLabel.setForeground(new Color(180, 180, 180));
|
||||
titleBox.add(subtitleLabel);
|
||||
headerPanel.add(titleBox);
|
||||
mainPanel.add(headerPanel, BorderLayout.NORTH);
|
||||
|
||||
// Certificate Details Area
|
||||
X509Certificate cert = (chain != null && chain.length > 0) ? chain[0] : null;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (exception != null) {
|
||||
sb.append("Validation Error:\n ").append(exception.getMessage() != null ? exception.getMessage() : exception.toString()).append("\n\n");
|
||||
}
|
||||
if (cert != null) {
|
||||
sb.append("Subject:\n ").append(cert.getSubjectX500Principal().getName()).append("\n\n");
|
||||
sb.append("Issuer:\n ").append(cert.getIssuerX500Principal().getName()).append("\n\n");
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||
sb.append("Validity:\n From: ").append(sdf.format(cert.getNotBefore()))
|
||||
.append("\n To: ").append(sdf.format(cert.getNotAfter())).append("\n\n");
|
||||
sb.append("Serial Number:\n ").append(cert.getSerialNumber().toString(16).toUpperCase()).append("\n\n");
|
||||
sb.append("SHA-256 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-256")).append("\n\n");
|
||||
sb.append("SHA-1 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-1"));
|
||||
} else {
|
||||
sb.append("No peer certificate information available.");
|
||||
}
|
||||
|
||||
JTextArea detailsArea = new JTextArea(sb.toString());
|
||||
detailsArea.setEditable(false);
|
||||
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
detailsArea.setBackground(new Color(20, 20, 20));
|
||||
detailsArea.setForeground(new Color(210, 210, 210));
|
||||
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(detailsArea);
|
||||
scrollPane.setPreferredSize(new Dimension(520, 260));
|
||||
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
|
||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel Connection");
|
||||
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||
cancelBtn.setForeground(new Color(220, 220, 220));
|
||||
cancelBtn.addActionListener(e -> {
|
||||
accepted = false;
|
||||
dispose();
|
||||
});
|
||||
|
||||
JButton trustBtn = new JButton("Connect Anyway");
|
||||
trustBtn.setBackground(new Color(180, 100, 40));
|
||||
trustBtn.setForeground(Color.WHITE);
|
||||
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||
trustBtn.addActionListener(e -> {
|
||||
accepted = true;
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(trustBtn);
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
getRootPane().setDefaultButton(trustBtn);
|
||||
}
|
||||
|
||||
public boolean isAccepted() {
|
||||
return accepted;
|
||||
}
|
||||
|
||||
private static String computeFingerprint(X509Certificate cert, String algorithm) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(algorithm);
|
||||
byte[] digest = md.digest(cert.getEncoded());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < digest.length; i++) {
|
||||
if (i > 0) sb.append(':');
|
||||
sb.append(String.format("%02X", digest[i]));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "Unable to compute fingerprint: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the prompt modal dialog on the Swing EDT and return user choice.
|
||||
*/
|
||||
public static boolean showPrompt(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
|
||||
dialog.setVisible(true);
|
||||
return dialog.isAccepted();
|
||||
} else {
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
|
||||
dialog.setVisible(true);
|
||||
result.set(dialog.isAccepted());
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
return result.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
jar {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
/**
|
||||
* Configuration for a 3270 terminal connection.
|
||||
*/
|
||||
public class ConnectionConfig {
|
||||
|
||||
private String host;
|
||||
private int port = 23;
|
||||
private TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
private String luName = null;
|
||||
private boolean extendedDataStream = true;
|
||||
private boolean useTls = false;
|
||||
private boolean tlsVerifyCert = true;
|
||||
private haus.nightmare.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
|
||||
private String sslProtocol = "TLS";
|
||||
private int connectTimeoutMs = 15000;
|
||||
private int nopIntervalSeconds = 0;
|
||||
private String terminalName = null; // override terminal type string
|
||||
private boolean tn3270eEnabled = true;
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
|
||||
public TerminalModel getModel() { return model; }
|
||||
public void setModel(TerminalModel model) { this.model = model; }
|
||||
|
||||
public String getLuName() { return luName; }
|
||||
public void setLuName(String luName) { this.luName = luName; }
|
||||
|
||||
public boolean isExtendedDataStream() { return extendedDataStream; }
|
||||
public void setExtendedDataStream(boolean ext) { this.extendedDataStream = ext; }
|
||||
|
||||
public boolean isUseTls() { return useTls; }
|
||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||
|
||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
|
||||
|
||||
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
|
||||
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
|
||||
|
||||
public haus.nightmare.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||
public void setCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
||||
|
||||
public String getSslProtocol() { return sslProtocol; }
|
||||
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
|
||||
|
||||
public int getConnectTimeoutMs() { return connectTimeoutMs; }
|
||||
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
|
||||
|
||||
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
||||
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
|
||||
public void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||
this.graphicsMode = (mode != null) ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
public String getTerminalName() { return terminalName; }
|
||||
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"),
|
||||
* 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) {
|
||||
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||
return new ConnectionConfig("localhost", defaultPort, defaultModel);
|
||||
}
|
||||
String s = hostStr.trim();
|
||||
boolean tls = false;
|
||||
boolean tn3270e = true;
|
||||
|
||||
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
|
||||
boolean prefixFound = true;
|
||||
while (prefixFound) {
|
||||
prefixFound = false;
|
||||
if (s.startsWith("L:") || s.startsWith("l:") || s.startsWith("Y:") || s.startsWith("y:")) {
|
||||
tls = true;
|
||||
s = s.substring(2);
|
||||
prefixFound = true;
|
||||
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
|
||||
tls = true;
|
||||
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;
|
||||
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||
|
||||
// Check for host:port (handle IPv6 [::1]:port)
|
||||
if (s.startsWith("[") && s.contains("]")) {
|
||||
int closeBracket = s.indexOf(']');
|
||||
host = s.substring(1, closeBracket);
|
||||
if (s.length() > closeBracket + 1 && s.charAt(closeBracket + 1) == ':') {
|
||||
try {
|
||||
port = Integer.parseInt(s.substring(closeBracket + 2));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
} else {
|
||||
int colon = s.lastIndexOf(':');
|
||||
if (colon > 0 && colon < s.length() - 1) {
|
||||
try {
|
||||
port = Integer.parseInt(s.substring(colon + 1));
|
||||
host = s.substring(0, colon);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective terminal type string to send during negotiation.
|
||||
*/
|
||||
public String getEffectiveTerminalType() {
|
||||
if (terminalName != null) {
|
||||
return terminalName;
|
||||
}
|
||||
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j;
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
/**
|
||||
* Connection state machine states.
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
package org.lib3270j;
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.listener.*;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.listener.*;
|
||||
|
||||
import java.util.logging.*;
|
||||
|
||||
@@ -15,7 +15,7 @@ public class DiagnosticClient {
|
||||
static Telnet3270Client client;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Logger rootLogger = Logger.getLogger("org.lib3270j");
|
||||
Logger rootLogger = Logger.getLogger("haus.nightmare.lib3270j");
|
||||
rootLogger.setLevel(Level.ALL);
|
||||
Handler handler = new ConsoleHandler();
|
||||
handler.setLevel(Level.ALL);
|
||||
@@ -26,7 +26,7 @@ public class DiagnosticClient {
|
||||
String host = args.length >= 1 ? args[0] : "192.168.0.30";
|
||||
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270;
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_2);
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4);
|
||||
client = new Telnet3270Client(config);
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
+36
-11
@@ -1,13 +1,13 @@
|
||||
package org.lib3270j;
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import org.lib3270j.datastream.DataStreamProcessor;
|
||||
import org.lib3270j.input.InputProcessor;
|
||||
import org.lib3270j.listener.ConnectionListener;
|
||||
import org.lib3270j.listener.ScreenUpdateListener;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.telnet.TelnetConnection;
|
||||
import org.lib3270j.telnet.TelnetFSM;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetConnection;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
@@ -46,12 +46,15 @@ public class Telnet3270Client {
|
||||
this.translator = new EbcdicTranslator();
|
||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||
|
||||
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||
dsProcessor.setOutputSender(fsm::send3270Data);
|
||||
dsProcessor.setInputProcessor(inputProcessor);
|
||||
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
||||
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +125,16 @@ public class Telnet3270Client {
|
||||
/** Get the connection config. */
|
||||
public ConnectionConfig getConfig() { return config; }
|
||||
|
||||
/** Set a custom or interactive TLS certificate verifier callback. */
|
||||
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
|
||||
config.setCertificateVerifier(verifier);
|
||||
}
|
||||
|
||||
/** Get the active SSLSession if connected over TLS, or null. */
|
||||
public javax.net.ssl.SSLSession getSslSession() {
|
||||
return connection.getSslSession();
|
||||
}
|
||||
|
||||
// ========== Convenience input methods ==========
|
||||
|
||||
/** Type a character at the cursor position. */
|
||||
@@ -141,7 +154,7 @@ public class Telnet3270Client {
|
||||
|
||||
/** Send Enter key. */
|
||||
public void sendEnter() {
|
||||
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
}
|
||||
|
||||
/** Send a PF key (1-24). */
|
||||
@@ -179,7 +192,7 @@ public class Telnet3270Client {
|
||||
|
||||
/** Send Clear key. */
|
||||
public void sendClear() {
|
||||
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||
}
|
||||
|
||||
/** Move cursor up. */
|
||||
@@ -210,4 +223,16 @@ public class Telnet3270Client {
|
||||
public void sysReq() { inputProcessor.sysReq(); }
|
||||
/** Reset (unlock keyboard). */
|
||||
public void reset() { inputProcessor.reset(); }
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||
return dsProcessor.getProgramSymbolManager();
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||
return dsProcessor.getGraphicsPlane();
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return dsProcessor.getGocaDecoder();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package org.lib3270j;
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Terminal model definitions for IBM 3278 and 3279 terminals.
|
||||
+15
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.charset;
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* EBCDIC ↔ Unicode translator.
|
||||
@@ -85,6 +85,20 @@ public class EbcdicTranslator {
|
||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper to translate EBCDIC byte to Unicode character.
|
||||
*/
|
||||
public static char toUnicode(int ebc) {
|
||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper to translate EBCDIC byte to ASCII character.
|
||||
*/
|
||||
public static char ebcdicToAscii(int ebc) {
|
||||
return toUnicode(ebc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate Unicode character to EBCDIC byte.
|
||||
* Returns -1 if the character cannot be mapped.
|
||||
+277
-97
@@ -1,13 +1,15 @@
|
||||
package org.lib3270j.datastream;
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import org.lib3270j.listener.ScreenUpdateListener;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
@@ -32,10 +34,17 @@ public class DataStreamProcessor {
|
||||
private OutputSender outputSender;
|
||||
|
||||
// File Transfer DFT handler
|
||||
private org.lib3270j.ft.FTDft ftDft;
|
||||
private haus.nightmare.lib3270j.ft.FTDft ftDft;
|
||||
|
||||
// Input processor reference to manage keyboard locking state
|
||||
private org.lib3270j.input.InputProcessor inputProcessor;
|
||||
private haus.nightmare.lib3270j.input.InputProcessor inputProcessor;
|
||||
|
||||
// Graphics & Programmed Symbols
|
||||
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
|
||||
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
||||
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||
private int currentGocaSubtype = 0;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
@@ -49,20 +58,42 @@ public class DataStreamProcessor {
|
||||
this.qrBuilder = new QueryReplyBuilder(screen);
|
||||
this.outputBuffer = new byte[32768];
|
||||
this.outputPos = 0;
|
||||
this.gocaDecoder.setProgramSymbolManager(programSymbolManager);
|
||||
this.graphicsPlane.setProgramSymbolManager(programSymbolManager);
|
||||
}
|
||||
|
||||
public QueryReplyBuilder getQueryReplyBuilder() {
|
||||
return qrBuilder;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||
return programSymbolManager;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||
return graphicsPlane;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return gocaDecoder;
|
||||
}
|
||||
|
||||
public void setOutputSender(OutputSender sender) {
|
||||
this.outputSender = sender;
|
||||
}
|
||||
|
||||
public void setFTDft(org.lib3270j.ft.FTDft ftDft) {
|
||||
public void setFTDft(haus.nightmare.lib3270j.ft.FTDft ftDft) {
|
||||
this.ftDft = ftDft;
|
||||
}
|
||||
|
||||
public void setInputProcessor(org.lib3270j.input.InputProcessor inputProcessor) {
|
||||
public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) {
|
||||
this.inputProcessor = inputProcessor;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.input.InputProcessor getInputProcessor() {
|
||||
return inputProcessor;
|
||||
}
|
||||
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||
screenListeners.add(l);
|
||||
}
|
||||
@@ -80,72 +111,97 @@ public class DataStreamProcessor {
|
||||
return;
|
||||
|
||||
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) {
|
||||
case CMD_W:
|
||||
case SNA_CMD_W:
|
||||
processWrite(data, offset, length, false);
|
||||
break;
|
||||
case CMD_EW:
|
||||
case SNA_CMD_EW:
|
||||
log.info(">>> ERASE/WRITE: clearing screen (default size)"); {
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(false);
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
switch (cmd) {
|
||||
case CMD_W:
|
||||
case SNA_CMD_W:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processWrite(data, offset, length, false);
|
||||
break;
|
||||
case CMD_EW:
|
||||
case SNA_CMD_EW:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine(">>> ERASE/WRITE: clearing screen (default size)");
|
||||
}
|
||||
{
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(false);
|
||||
graphicsPlane.clear();
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_EWA:
|
||||
case SNA_CMD_EWA:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)");
|
||||
}
|
||||
{
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(true);
|
||||
graphicsPlane.clear();
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_RB:
|
||||
case SNA_CMD_RB:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadBuffer();
|
||||
break;
|
||||
case CMD_RM:
|
||||
case SNA_CMD_RM:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadModified(false);
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadModified(true);
|
||||
break;
|
||||
case CMD_EAU:
|
||||
case SNA_CMD_EAU:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
processWriteStructuredField(data, offset, length);
|
||||
keyboardRestore = true;
|
||||
break;
|
||||
case CMD_NOP:
|
||||
log.info(">>> NOP command");
|
||||
break;
|
||||
default:
|
||||
log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case CMD_EWA:
|
||||
case SNA_CMD_EWA:
|
||||
log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); {
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(true);
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_RB:
|
||||
case SNA_CMD_RB:
|
||||
processReadBuffer();
|
||||
break;
|
||||
case CMD_RM:
|
||||
case SNA_CMD_RM:
|
||||
processReadModified(false);
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
processReadModified(true);
|
||||
break;
|
||||
case CMD_EAU:
|
||||
case SNA_CMD_EAU:
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
processWriteStructuredField(data, offset, length);
|
||||
break;
|
||||
case CMD_NOP:
|
||||
log.info(">>> NOP command");
|
||||
break;
|
||||
default:
|
||||
log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd));
|
||||
break;
|
||||
|
||||
// Translate EBCDIC to Unicode for display
|
||||
screen.translateToUnicode();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
// Translate EBCDIC to Unicode for display
|
||||
screen.translateToUnicode();
|
||||
screen.markAllChanged();
|
||||
if (keyboardRestore && inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
|
||||
// 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 c = screen.getCols();
|
||||
StringBuilder dump = new StringBuilder();
|
||||
@@ -158,7 +214,7 @@ public class DataStreamProcessor {
|
||||
ExtendedAttribute ea = screen.getCell(addr);
|
||||
byte fa = screen.getFieldAttributeAt(addr);
|
||||
|
||||
if ((fa & org.lib3270j.protocol.DS3270Constants.FA_MASK) == org.lib3270j.protocol.DS3270Constants.FA_INT_ZERO_NSEL) {
|
||||
if ((fa & haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MASK) == haus.nightmare.lib3270j.protocol.DS3270Constants.FA_INT_ZERO_NSEL) {
|
||||
if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
|
||||
line.append('*');
|
||||
hasContent = true;
|
||||
@@ -178,7 +234,7 @@ public class DataStreamProcessor {
|
||||
}
|
||||
}
|
||||
if (dump.length() > 0) {
|
||||
log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
|
||||
log.fine("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,8 +507,8 @@ public class DataStreamProcessor {
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// Fill from current position to target
|
||||
while (baddr != toAddr) {
|
||||
// Fill from current position to target (do-while wraps entire buffer if baddr == toAddr)
|
||||
do {
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.fa = 0; // Destroy previous field attribute if any
|
||||
ea.ec = (byte) fillChar;
|
||||
@@ -461,7 +517,7 @@ public class DataStreamProcessor {
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.cs = fillCs;
|
||||
baddr = (baddr + 1) % size;
|
||||
}
|
||||
} while (baddr != toAddr);
|
||||
screen.setBufferAddress(baddr);
|
||||
lastWasOrder = true;
|
||||
break;
|
||||
@@ -476,7 +532,7 @@ public class DataStreamProcessor {
|
||||
if (toAddr >= size)
|
||||
toAddr = toAddr % size;
|
||||
|
||||
while (baddr != toAddr) {
|
||||
do {
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
if (!ea.isFieldAttribute()) {
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
@@ -491,7 +547,7 @@ public class DataStreamProcessor {
|
||||
}
|
||||
}
|
||||
baddr = (baddr + 1) % size;
|
||||
}
|
||||
} while (baddr != toAddr);
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 3;
|
||||
lastWasOrder = true;
|
||||
@@ -607,8 +663,15 @@ public class DataStreamProcessor {
|
||||
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
||||
? inputProcessor.getLastAid()
|
||||
: AID_NO;
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
}
|
||||
|
||||
// AID byte
|
||||
outputWrite(AID_NO);
|
||||
outputWrite(aid);
|
||||
// Cursor address
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
outputWrite(caddr[0] & 0xFF);
|
||||
@@ -637,7 +700,13 @@ public class DataStreamProcessor {
|
||||
|
||||
outputPos = 0;
|
||||
|
||||
int aid = AID_NO; // Last AID
|
||||
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
|
||||
? inputProcessor.getLastAid()
|
||||
: AID_NO;
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setLastAid(AID_NO);
|
||||
}
|
||||
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
// AID byte
|
||||
@@ -649,28 +718,33 @@ public class DataStreamProcessor {
|
||||
outputWrite(caddr[1] & 0xFF);
|
||||
|
||||
if (!screen.isFormatted()) {
|
||||
// Unformatted: send everything
|
||||
if (all) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
outputWrite(screen.getCell(i).ec & 0xFF);
|
||||
// Unformatted screen: send all non-null characters (null suppression)
|
||||
for (int i = 0; i < size; i++) {
|
||||
int b = screen.getCell(i).ec & 0xFF;
|
||||
if (b != 0x00) {
|
||||
outputWrite(b);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Formatted: send modified fields
|
||||
// Formatted screen: send modified fields with null suppression per 3270 spec
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
||||
int fieldStart = (i + 1) % size;
|
||||
// Send SBA for field start
|
||||
|
||||
// Always send SBA and address of first character position in field
|
||||
outputWrite(ORDER_SBA);
|
||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||
outputWrite(addr[0] & 0xFF);
|
||||
outputWrite(addr[1] & 0xFF);
|
||||
|
||||
// Send field contents until next FA
|
||||
// Send all non-null characters in field (suppressing 0x00)
|
||||
int pos = fieldStart;
|
||||
while (pos < size && !screen.getCell(pos).isFieldAttribute()) {
|
||||
outputWrite(screen.getCell(pos).ec & 0xFF);
|
||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||
int b = screen.getCell(pos).ec & 0xFF;
|
||||
if (b != 0x00) {
|
||||
outputWrite(b);
|
||||
}
|
||||
pos = (pos + 1) % size;
|
||||
if (pos == fieldStart)
|
||||
break;
|
||||
@@ -705,20 +779,27 @@ public class DataStreamProcessor {
|
||||
case SF_READ_PART:
|
||||
processSFReadPartition(data, pos, fieldLen);
|
||||
break;
|
||||
case SF_ERASE_RESET:
|
||||
if (fieldLen >= 4) {
|
||||
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT;
|
||||
screen.erase(alt);
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
case SF_ERASE_RESET: {
|
||||
boolean alt = (fieldLen >= 4) && ((data[pos + 3] & 0xFF) == SF_ER_ALT);
|
||||
screen.erase(alt);
|
||||
graphicsPlane.clear();
|
||||
gocaDecoder.resetDefaults();
|
||||
notifyScreenSizeChanged();
|
||||
break;
|
||||
}
|
||||
case SF_SET_REPLY_MODE:
|
||||
if (fieldLen >= 5) {
|
||||
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
|
||||
}
|
||||
break;
|
||||
case SF_CREATE_PART:
|
||||
// Acknowledged — we use implicit partition
|
||||
if (fieldLen >= 4) {
|
||||
int pid = data[pos + 3] & 0xFF;
|
||||
screen.setActivePartition(pid);
|
||||
log.fine("Created active partition ID=" + pid);
|
||||
}
|
||||
graphicsPlane.clear();
|
||||
gocaDecoder.resetDefaults();
|
||||
break;
|
||||
case SF_OUTBOUND_DS:
|
||||
if (fieldLen > 5) {
|
||||
@@ -733,6 +814,97 @@ public class DataStreamProcessor {
|
||||
log.fine("SF_TRANSFER_DATA received but ftDft is null");
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_DIRECT: // 0x06: Load Programmed Symbols direct
|
||||
if (fieldLen > 3) {
|
||||
byte[] psData = new byte[fieldLen - 3];
|
||||
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
||||
programSymbolManager.loadps(psData);
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
||||
if (fieldLen >= 4) {
|
||||
int sfSubId = data[pos + 3] & 0xFF;
|
||||
switch (sfSubId) {
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
||||
if (fieldLen > 4) {
|
||||
byte[] psData = new byte[fieldLen - 4];
|
||||
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
||||
programSymbolManager.loadps(psData);
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: // 0x10: Object Picture (Picture segments)
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Data (GOCA draw orders)
|
||||
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
||||
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
||||
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
|
||||
|
||||
StringBuilder hexDump = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(32, fieldLen); i++) {
|
||||
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
|
||||
}
|
||||
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
|
||||
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
|
||||
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
|
||||
if (flags == 0x80) { // SPAN_FIRST
|
||||
gocaAccumulator.reset();
|
||||
currentGocaSubtype = sfSubId;
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
} else if (flags == 0x00) { // SPAN_MIDDLE
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
} else if (flags == 0x40) { // SPAN_LAST
|
||||
if (orderLen > 0) {
|
||||
gocaAccumulator.write(data, orderOffset, orderLen);
|
||||
}
|
||||
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||
gocaAccumulator.reset();
|
||||
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
|
||||
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||
} else {
|
||||
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
||||
}
|
||||
notifyScreenUpdated();
|
||||
} else { // SPAN_ONLY (0xC0)
|
||||
gocaAccumulator.reset();
|
||||
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
|
||||
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||
} else {
|
||||
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
||||
}
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
log.fine("Unknown SF 0x0F subtype: " + String.format("0x%02x", sfSubId));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
|
||||
if (fieldLen > 3) {
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
|
||||
if (fieldLen > 3) {
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
|
||||
break;
|
||||
@@ -761,6 +933,8 @@ public class DataStreamProcessor {
|
||||
switch (type) {
|
||||
case SF_RP_QUERY:
|
||||
log.info("ReadPartition Query — sending all query replies");
|
||||
graphicsPlane.clear();
|
||||
gocaDecoder.resetDefaults();
|
||||
sendAllQueryReplies();
|
||||
break;
|
||||
case SF_RP_QLIST:
|
||||
@@ -809,7 +983,7 @@ public class DataStreamProcessor {
|
||||
if ((i + 1) % 32 == 0)
|
||||
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) {
|
||||
outputSender.send3270Data(qr);
|
||||
}
|
||||
@@ -824,7 +998,7 @@ public class DataStreamProcessor {
|
||||
if ((i + 1) % 32 == 0)
|
||||
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) {
|
||||
outputSender.send3270Data(qr);
|
||||
}
|
||||
@@ -855,4 +1029,10 @@ public class DataStreamProcessor {
|
||||
l.onScreenSizeChanged(screen.getRows(), screen.getCols());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyScreenUpdated() {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
|
||||
*/
|
||||
public class QueryReplyBuilder {
|
||||
|
||||
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
|
||||
|
||||
// Canned values from 3279-2 (matching sf.c)
|
||||
private static final int SW_3279_2 = 0x09;
|
||||
private static final int SH_3279_2 = 0x0c;
|
||||
private static final int Xr_3279_2 = 0x000a02e5;
|
||||
private static final int Yr_3279_2 = 0x0002006f;
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
|
||||
|
||||
// Base query reply codes (text mode)
|
||||
private static final int[] SUPPORTED_QR_BASE = {
|
||||
QR_SUMMARY, // 0x80 — summary must list itself
|
||||
QR_USABLE_AREA, // 0x81
|
||||
QR_ALPHA_PART, // 0x84
|
||||
QR_CHARSETS, // 0x85
|
||||
QR_COLOR, // 0x86
|
||||
QR_HIGHLIGHTING, // 0x87
|
||||
QR_REPLY_MODES, // 0x88
|
||||
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
|
||||
QR_IMP_PART, // 0xa6
|
||||
};
|
||||
|
||||
// Vector graphics query reply codes matching HOD DS3270.java line 1723
|
||||
private static final int[] SUPPORTED_QR_VECTOR = {
|
||||
QR_SUMMARY, // 0x80
|
||||
QR_USABLE_AREA, // 0x81
|
||||
QR_ALPHA_PART, // 0x84
|
||||
QR_CHARSETS, // 0x85
|
||||
QR_COLOR, // 0x86
|
||||
QR_HIGHLIGHTING, // 0x87
|
||||
QR_REPLY_MODES, // 0x88
|
||||
QR_SAVE_RESTORE, // 0x8c
|
||||
QR_DDM, // 0x95
|
||||
QR_TRANSPARENCY, // 0x99
|
||||
QR_IMP_PART, // 0xa6
|
||||
QR_RPQ_NAMES, // 0xa8
|
||||
QR_GRAPHICS, // 0xb0
|
||||
QR_GIMAGE, // 0xb1
|
||||
QR_AUX_DEV, // 0xb2
|
||||
QR_OEM_FMT, // 0xb3
|
||||
QR_GCOLOR, // 0xb4
|
||||
QR_GSYMBOLS, // 0xb6
|
||||
};
|
||||
|
||||
public QueryReplyBuilder(ScreenBuffer screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
public QueryReplyBuilder(ScreenBuffer screen, GraphicsMode graphicsMode) {
|
||||
this.screen = screen;
|
||||
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
public GraphicsMode getGraphicsMode() {
|
||||
return graphicsMode;
|
||||
}
|
||||
|
||||
public void setGraphicsMode(GraphicsMode graphicsMode) {
|
||||
this.graphicsMode = (graphicsMode != null) ? graphicsMode : GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build all query replies as a single AID_SF + structured field response.
|
||||
*/
|
||||
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
||||
|
||||
// AID byte for structured field
|
||||
out.write(AID_SF);
|
||||
|
||||
// Summary
|
||||
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
||||
|
||||
// Usable Area
|
||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||
|
||||
// Alpha Partitions
|
||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||
|
||||
// Character Sets
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
|
||||
// Color
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
|
||||
// Highlighting
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
|
||||
// Reply Modes (0x88)
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
// Save/Restore (0x8C)
|
||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||
}
|
||||
|
||||
// Distributed Data Management (0x95)
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
// Transparency (0x99)
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||
}
|
||||
|
||||
// Implicit Partition (0xA6)
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
// Vector Graphics QRs if enabled
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1
|
||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
|
||||
appendOemFmt(out); // 0xB3
|
||||
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
|
||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6
|
||||
}
|
||||
|
||||
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
|
||||
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
|
||||
* matching x3270 sf.c behavior.
|
||||
*/
|
||||
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
|
||||
if (requestedCodes == null || requestedCodes.length == 0) {
|
||||
return buildAllQueryReplies(maxCols, maxRows, bufferSize);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
||||
out.write(AID_SF);
|
||||
|
||||
for (byte codeByte : requestedCodes) {
|
||||
int code = codeByte & 0xFF;
|
||||
switch (code) {
|
||||
case QR_SUMMARY:
|
||||
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
||||
break;
|
||||
case QR_USABLE_AREA:
|
||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||
break;
|
||||
case QR_ALPHA_PART:
|
||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||
break;
|
||||
case QR_CHARSETS:
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
break;
|
||||
case QR_COLOR:
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
break;
|
||||
case QR_HIGHLIGHTING:
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
break;
|
||||
case QR_REPLY_MODES:
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
break;
|
||||
case QR_SAVE_RESTORE:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_DDM:
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
break;
|
||||
case QR_TRANSPARENCY:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_IMP_PART:
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
break;
|
||||
case QR_RPQ_NAMES:
|
||||
case QR_RPQNAMES:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, code, buildRpqNames());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GRAPHICS:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows));
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GIMAGE:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows));
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_AUX_DEV:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_OEM_FMT:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendOemFmt(out);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GCOLOR:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GCOLOR, buildGColor());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GSYMBOLS:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unsupported query reply code — emit QR_NULL
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Built " + out.size() + " bytes of requested query replies for " + requestedCodes.length + " codes");
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private void appendQueryReply(ByteArrayOutputStream out, int code, byte[] data) {
|
||||
// Length includes the 2-byte length field + SFID_QREPLY + code + data
|
||||
int len = 4 + data.length;
|
||||
out.write((len >> 8) & 0xFF);
|
||||
out.write(len & 0xFF);
|
||||
out.write(SFID_QREPLY);
|
||||
out.write(code);
|
||||
out.write(data, 0, data.length);
|
||||
}
|
||||
|
||||
private byte[] buildSummary() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
|
||||
for (int code : codes) {
|
||||
out.write(code);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
|
||||
out.write(0x01); // 12/14-bit addressing
|
||||
out.write(0x00); // no special character features
|
||||
out.write((maxCols >> 8) & 0xFF); // usable width high
|
||||
out.write(maxCols & 0xFF); // usable width low
|
||||
out.write((maxRows >> 8) & 0xFF); // usable height high
|
||||
out.write(maxRows & 0xFF); // usable height low
|
||||
out.write(0x01); // units (mm)
|
||||
// Xr (4 bytes) - canned from 3279-2
|
||||
out.write((Xr_3279_2 >> 24) & 0xFF);
|
||||
out.write((Xr_3279_2 >> 16) & 0xFF);
|
||||
out.write((Xr_3279_2 >> 8) & 0xFF);
|
||||
out.write(Xr_3279_2 & 0xFF);
|
||||
// Yr (4 bytes) - canned from 3279-2
|
||||
out.write((Yr_3279_2 >> 24) & 0xFF);
|
||||
out.write((Yr_3279_2 >> 16) & 0xFF);
|
||||
out.write((Yr_3279_2 >> 8) & 0xFF);
|
||||
out.write(Yr_3279_2 & 0xFF);
|
||||
out.write(SW_3279_2); // AW
|
||||
out.write(SH_3279_2); // AH
|
||||
int buf = maxCols * maxRows;
|
||||
out.write((buf >> 8) & 0xFF); // buffer size high
|
||||
out.write(buf & 0xFF); // buffer size low
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildAlphaPartitions(int maxRows) {
|
||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||
out.write(0x00); // max partitions (1 byte: 0x00 = 1 partition)
|
||||
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
||||
out.write(bufSize & 0xFF); // total partition storage low
|
||||
out.write(0x00); // flags
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildCharsets() {
|
||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW (9)
|
||||
out.write(SH_3279_2); // SDH (12)
|
||||
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 (low)
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x07); // DL = 7 bytes per descriptor
|
||||
// Descriptor 1 (SET 0): default character set (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);
|
||||
// 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);
|
||||
// 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(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) - Flags = 0xC0 (0x80 Loadable | 0x40 Triple-plane)
|
||||
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(0xc0); out.write(0x43); 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(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();
|
||||
}
|
||||
|
||||
private byte[] buildColor() {
|
||||
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
|
||||
return new byte[] {
|
||||
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||
(byte) 0xF1, (byte) 0xF1, // Blue
|
||||
(byte) 0xF2, (byte) 0xF2, // Red
|
||||
(byte) 0xF3, (byte) 0xF3, // Pink
|
||||
(byte) 0xF4, (byte) 0xF4, // Green
|
||||
(byte) 0xF5, (byte) 0xF5, // Turquoise
|
||||
(byte) 0xF6, (byte) 0xF6, // Yellow
|
||||
(byte) 0xF7, (byte) 0xF7 // Neutral/White
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildHighlighting() {
|
||||
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
|
||||
return new byte[] {
|
||||
0x04, 0x00, (byte) 0xF0,
|
||||
(byte) 0xF1, (byte) 0xF1,
|
||||
(byte) 0xF2, (byte) 0xF2,
|
||||
(byte) 0xF4, (byte) 0xF4
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildReplyModes() {
|
||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||
}
|
||||
|
||||
private byte[] buildDdm(int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x00); // reserved
|
||||
out.write((bufferSize >> 8) & 0xFF); // inbound length limit (INLIM)
|
||||
out.write(bufferSize & 0xFF);
|
||||
out.write((bufferSize >> 8) & 0xFF); // outbound length limit (OUTLIM)
|
||||
out.write(bufferSize & 0xFF);
|
||||
out.write(0x01); // NSS = 01
|
||||
out.write(0x01); // DDMSS = 01
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||
// Implicit partition sizes, 2 self-defining parameters
|
||||
|
||||
// SDP 1: Default screen size (Model 2: 80x24)
|
||||
out.write(0x00); // flags
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x0b); // SDP length (11 bytes)
|
||||
out.write(0x01); // SDP type: implicit partition sizes
|
||||
out.write(0x00); // reserved
|
||||
// Default size
|
||||
out.write((MODEL_2_COLS >> 8) & 0xFF);
|
||||
out.write(MODEL_2_COLS & 0xFF);
|
||||
out.write((MODEL_2_ROWS >> 8) & 0xFF);
|
||||
out.write(MODEL_2_ROWS & 0xFF);
|
||||
// Alternate size (Model 4: 80x43, Model 5: 132x27, etc.)
|
||||
out.write((maxCols >> 8) & 0xFF);
|
||||
out.write(maxCols & 0xFF);
|
||||
out.write((maxRows >> 8) & 0xFF);
|
||||
out.write(maxRows & 0xFF);
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildGraphics(int maxCols, int maxRows) {
|
||||
return new byte[]{
|
||||
(byte) 0x80, 0x02,
|
||||
0x00, 0x00,
|
||||
0x00, (byte) 0xFC,
|
||||
0x00
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildGImage(int maxCols, int maxRows) {
|
||||
return new byte[]{
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, (byte) 0xFC,
|
||||
0x00,
|
||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildAuxDev() {
|
||||
return new byte[]{
|
||||
0x00, 0x09, 0x00, 0x07,
|
||||
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildSaveRestore() {
|
||||
// HOD DS3270.java line 1768: 6 bytes payload
|
||||
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
}
|
||||
|
||||
private byte[] buildTransparency() {
|
||||
// HOD DS3270.java line 1782: 2 bytes payload
|
||||
return new byte[]{ 0x00, 0x00 };
|
||||
}
|
||||
|
||||
private byte[] buildRpqNames() {
|
||||
// HOD DS3270.java line 1798: 5 bytes payload
|
||||
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||
}
|
||||
|
||||
private void appendOemFmt(ByteArrayOutputStream out) {
|
||||
// HOD DS3270.java line 1814: 4 distinct OEM format sub-fields
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_OEM_FMT, new byte[]{
|
||||
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] buildGColor() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
||||
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
|
||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
out.write(0x00);
|
||||
out.write(i);
|
||||
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||
int r = (argb >> 16) & 0xFF;
|
||||
int g = (argb >> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
out.write(r);
|
||||
out.write(g);
|
||||
out.write(b);
|
||||
out.write(0x00); // 6th byte in HOD color table
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildGSymbols() {
|
||||
return new byte[]{
|
||||
0x00, 0x00, 0x0C, 0x18, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
|
||||
0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.ft;
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
/**
|
||||
* Configuration for an IND$FILE file transfer session.
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.ft;
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
/**
|
||||
* Constants for IND$FILE file transfer protocol.
|
||||
@@ -183,7 +183,7 @@ public final class FTConstants {
|
||||
* Decode a CUT-mode base-64 encoded integer from EBCDIC.
|
||||
* Converts a table6-encoded EBCDIC character to its 6-bit value.
|
||||
*/
|
||||
public static int from6(int ebcdicByte, org.lib3270j.charset.EbcdicTranslator translator) {
|
||||
public static int from6(int ebcdicByte, haus.nightmare.lib3270j.charset.EbcdicTranslator translator) {
|
||||
char ascii = translator.ebcdicToUnicode(ebcdicByte & 0xFF);
|
||||
int idx = TABLE6.indexOf(ascii);
|
||||
return idx >= 0 ? idx : 0;
|
||||
@@ -199,7 +199,7 @@ public final class FTConstants {
|
||||
/**
|
||||
* Encode a 6-bit value into a table6-encoded EBCDIC character.
|
||||
*/
|
||||
public static int to6(int value, org.lib3270j.charset.EbcdicTranslator translator) {
|
||||
public static int to6(int value, haus.nightmare.lib3270j.charset.EbcdicTranslator translator) {
|
||||
char ascii = TABLE6.charAt(value & 0x3F);
|
||||
int ebc = translator.unicodeToEbcdic(ascii);
|
||||
return ebc >= 0 ? ebc : ASC2FT[ascii & 0xFF];
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
package org.lib3270j.ft;
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.input.InputProcessor;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import static org.lib3270j.ft.FTConstants.*;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import static haus.nightmare.lib3270j.ft.FTConstants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
package org.lib3270j.ft;
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import org.lib3270j.input.InputProcessor;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import static org.lib3270j.ft.FTConstants.*;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import static haus.nightmare.lib3270j.ft.FTConstants.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -0,0 +1,195 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Constants for IBM 3179G / 3270-PC GOCA (Graphics Object Content Architecture)
|
||||
* vector graphics and Programmed Symbols (PS).
|
||||
*/
|
||||
public final class GocaConstants {
|
||||
|
||||
private GocaConstants() {}
|
||||
|
||||
// Structured Field IDs
|
||||
public static final int SF_LOADPS = 0x0F; // 2-byte Structured Field prefix (or 0x06 for direct Load PS)
|
||||
public static final int SF_LOADPS_DIRECT = 0x06; // Load Programmed Symbols direct SFID
|
||||
public static final int SF_OBJCNTL = 0x24; // Object Control
|
||||
public static final int SF_OBJDATA = 0x85; // Graphics Object Data
|
||||
public static final int SF_3270_G = 0x20; // 3270 Graphics / Picture
|
||||
|
||||
// Structured Field Sub-IDs (for SF 0x0F)
|
||||
public static final int SF_LOADPS_SUB = 0x06; // Load Programmed Symbols
|
||||
public static final int SF_LOADLT_SUB = 0x07; // Load Line Type / Symbol Set
|
||||
public static final int SF_OBJDATA_SUB = 0x0F; // Graphics Object Data (GOCA draw orders)
|
||||
public static final int SF_OBJPICT_SUB = 0x10; // Graphics Object Picture (Segment draw orders)
|
||||
public static final int SF_OBJCNTL_SUB = 0x11; // Graphics Object Control (Procedure orders)
|
||||
|
||||
// Procedure Orders (for SF_OBJCNTL_SUB 0x11)
|
||||
public static final int P_NOP1 = 0x00; // Procedure NOOP
|
||||
public static final int P_COMT = 0x01; // Procedure Comment
|
||||
public static final int P_ATTCUR = 0x08; // Attach Graphic Cursor
|
||||
public static final int P_DETCUR = 0x09; // Detach Graphic Cursor
|
||||
public static final int P_ERASE = 0x0A; // Erase Graphics Presentation Space
|
||||
public static final int P_STOPDR = 0x0F; // Stop Draw
|
||||
public static final int P_SCUDEF = 0x21; // Set Current Defaults
|
||||
public static final int P_BEGPROC = 0x30; // Begin Procedure
|
||||
public static final int P_SETCUR = 0x31; // Set Graphic Cursor Position
|
||||
|
||||
// Coordinate space
|
||||
public static final int VIRTUAL_COORD_MAX = 4096;
|
||||
|
||||
// GOCA Drawing / Segment Orders
|
||||
public static final int G_NOP1 = 0x00; // NOOP 1-byte
|
||||
public static final int G_COMT = 0x01; // Comment
|
||||
public static final int G_GSMC = 0x07; // Set Marker Color
|
||||
public static final int G_GSPS = 0x08; // Set Pattern Set
|
||||
public static final int G_GSCOL = 0x0A; // Set Color
|
||||
public static final int G_GSMX = 0x0C; // Set Foreground Mix
|
||||
public static final int G_GSBMX = 0x0D; // Set Background Mix
|
||||
public static final int G_GSFLW = 0x11; // Set Fractional Line Width
|
||||
public static final int G_GSLT = 0x18; // Set Line Type
|
||||
public static final int G_GSLW = 0x19; // Set Line Width
|
||||
public static final int G_GSMS = 0x1B; // Set Marker Size
|
||||
public static final int G_GSCP = 0x21; // Set Current Position
|
||||
public static final int G_GSAP = 0x22; // Arc Parameters
|
||||
public static final int G_GSECOL = 0x26; // Set Extended Color
|
||||
public static final int G_GSVW = 0x27; // Set Viewing Window
|
||||
public static final int G_GSPT = 0x28; // Set Pattern Symbol
|
||||
public static final int G_GSMT = 0x29; // Set Marker Symbol / Type
|
||||
public static final int G_GCALL = 0x2A; // Call Segment
|
||||
public static final int G_GSCH = 0x33; // Set Character Cell
|
||||
public static final int G_GSCA = 0x34; // Set Character Angle
|
||||
public static final int G_GSCR = 0x35; // Set Character Shear
|
||||
public static final int G_GSMCEL = 0x37; // Set Marker Cell
|
||||
public static final int G_GSCS = 0x38; // Set Character Set
|
||||
public static final int G_GSMP = 0x39; // Set Marker Precision
|
||||
public static final int G_GSETAG = 0x39; // Set Pick Identifier / Tag
|
||||
public static final int G_GSCD = 0x3A; // Set Character Direction
|
||||
public static final int G_GSCC = 0x3B; // Set Character Precision
|
||||
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
|
||||
public static final int G_ENDPROLOGUE = 0x3E; // End Prologue
|
||||
public static final int G_GPOP = 0x3F; // Pop Attribute
|
||||
public static final int G_GEAR = 0x60; // End Area
|
||||
public static final int G_GBAR = 0x68; // Begin Area
|
||||
public static final int G_BEGSEGM = 0x70; // Begin Segment
|
||||
public static final int G_ENDSEGM = 0x71; // End Segment
|
||||
public static final int G_GERASE = 0x7E; // Erase Graphics Plane
|
||||
public static final int G_GCLINE = 0x81; // Line at Current Position
|
||||
public static final int G_GCMRK = 0x82; // Marker at Current Position
|
||||
public static final int G_GCCHST = 0x83; // Character String at Current Position
|
||||
public static final int G_GCFLT = 0x85; // Fillet at Current Position
|
||||
public static final int G_GCARC = 0x86; // Partial Arc at Current Position
|
||||
public static final int G_GCFARC = 0x87; // Full Arc at Current Position
|
||||
public static final int G_GEIMG = 0x91; // End Image
|
||||
public static final int G_GIMD = 0x92; // Image Data
|
||||
public static final int G_GCRLIN = 0xA1; // Relative Line at Current Position
|
||||
public static final int G_GLINE = 0xC1; // Line (Absolute)
|
||||
public static final int G_GMRK = 0xC2; // Marker (Absolute)
|
||||
public static final int G_GCHST = 0xC3; // Character String (Absolute)
|
||||
public static final int G_GFLT = 0xC5; // Fillet (Absolute)
|
||||
public static final int G_GARC = 0xC6; // Partial Arc (Absolute)
|
||||
public static final int G_GFARC = 0xC7; // Full Arc (Absolute)
|
||||
public static final int G_GBIMG = 0xD1; // Begin Image
|
||||
public static final int G_GRLINE = 0xE1; // Relative Line (Absolute Start)
|
||||
|
||||
// Line Types (GSLT)
|
||||
public static final int LT_DEFAULT = 0;
|
||||
public static final int LT_DOT = 1;
|
||||
public static final int LT_SHORTDASH= 2;
|
||||
public static final int LT_DASHDOT = 3;
|
||||
public static final int LT_DOUBLEDOT= 4;
|
||||
public static final int LT_LONGDASH = 5;
|
||||
public static final int LT_DASHDOUBLEDOT = 6;
|
||||
public static final int LT_SOLID = 7;
|
||||
|
||||
// Line Widths (GSLW)
|
||||
public static final int LW_DEFAULT = 0;
|
||||
public static final int LW_NORMAL = 1;
|
||||
public static final int LW_THICK = 2;
|
||||
|
||||
// Character Precision (G_GSCC / 0x3B)
|
||||
public static final int CP_DEFAULT = 0;
|
||||
public static final int CP_STRING = 1;
|
||||
public static final int CP_CHAR = 2;
|
||||
public static final int CP_STROKE = 3;
|
||||
|
||||
// Fill Patterns (GSPT)
|
||||
public static final int PT_DEFAULT = 0;
|
||||
public static final int PT_D1 = 1;
|
||||
public static final int PT_D2 = 2;
|
||||
public static final int PT_D3 = 3;
|
||||
public static final int PT_D4 = 4;
|
||||
public static final int PT_D5 = 5;
|
||||
public static final int PT_D6 = 6;
|
||||
public static final int PT_D7 = 7;
|
||||
public static final int PT_D8 = 8;
|
||||
public static final int PT_VERT_LINE = 9;
|
||||
public static final int PT_HORIZ_LINE = 10;
|
||||
public static final int PT_DIAG_BLTR = 11;
|
||||
public static final int PT_DIAG_BLTR2 = 12;
|
||||
public static final int PT_DIAG_TLBR = 13;
|
||||
public static final int PT_DIAG_TLBR2 = 14;
|
||||
public static final int PT_EMPTY = 15;
|
||||
public static final int PT_SOLID = 16;
|
||||
|
||||
// Marker Symbols (GSMT)
|
||||
public static final int MK_DEFAULT = 0;
|
||||
public static final int MK_CROSS = 1; // x
|
||||
public static final int MK_PLUS = 2; // +
|
||||
public static final int MK_DIAMOND = 3; // <>
|
||||
public static final int MK_SQUARE = 4; // []
|
||||
public static final int MK_6STAR = 5; // * 6-point
|
||||
public static final int MK_8STAR = 6; // * 8-point
|
||||
public static final int MK_SDIAMOND = 7; // solid diamond
|
||||
public static final int MK_SSQUARE = 8; // solid square
|
||||
public static final int MK_DOT = 9; // .
|
||||
public static final int MK_CIRCLE = 10;// o
|
||||
|
||||
// Character Direction (GSCD)
|
||||
public static final int CD_DEFAULT = 0;
|
||||
public static final int CD_LR = 1; // Left to Right
|
||||
public static final int CD_TB = 2; // Top to Bottom
|
||||
public static final int CD_RL = 3; // Right to Left
|
||||
public static final int CD_BT = 4; // Bottom to Top
|
||||
|
||||
// Foreground / Background Mix Modes (GSMX / GSBMX)
|
||||
public static final int MIX_DEFAULT = 0;
|
||||
public static final int MIX_OR = 1;
|
||||
public static final int MIX_OVER = 2;
|
||||
public static final int MIX_LEAVE = 3;
|
||||
public static final int MIX_XOR = 4;
|
||||
public static final int MIX_UNDER = 5;
|
||||
|
||||
// Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
|
||||
public static final int[] GOCA_COLORS = new int[] {
|
||||
0xFF00FF00, // 0: Default (Green)
|
||||
0xFF7890F0, // 1: Blue (120, 144, 240)
|
||||
0xFFFF0000, // 2: Red (255, 0, 0)
|
||||
0xFFFF00FF, // 3: Pink / Magenta (255, 0, 255)
|
||||
0xFF00FF00, // 4: Green (0, 255, 0)
|
||||
0xFF00FFFF, // 5: Turquoise / Cyan (0, 255, 255)
|
||||
0xFFFFFF00, // 6: Yellow (255, 255, 0)
|
||||
0xFFFFFFFF, // 7: Neutral White (255, 255, 255)
|
||||
0xFF000000, // 8: Black (0, 0, 0)
|
||||
0xFF000080, // 9: Deep Blue (0, 0, 128)
|
||||
0xFF800000, // 10: Orange / Dark Red (128, 0, 0)
|
||||
0xFF800080, // 11: Purple (128, 0, 128)
|
||||
0xFF008000, // 12: Pale Green (0, 128, 0)
|
||||
0xFF008080, // 13: Pale Cyan (0, 128, 128)
|
||||
0xFFD79700, // 14: Mustard (215, 151, 0)
|
||||
0xFFC0C0C0, // 15: Grey / Light White (192, 192, 192)
|
||||
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 == 0xFF) {
|
||||
return GOCA_COLORS[7];
|
||||
}
|
||||
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
|
||||
return GOCA_COLORS[colorIndex];
|
||||
}
|
||||
return GOCA_COLORS[0];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Builds the 56-byte IBM 3179G / 3270G Graphic Input Structured Field.
|
||||
* Used for light-pen and graphics cursor interactive input per IBM HOD / GDDM specifications.
|
||||
*/
|
||||
public class GraphicInputBuilder {
|
||||
|
||||
// 56-byte template mask from IBM Host On-Demand (HODInput.java)
|
||||
private static final byte[] MASK = new byte[] {
|
||||
0x00, 0x38, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
|
||||
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field.
|
||||
*
|
||||
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
|
||||
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
|
||||
* @param buttonOrAidCode The mouse button number (1=Pick, 2=Action) or 3270 AID code for keyboard
|
||||
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
|
||||
* @param isShift true if shift key was down
|
||||
* @param isCtrl true if ctrl key was down
|
||||
* @return 56-byte payload
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
||||
return buildGraphicInput(gocaX, gocaY, buttonOrAidCode, isMouseAction, isShift, isCtrl, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
||||
int pickedSegId, int pickTag) {
|
||||
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl, pickedSegId, pickTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field with cursor row/col, picked segment ID and correlation tag.
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
||||
int pickedSegId, int pickTag) {
|
||||
byte[] sf = new byte[MASK.length];
|
||||
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
||||
|
||||
// Byte 0-1: Structured Field Length (56 bytes)
|
||||
sf[0] = (byte) ((MASK.length >> 8) & 0xFF);
|
||||
sf[1] = (byte) (MASK.length & 0xFF);
|
||||
|
||||
// Bytes 16-19: Cursor Row & Column
|
||||
if (row > 0 || col > 0) {
|
||||
sf[16] = (byte) (row & 0xFF);
|
||||
sf[17] = (byte) (col & 0xFF);
|
||||
sf[18] = (byte) (row & 0xFF);
|
||||
sf[19] = (byte) (col & 0xFF);
|
||||
}
|
||||
|
||||
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
|
||||
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
|
||||
sf[25] = (byte) (gocaX & 0xFF);
|
||||
|
||||
// Byte 26-27: GOCA Y coordinate (signed 16-bit big-endian)
|
||||
sf[26] = (byte) ((gocaY >> 8) & 0xFF);
|
||||
sf[27] = (byte) (gocaY & 0xFF);
|
||||
|
||||
if (isMouseAction) {
|
||||
// Byte 28-31: Picked Segment ID (4 bytes big-endian)
|
||||
sf[28] = (byte) ((pickedSegId >> 24) & 0xFF);
|
||||
sf[29] = (byte) ((pickedSegId >> 16) & 0xFF);
|
||||
sf[30] = (byte) ((pickedSegId >> 8) & 0xFF);
|
||||
sf[31] = (byte) (pickedSegId & 0xFF);
|
||||
|
||||
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
|
||||
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
|
||||
sf[33] = (byte) (pickTag & 0xFF);
|
||||
|
||||
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
|
||||
sf[35] = (byte) (buttonOrAidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
|
||||
} else {
|
||||
// Keyboard AID (Enter, PF keys)
|
||||
sf[28] = 0x00;
|
||||
sf[29] = 0x00;
|
||||
sf[30] = 0x00;
|
||||
sf[31] = 0x07;
|
||||
sf[32] = 0x00;
|
||||
sf[33] = 0x07;
|
||||
sf[34] = (byte) 0xFF;
|
||||
sf[35] = (byte) (buttonOrAidCode & 0xFF);
|
||||
}
|
||||
|
||||
return sf;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Graphics modes supported by j3270 / lib3270j.
|
||||
*/
|
||||
public enum GraphicsMode {
|
||||
/** Text only (no graphics query replies, classic 3279-4 terminal behavior). */
|
||||
NONE("None (Text Only)"),
|
||||
|
||||
/** Programmed Symbols only (custom character matrices and APL, single & triple plane). */
|
||||
PROGRAMMED_SYMBOLS("Programmed Symbols Only"),
|
||||
|
||||
/** Vector graphics only (GOCA / 3179G drawing orders). */
|
||||
VECTOR_GRAPHICS("Vector Graphics Only"),
|
||||
|
||||
/** Full graphics support (both Programmed Symbols and Vector Graphics). */
|
||||
BOTH("Both (Programmed Symbols & Vector Graphics)");
|
||||
|
||||
private final String description;
|
||||
|
||||
GraphicsMode(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean isProgrammedSymbolsEnabled() {
|
||||
return this == PROGRAMMED_SYMBOLS || this == BOTH;
|
||||
}
|
||||
|
||||
public boolean isVectorGraphicsEnabled() {
|
||||
return this == VECTOR_GRAPHICS || this == BOTH;
|
||||
}
|
||||
|
||||
public static GraphicsMode fromString(String str) {
|
||||
if (str == null || str.trim().isEmpty()) {
|
||||
return NONE;
|
||||
}
|
||||
String s = str.trim().toUpperCase();
|
||||
switch (s) {
|
||||
case "BOTH":
|
||||
case "ALL":
|
||||
case "FULL":
|
||||
case "ON":
|
||||
case "TRUE":
|
||||
return BOTH;
|
||||
case "PS":
|
||||
case "PROGRAMMED_SYMBOLS":
|
||||
case "PROGRAMMEDSYMBOLS":
|
||||
case "SYMBOLS":
|
||||
case "APL":
|
||||
return PROGRAMMED_SYMBOLS;
|
||||
case "VECTOR":
|
||||
case "VECTOR_GRAPHICS":
|
||||
case "VECTORGRAPHICS":
|
||||
case "GOCA":
|
||||
case "GDDM":
|
||||
return VECTOR_GRAPHICS;
|
||||
case "NONE":
|
||||
case "OFF":
|
||||
case "FALSE":
|
||||
case "DISABLED":
|
||||
case "TEXT":
|
||||
default:
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,901 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Offscreen rendering surface for GOCA vector graphics.
|
||||
* 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 {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName());
|
||||
|
||||
// 17 Standard GOCA 8x8 Fill Patterns
|
||||
public static final byte[][] PATTERN_DATA = new byte[][]{
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1}, // 0: Solid (all 1s)
|
||||
{-1, -1, -1, -18, -1, -1, -1, -18}, // 1: D1
|
||||
{-1, -69, -1, -18, -1, -69, -1, -18}, // 2: D2
|
||||
{119, -35, -69, -18, 119, -35, -69, -18}, // 3: D3
|
||||
{-69, -52, 51, -18, -69, -52, 51, -18}, // 4: D4
|
||||
{85, -86, 85, -86, 85, -86, 85, -86}, // 5: D5 (50% checker)
|
||||
{68, 51, -52, 17, 68, 51, -52, 17}, // 6: D6
|
||||
{-120, 34, 68, 17, -120, 34, 68, 17}, // 7: D7
|
||||
{0, 68, 0, 17, 0, 68, 0, 17}, // 8: D8 (sparse dots)
|
||||
{-128, -128, -128, -128, -128, -128, -128, -128}, // 9: Vertical line
|
||||
{-1, 0, 0, 0, 0, 0, 0, 0}, // 10: Horizontal line
|
||||
{1, 2, 4, 8, 16, 32, 64, -128}, // 11: Diagonal bottom-left to top-right
|
||||
{3, 12, 48, -64, 3, 12, 48, -64}, // 12: Diagonal BL-TR dense
|
||||
{-128, 64, 32, 16, 8, 4, 2, 1}, // 13: Diagonal top-left to bottom-right
|
||||
{-64, 48, 12, 3, -64, 48, 12, 3}, // 14: Diagonal TL-BR dense
|
||||
{0, 0, 0, 0, 0, 0, 0, 0}, // 15: Empty (transparent)
|
||||
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
|
||||
};
|
||||
|
||||
private int canvasWidth = 800;
|
||||
private int canvasHeight = 600;
|
||||
private int[] rgbBuffer;
|
||||
private boolean hasContent = false;
|
||||
private long updateCount = 0;
|
||||
|
||||
private int screenCols = 80;
|
||||
private int screenRows = 24;
|
||||
private ProgramSymbolManager programSymbolManager;
|
||||
|
||||
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||
this.programSymbolManager = psm;
|
||||
}
|
||||
|
||||
public ProgramSymbolManager getProgramSymbolManager() {
|
||||
return programSymbolManager;
|
||||
}
|
||||
|
||||
public GraphicsPlane(int width, int 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) {
|
||||
int w = Math.max(1, width);
|
||||
int h = Math.max(1, height);
|
||||
if (w == canvasWidth && h == canvasHeight && rgbBuffer != null) {
|
||||
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.canvasHeight = h;
|
||||
this.rgbBuffer = newBuffer;
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
if (rgbBuffer != null) {
|
||||
Arrays.fill(rgbBuffer, 0);
|
||||
}
|
||||
hasContent = false;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized long getUpdateCount() {
|
||||
return updateCount;
|
||||
}
|
||||
|
||||
public synchronized boolean hasContent() {
|
||||
return hasContent;
|
||||
}
|
||||
|
||||
public synchronized int[] getRgbBuffer() {
|
||||
return rgbBuffer;
|
||||
}
|
||||
|
||||
public int getCanvasWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
public int getCanvasHeight() {
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
public synchronized void setScreenDimensions(int cols, int rows) {
|
||||
this.screenCols = cols > 0 ? cols : 80;
|
||||
this.screenRows = rows > 0 ? rows : 24;
|
||||
}
|
||||
|
||||
public int getScreenCols() {
|
||||
return screenCols;
|
||||
}
|
||||
|
||||
public int getScreenRows() {
|
||||
return screenRows;
|
||||
}
|
||||
|
||||
public int getTotalWidth() {
|
||||
int cols = screenCols > 0 ? screenCols : 80;
|
||||
return cols * 9;
|
||||
}
|
||||
|
||||
public int getTotalHeight() {
|
||||
int rows = screenRows > 0 ? screenRows : 24;
|
||||
return rows * 12;
|
||||
}
|
||||
|
||||
public int getXMax() {
|
||||
int totalW = getTotalWidth();
|
||||
return (totalW - 1) / 2 + (totalW - 1) % 2;
|
||||
}
|
||||
|
||||
public int getYMax() {
|
||||
int totalH = getTotalHeight();
|
||||
return (totalH - 1) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double.
|
||||
* IBM 3179G / HOD presentation space coordinate range is [-xMax .. +xMax] (width = cols * 9).
|
||||
*/
|
||||
public double mapXDouble(double gocaX) {
|
||||
int totalW = getTotalWidth();
|
||||
int xMax = getXMax();
|
||||
double nx = gocaX + xMax;
|
||||
return (nx * canvasWidth) / (double) totalW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
|
||||
* IBM 3179G / HOD presentation space coordinate range is [-yMax .. +yMax] (height = rows * 12).
|
||||
*/
|
||||
public double mapYDouble(double gocaY) {
|
||||
int totalH = getTotalHeight();
|
||||
int yMax = getYMax();
|
||||
double ny = yMax - gocaY;
|
||||
return (ny * canvasHeight) / (double) totalH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
|
||||
*/
|
||||
public int mapX(int gocaX) {
|
||||
return (int) Math.round(mapXDouble((double) gocaX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
|
||||
*/
|
||||
public int mapY(int gocaY) {
|
||||
return (int) Math.round(mapYDouble((double) gocaY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a canvas pixel X coordinate back to GOCA signed coordinate (-xMax..+xMax).
|
||||
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
|
||||
*/
|
||||
public int unmapX(int px) {
|
||||
int totalW = getTotalWidth();
|
||||
int xMax = getXMax();
|
||||
int nx = (int) Math.round((double) px * totalW / (canvasWidth > 0 ? canvasWidth : 1));
|
||||
return nx - xMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up, -yMax..+yMax).
|
||||
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
|
||||
*/
|
||||
public int unmapY(int py) {
|
||||
int totalH = getTotalHeight();
|
||||
int yMax = getYMax();
|
||||
int ny = (int) Math.round((double) py * totalH / (canvasHeight > 0 ? canvasHeight : 1));
|
||||
return yMax - ny;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
|
||||
*/
|
||||
public synchronized void setPixel(int x, int y, int colorArgb) {
|
||||
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
||||
int srcA = (colorArgb >>> 24) & 0xFF;
|
||||
if (srcA == 0) return;
|
||||
int idx = y * canvasWidth + x;
|
||||
if (srcA == 255) {
|
||||
rgbBuffer[idx] = colorArgb;
|
||||
} else {
|
||||
int dst = rgbBuffer[idx];
|
||||
int dstA = (dst >>> 24) & 0xFF;
|
||||
if (dstA == 0) {
|
||||
rgbBuffer[idx] = colorArgb;
|
||||
} else {
|
||||
int srcR = (colorArgb >>> 16) & 0xFF;
|
||||
int srcG = (colorArgb >>> 8) & 0xFF;
|
||||
int srcB = colorArgb & 0xFF;
|
||||
|
||||
int dstR = (dst >>> 16) & 0xFF;
|
||||
int dstG = (dst >>> 8) & 0xFF;
|
||||
int dstB = dst & 0xFF;
|
||||
|
||||
int invSrcA = 255 - srcA;
|
||||
int outA = srcA + (dstA * invSrcA + 127) / 255;
|
||||
int outR = (srcR * srcA + dstR * invSrcA + 127) / 255;
|
||||
int outG = (srcG * srcA + dstG * invSrcA + 127) / 255;
|
||||
int outB = (srcB * srcA + dstB * invSrcA + 127) / 255;
|
||||
|
||||
rgbBuffer[idx] = ((outA & 0xFF) << 24) | ((outR & 0xFF) << 16) | ((outG & 0xFF) << 8) | (outB & 0xFF);
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plots a pixel with fractional alpha coverage (0.0 to 1.0) for anti-aliasing.
|
||||
*/
|
||||
public synchronized void setPixelCoverage(int x, int y, int colorRgb, double coverage) {
|
||||
if (coverage <= 0.0) return;
|
||||
int alpha = (int) Math.round(coverage * 255.0);
|
||||
if (alpha > 255) alpha = 255;
|
||||
if (alpha <= 0) return;
|
||||
setPixel(x, y, (alpha << 24) | (colorRgb & 0x00FFFFFF));
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an anti-aliased line using Xiaolin Wu's algorithm with sub-pixel double coordinates.
|
||||
*/
|
||||
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
|
||||
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : 0x00FFFFFF;
|
||||
|
||||
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
|
||||
drawStyledLine((int) Math.round(x0), (int) Math.round(y0),
|
||||
(int) Math.round(x1), (int) Math.round(y1),
|
||||
(0xFF << 24) | color, lineType, lineWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: single point or zero-length line
|
||||
if (Math.abs(x1 - x0) < 1e-5 && Math.abs(y1 - y0) < 1e-5) {
|
||||
drawPixelWithThickness((int) Math.round(x0), (int) Math.round(y0), (0xFF << 24) | color, (lineWidth == GocaConstants.LW_THICK) ? 2 : 1);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
|
||||
if (steep) {
|
||||
double tmp = x0; x0 = y0; y0 = tmp;
|
||||
tmp = x1; x1 = y1; y1 = tmp;
|
||||
}
|
||||
if (x0 > x1) {
|
||||
double tmp = x0; x0 = x1; x1 = tmp;
|
||||
tmp = y0; y0 = y1; y1 = tmp;
|
||||
}
|
||||
|
||||
double dx = x1 - x0;
|
||||
double dy = y1 - y0;
|
||||
double gradient = (dx == 0.0) ? 1.0 : (dy / dx);
|
||||
|
||||
// First endpoint
|
||||
double xend = Math.round(x0);
|
||||
double yend = y0 + gradient * (xend - x0);
|
||||
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
|
||||
int xpxl1 = (int) xend;
|
||||
int ypxl1 = (int) Math.floor(yend);
|
||||
|
||||
if (steep) {
|
||||
plotPixelWu(ypxl1, xpxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(ypxl1 + 1, xpxl1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
} else {
|
||||
plotPixelWu(xpxl1, ypxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(xpxl1, ypxl1 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
}
|
||||
double intery = yend + gradient;
|
||||
|
||||
// Second endpoint
|
||||
xend = Math.round(x1);
|
||||
yend = y1 + gradient * (xend - x1);
|
||||
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
|
||||
int xpxl2 = (int) xend;
|
||||
int ypxl2 = (int) Math.floor(yend);
|
||||
|
||||
if (steep) {
|
||||
plotPixelWu(ypxl2, xpxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(ypxl2 + 1, xpxl2, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
} else {
|
||||
plotPixelWu(xpxl2, ypxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(xpxl2, ypxl2 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
}
|
||||
|
||||
// Main anti-aliased stepping loop
|
||||
if (steep) {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int y = (int) Math.floor(intery);
|
||||
double frac = intery - y;
|
||||
plotPixelWu(y, x, color, 1.0 - frac, lineWidth);
|
||||
plotPixelWu(y + 1, x, color, frac, lineWidth);
|
||||
intery += gradient;
|
||||
}
|
||||
} else {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int y = (int) Math.floor(intery);
|
||||
double frac = intery - y;
|
||||
plotPixelWu(x, y, color, 1.0 - frac, lineWidth);
|
||||
plotPixelWu(x, y + 1, color, frac, lineWidth);
|
||||
intery += gradient;
|
||||
}
|
||||
}
|
||||
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
|
||||
if (brightness <= 0.0) return;
|
||||
if (lineWidth == GocaConstants.LW_THICK) {
|
||||
setPixelCoverage(x, y, colorRgb, 1.0);
|
||||
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness));
|
||||
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness));
|
||||
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7));
|
||||
} else {
|
||||
// Perceptual gamma correction for crisp contrast on dark backgrounds
|
||||
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
|
||||
setPixelCoverage(x, y, colorRgb, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an absolute or relative line using anti-aliasing for smooth vectors.
|
||||
*/
|
||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
|
||||
drawLine((double) x1, (double) y1, (double) x2, (double) y2, colorArgb, lineType, lineWidth);
|
||||
}
|
||||
|
||||
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
|
||||
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
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;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
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 with sub-pixel double precision and anti-aliasing.
|
||||
*/
|
||||
public synchronized void drawArc(double cx, double cy, double rx, double ry, double startAngleDeg, double sweepAngleDeg,
|
||||
int colorArgb, int lineType, int lineWidth, boolean isFull) {
|
||||
if (rx <= 0) rx = 1.0;
|
||||
if (ry <= 0) ry = 1.0;
|
||||
|
||||
int numSteps = (int) Math.max(36, Math.max(rx, ry) * 6);
|
||||
double startRad = Math.toRadians(startAngleDeg);
|
||||
double sweepRad = isFull ? (2.0 * Math.PI) : Math.toRadians(sweepAngleDeg);
|
||||
double stepRad = sweepRad / numSteps;
|
||||
|
||||
double prevX = cx + rx * Math.cos(startRad);
|
||||
double prevY = cy - ry * Math.sin(startRad);
|
||||
|
||||
for (int i = 1; i <= numSteps; i++) {
|
||||
double angle = startRad + i * stepRad;
|
||||
double nextX = cx + rx * Math.cos(angle);
|
||||
double nextY = cy - ry * Math.sin(angle);
|
||||
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
|
||||
prevX = nextX;
|
||||
prevY = nextY;
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
|
||||
int colorArgb, int lineType, int lineWidth, boolean isFull) {
|
||||
drawArc((double) cx, (double) cy, (double) rx, (double) ry, startAngleDeg, sweepAngleDeg, colorArgb, lineType, lineWidth, isFull);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a Fillet (spline / curve approximation across control points) with sub-pixel precision.
|
||||
*/
|
||||
public synchronized void drawFillet(double[] px, double[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
|
||||
if (px == null || py == null || numPoints < 2) return;
|
||||
|
||||
if (numPoints == 2) {
|
||||
drawLine(px[0], py[0], px[1], py[1], colorArgb, lineType, lineWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
double prevX = px[0];
|
||||
double 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 = 30;
|
||||
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;
|
||||
drawLine(prevX, prevY, bx, by, colorArgb, lineType, lineWidth);
|
||||
prevX = bx;
|
||||
prevY = by;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized void drawFillet(int[] px, int[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
|
||||
if (px == null || py == null || numPoints < 2) return;
|
||||
double[] dpx = new double[numPoints];
|
||||
double[] dpy = new double[numPoints];
|
||||
for (int i = 0; i < numPoints; i++) {
|
||||
dpx[i] = px[i];
|
||||
dpy[i] = py[i];
|
||||
}
|
||||
drawFillet(dpx, dpy, numPoints, colorArgb, lineType, lineWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a closed polygon area with a solid color or hatching pattern.
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) {
|
||||
fillArea(px, py, numPoints, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, 2, 0xFF000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a closed polygon area with a solid color or hatching pattern and background mix.
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb) {
|
||||
fillArea(px, py, numPoints, null, 1, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an area containing one or more closed polygon subpaths with a solid color or hatching pattern,
|
||||
* using the even-odd fill rule across all subpath contours.
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
|
||||
int fillColorArgb, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb) {
|
||||
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
|
||||
*/
|
||||
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
|
||||
int fillColorArgb, int patternSet, int pattern,
|
||||
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
|
||||
int bgMix, int bgColorArgb) {
|
||||
if (px == null || py == null || numPoints < 3) return;
|
||||
|
||||
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int bg = bgColorArgb;
|
||||
|
||||
boolean isTransparentBlack = (fill == GocaConstants.GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != GocaConstants.MIX_OVER);
|
||||
if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary) && !isTransparentBlack) {
|
||||
// Find polygon vertical bounds across all points
|
||||
int minY = py[0];
|
||||
int maxY = py[0];
|
||||
for (int i = 1; i < numPoints; i++) {
|
||||
if (py[i] < minY) minY = py[i];
|
||||
if (py[i] > maxY) maxY = py[i];
|
||||
}
|
||||
minY = Math.max(0, minY);
|
||||
maxY = Math.min(canvasHeight - 1, maxY);
|
||||
|
||||
List<Integer> nodeX = new ArrayList<>();
|
||||
byte[] patRows = null;
|
||||
ProgramSymbolSet.SymbolSlot psSlot = null;
|
||||
if (patternSet >= 0x40 && programSymbolManager != null) {
|
||||
psSlot = programSymbolManager.getSymbol(patternSet, pattern);
|
||||
}
|
||||
if (psSlot == null) {
|
||||
if (pattern >= 0 && pattern < PATTERN_DATA.length) {
|
||||
patRows = PATTERN_DATA[pattern];
|
||||
} else {
|
||||
patRows = PATTERN_DATA[0];
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
nodeX.clear();
|
||||
int offset = 0;
|
||||
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
|
||||
for (int p = 0; p < polyCount; p++) {
|
||||
int pLen = (polyCounts != null && p < polyCounts.length) ? polyCounts[p] : numPoints;
|
||||
if (pLen >= 3) {
|
||||
int j = pLen - 1;
|
||||
for (int i = 0; i < pLen; i++) {
|
||||
int yi = py[offset + i];
|
||||
int yj = py[offset + j];
|
||||
int xi = px[offset + i];
|
||||
int xj = px[offset + j];
|
||||
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
|
||||
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
|
||||
nodeX.add(x);
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
}
|
||||
offset += pLen;
|
||||
}
|
||||
|
||||
Collections.sort(nodeX);
|
||||
|
||||
for (int i = 0; i < nodeX.size(); i += 2) {
|
||||
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 (psSlot != null) {
|
||||
int psW = psSlot.getWidth();
|
||||
int psH = psSlot.getHeight();
|
||||
int psX = (psW > 0) ? (x % psW) : 0;
|
||||
int psY = (psH > 0) ? (y % psH) : 0;
|
||||
byte[] psPix = psSlot.getPixelData();
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) {
|
||||
setPixel(x, y, bg);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
setPixel(x, y, fill);
|
||||
} else {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background)
|
||||
setPixel(x, y, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (drawBoundary && boundaryColorArgb != 0) {
|
||||
int offset = 0;
|
||||
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
|
||||
for (int p = 0; p < polyCount; p++) {
|
||||
int pLen = (polyCounts != null && p < polyCounts.length) ? polyCounts[p] : numPoints;
|
||||
if (pLen >= 2) {
|
||||
for (int i = 0; i < pLen - 1; i++) {
|
||||
drawLine((double) px[offset + i], (double) py[offset + i],
|
||||
(double) px[offset + i + 1], (double) py[offset + i + 1],
|
||||
boundaryColorArgb, lineType, lineWidth);
|
||||
}
|
||||
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
|
||||
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
|
||||
(double) px[offset], (double) py[offset],
|
||||
boundaryColorArgb, lineType, lineWidth);
|
||||
}
|
||||
}
|
||||
offset += pLen;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle) with sub-pixel precision.
|
||||
*/
|
||||
public synchronized void drawMarker(double x, double y, int markerType, int size, int colorArgb) {
|
||||
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
double s = Math.max(3.0, size > 0 ? (double) size : 5.0);
|
||||
|
||||
switch (markerType) {
|
||||
case GocaConstants.MK_CROSS: // x
|
||||
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;
|
||||
case GocaConstants.MK_PLUS: // +
|
||||
case GocaConstants.MK_DEFAULT:
|
||||
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
break;
|
||||
case GocaConstants.MK_DIAMOND: // <>
|
||||
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;
|
||||
case GocaConstants.MK_SQUARE: // []
|
||||
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;
|
||||
case GocaConstants.MK_6STAR: // 6-point star
|
||||
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s / 2.0, y - s, x + s / 2.0, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x - s / 2.0, y + s, x + s / 2.0, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
break;
|
||||
case GocaConstants.MK_8STAR: // 8-point star
|
||||
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
drawLine(x, y - s, x, 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;
|
||||
case GocaConstants.MK_SDIAMOND: // solid diamond
|
||||
fillArea(new int[]{(int) Math.round(x), (int) Math.round(x + s), (int) Math.round(x), (int) Math.round(x - s)},
|
||||
new int[]{(int) Math.round(y - s), (int) Math.round(y), (int) Math.round(y + s), (int) Math.round(y)},
|
||||
4, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||
break;
|
||||
case GocaConstants.MK_SSQUARE: // solid square
|
||||
fillArea(new int[]{(int) Math.round(x - s), (int) Math.round(x + s), (int) Math.round(x + s), (int) Math.round(x - s)},
|
||||
new int[]{(int) Math.round(y - s), (int) Math.round(y - s), (int) Math.round(y + s), (int) Math.round(y + s)},
|
||||
4, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||
break;
|
||||
case GocaConstants.MK_DOT: // dot
|
||||
drawArc(x, y, 2.0, 2.0, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
|
||||
break;
|
||||
case GocaConstants.MK_CIRCLE: // circle
|
||||
default:
|
||||
drawArc(x, y, s, s, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
|
||||
break;
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) {
|
||||
drawMarker((double) x, (double) y, markerType, size, colorArgb);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TextRenderer {
|
||||
void drawText(GraphicsPlane plane, double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle);
|
||||
}
|
||||
|
||||
private TextRenderer textRenderer;
|
||||
|
||||
public void setTextRenderer(TextRenderer renderer) {
|
||||
this.textRenderer = renderer;
|
||||
}
|
||||
|
||||
public TextRenderer getTextRenderer() {
|
||||
return this.textRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws character text (using pluggable TextRenderer or fallback vector font).
|
||||
*/
|
||||
public synchronized void drawText(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
if (textRenderer != null) {
|
||||
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
} else {
|
||||
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void drawText(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle) {
|
||||
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws stroked vector text using IBM Vector Symbol Set (VSS) with sub-pixel anti-aliasing.
|
||||
*/
|
||||
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
|
||||
double curX = x;
|
||||
double curY = y;
|
||||
double cw = cellWidth > 0 ? cellWidth : 12.0;
|
||||
double ch = cellHeight > 0 ? cellHeight : 20.0;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
drawVssChar(curX, curY, c, color, cw, ch);
|
||||
|
||||
switch (dir) {
|
||||
case GocaConstants.CD_TB: curY += ch; break;
|
||||
case GocaConstants.CD_RL: curX -= cw; break;
|
||||
case GocaConstants.CD_BT: curY -= ch; break;
|
||||
case GocaConstants.CD_LR:
|
||||
case GocaConstants.CD_DEFAULT:
|
||||
default:
|
||||
curX += cw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle) {
|
||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
|
||||
}
|
||||
|
||||
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
|
||||
int code = (int) c;
|
||||
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
|
||||
return;
|
||||
}
|
||||
int offset = VSS_OFFSETS[code];
|
||||
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) {
|
||||
double[] px = new double[numPoints];
|
||||
double[] py = new double[numPoints];
|
||||
int[] ipx = new int[numPoints];
|
||||
int[] ipy = new int[numPoints];
|
||||
|
||||
for (int p = 0; 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);
|
||||
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||
py[p] = y + ((double) (VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||
ipx[p] = (int) Math.round(px[p]);
|
||||
ipy[p] = (int) Math.round(py[p]);
|
||||
}
|
||||
|
||||
// If contour is closed (e.g. bold character loop), fill with solid color
|
||||
int firstVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
|
||||
int firstVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
|
||||
int lastVx = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 1] & 0xFF);
|
||||
int lastVy = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 3] & 0xFF);
|
||||
|
||||
boolean isClosed = (numPoints >= 4) && (firstVx == lastVx) && (firstVy == lastVy);
|
||||
if (isClosed) {
|
||||
fillArea(ipx, ipy, numPoints, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||
}
|
||||
|
||||
for (int p = 0; p < numPoints - 1; p++) {
|
||||
drawLine(px[p], py[p], px[p + 1], py[p + 1], color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
}
|
||||
}
|
||||
ptr += 2 + byteLen;
|
||||
} else {
|
||||
ptr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws raw image pixel bitmap.
|
||||
*/
|
||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
|
||||
if (imageData == null || width <= 0 || height <= 0) return;
|
||||
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int bytesPerRow = (width + 7) / 8;
|
||||
|
||||
for (int row = 0; row < height; row++) {
|
||||
int rowOffset = row * bytesPerRow;
|
||||
for (int col = 0; col < width; col++) {
|
||||
int byteIdx = rowOffset + (col / 8);
|
||||
if (byteIdx < imageData.length) {
|
||||
boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
||||
if (bit) {
|
||||
setPixel(x + col, y + row, fgColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Manages IBM 3270 Programmed Symbols (PS / APL) character sets.
|
||||
* Handles the Load Programmed Symbols (LOADPS structured field 0x0F).
|
||||
*/
|
||||
public class ProgramSymbolManager {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ProgramSymbolManager.class.getName());
|
||||
|
||||
public static final int NUMBER_SYMBOL_SETS = 10;
|
||||
public static final int NUMBER_SINGLE_PLANE_PS_SETS = 2; // RWS 2..3 (Sets 0..1)
|
||||
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)
|
||||
|
||||
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[] stagingSets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||
private final ProgramSymbolSet[] lcidMap = new ProgramSymbolSet[256];
|
||||
|
||||
public ProgramSymbolManager() {
|
||||
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);
|
||||
sets[i] = new ProgramSymbolSet(isTriple);
|
||||
stagingSets[i] = new ProgramSymbolSet(isTriple);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all symbol sets.
|
||||
*/
|
||||
public synchronized void clearAll() {
|
||||
Arrays.fill(lcidMap, null);
|
||||
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||
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).
|
||||
*/
|
||||
public ProgramSymbolSet getSymbolSet(int lcid) {
|
||||
if (lcid <= 0 || lcid >= 256) {
|
||||
return null;
|
||||
}
|
||||
ProgramSymbolSet set = lcidMap[lcid];
|
||||
if (set == null) {
|
||||
for (ProgramSymbolSet s : sets) {
|
||||
if (s != null && s.getLcid() == lcid) return s;
|
||||
}
|
||||
for (ProgramSymbolSet s : stagingSets) {
|
||||
if (s != null && s.getLcid() == lcid) return s;
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
|
||||
*/
|
||||
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
|
||||
ProgramSymbolSet set = getSymbolSet(lcid);
|
||||
if (set == null) {
|
||||
return null;
|
||||
}
|
||||
int index = (codePoint >= 0x40) ? (codePoint - 0x40) : codePoint;
|
||||
return set.getSlot(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload.
|
||||
*/
|
||||
public synchronized void loadps(byte[] data) {
|
||||
if (data == null || data.length < 4) {
|
||||
logger.warning("LOADPS: Payload too short (" + (data == null ? 0 : data.length) + " bytes)");
|
||||
return;
|
||||
}
|
||||
|
||||
int flags = data[0] & 0xFF;
|
||||
int loadFormat = flags & 0x1F;
|
||||
boolean clearAll = (flags & 0x40) != 0;
|
||||
boolean hasExtHeader = (flags & 0x80) != 0;
|
||||
|
||||
int lcid = data[1] & 0xFF;
|
||||
int startCodePoint = data[2] & 0xFF;
|
||||
int rws = data[3] & 0xFF;
|
||||
|
||||
int setIndex;
|
||||
switch (rws) {
|
||||
case 2: setIndex = 0; break;
|
||||
case 3: setIndex = 1; break;
|
||||
case 4: setIndex = 2; break;
|
||||
case 5: setIndex = 3; break;
|
||||
case 6: setIndex = 4; break;
|
||||
case 7: setIndex = 5; break;
|
||||
case 8: setIndex = 6; break;
|
||||
case 9: setIndex = 7; break;
|
||||
case 10: setIndex = 8; break;
|
||||
case 11: setIndex = 9; break;
|
||||
default:
|
||||
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||
ProgramSymbolSet set = sets[setIndex];
|
||||
|
||||
int extHeaderLen = 0;
|
||||
int cellWidth = defaultCellWidth;
|
||||
int cellHeight = defaultCellHeight;
|
||||
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
||||
|
||||
if (hasExtHeader && data.length > 4) {
|
||||
extHeaderLen = data[4] & 0xFF;
|
||||
if (extHeaderLen > 3 && data.length > 6) {
|
||||
int lw = data[6] & 0xFF;
|
||||
if (lw > 0) cellWidth = lw;
|
||||
}
|
||||
if (extHeaderLen > 4 && data.length > 7) {
|
||||
int lh = data[7] & 0xFF;
|
||||
if (lh > 0) cellHeight = lh;
|
||||
}
|
||||
if (extHeaderLen >= 6 && data.length > 9) {
|
||||
colorPlane = data[9] & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
set.setLcid(lcid);
|
||||
if (lcid > 0 && lcid < 256) {
|
||||
lcidMap[lcid] = set;
|
||||
}
|
||||
|
||||
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
|
||||
int remaining = data.length - offset;
|
||||
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
||||
|
||||
int bytesPerSymbol;
|
||||
if (loadFormat == 1) {
|
||||
bytesPerSymbol = 18; // 9x16 cell in standard transmission format
|
||||
} else {
|
||||
bytesPerSymbol = (cellWidth * cellHeight + 7) / 8;
|
||||
}
|
||||
|
||||
if (bytesPerSymbol <= 0) {
|
||||
bytesPerSymbol = 18;
|
||||
}
|
||||
|
||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||
byte[] pixelData = new byte[cellWidth * cellHeight];
|
||||
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
||||
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
||||
}
|
||||
|
||||
if (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
} else {
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
}
|
||||
|
||||
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
||||
|
||||
codeIndex++;
|
||||
offset += bytesPerSymbol;
|
||||
remaining -= bytesPerSymbol;
|
||||
}
|
||||
|
||||
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",
|
||||
cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks Format 1 (9x16) symbol slice bit-pattern.
|
||||
*/
|
||||
private void unpackFormat1(byte[] src, int srcOff, byte[] dst, int width, int height,
|
||||
boolean isTriplePlane, int colorPlane) {
|
||||
// Format 1 transmits 18 bytes:
|
||||
// Byte 0-1: contains column 0 for each of the 16 rows
|
||||
// Bytes 2-17: contains columns 1-8 for each of the 16 rows
|
||||
int planeMask = (colorPlane != 0) ? colorPlane : (isTriplePlane ? 7 : 1);
|
||||
|
||||
for (int row = 0; row < 16 && row < height; row++) {
|
||||
// Column 0 bit from byte 0 or byte 1
|
||||
int b0 = (row < 8) ? (src[srcOff] & 0xFF) : (src[srcOff + 1] & 0xFF);
|
||||
int bitShift0 = 7 - (row % 8);
|
||||
boolean bit0 = ((b0 >> bitShift0) & 1) != 0;
|
||||
|
||||
int dstIdx0 = row * width;
|
||||
if (dstIdx0 < dst.length) {
|
||||
if (!isTriplePlane) {
|
||||
dst[dstIdx0] = bit0 ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[dstIdx0] = bit0 ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit0) {
|
||||
dst[dstIdx0] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[dstIdx0] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Columns 1..8 from bytes 2..17
|
||||
if (srcOff + 2 + row < src.length) {
|
||||
int rowByte = src[srcOff + 2 + row] & 0xFF;
|
||||
for (int col = 1; col < 9 && col < width; col++) {
|
||||
int dstIdx = row * width + col;
|
||||
if (dstIdx < dst.length) {
|
||||
boolean bit = ((rowByte >> (8 - col)) & 1) != 0;
|
||||
if (!isTriplePlane) {
|
||||
dst[dstIdx] = bit ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[dstIdx] = bit ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit) {
|
||||
dst[dstIdx] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[dstIdx] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks Format 3 variable dimension bit-pattern.
|
||||
*/
|
||||
private void unpackFormat3(byte[] src, int srcOff, byte[] dst, int width, int height,
|
||||
boolean isTriplePlane, int colorPlane) {
|
||||
int totalPixels = width * height;
|
||||
for (int i = 0; i < totalPixels; i++) {
|
||||
int byteIndex = srcOff + (i / 8);
|
||||
if (byteIndex >= src.length) break;
|
||||
int bitIndex = 7 - (i % 8);
|
||||
boolean bit = ((src[byteIndex] >> bitIndex) & 1) != 0;
|
||||
|
||||
if (!isTriplePlane) {
|
||||
dst[i] = bit ? (byte) 1 : 0;
|
||||
} else if (colorPlane == 0) {
|
||||
dst[i] = bit ? (byte) 7 : 0;
|
||||
} else {
|
||||
if (bit) {
|
||||
dst[i] |= (byte) colorPlane;
|
||||
} else {
|
||||
dst[i] &= (byte) ~colorPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
public class ProgramSymbolSet {
|
||||
|
||||
public static final int NUM_SLOTS = 191; // Code points 0x40 - 0xFE (0..190)
|
||||
|
||||
private int lcid = 0;
|
||||
private final boolean isTriplePlane;
|
||||
private final SymbolSlot[] slots = new SymbolSlot[NUM_SLOTS];
|
||||
|
||||
public ProgramSymbolSet(boolean isTriplePlane) {
|
||||
this.isTriplePlane = isTriplePlane;
|
||||
}
|
||||
|
||||
public int getLcid() {
|
||||
return lcid;
|
||||
}
|
||||
|
||||
public void setLcid(int lcid) {
|
||||
this.lcid = lcid;
|
||||
}
|
||||
|
||||
public boolean isTriplePlane() {
|
||||
return isTriplePlane;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
for (int i = 0; i < NUM_SLOTS; i++) {
|
||||
slots[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSlot(int index) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
slots[index] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSlot(int index, SymbolSlot slot) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
slots[index] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
public SymbolSlot getSlot(int index) {
|
||||
if (index >= 0 && index < NUM_SLOTS) {
|
||||
return slots[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single custom symbol bitmap.
|
||||
*/
|
||||
public static class SymbolSlot {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
||||
private final boolean isTriplePlane;
|
||||
private int[] cachedRgbArray;
|
||||
private int cachedFgRgb = -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) {
|
||||
this.width = width > 0 ? width : 9;
|
||||
this.height = height > 0 ? height : 16;
|
||||
this.pixelData = pixelData;
|
||||
this.isTriplePlane = isTriplePlane;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public byte[] getPixelData() {
|
||||
return pixelData;
|
||||
}
|
||||
|
||||
public boolean isTriplePlane() {
|
||||
return isTriplePlane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
||||
* Enables unscaled 1:1 hardware blitting in Java2D.
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
if (targetW <= 0 || targetH <= 0) {
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
int[] rgbArray = new int[width * height];
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int idx = y * width + x;
|
||||
int val = (pixelData != null && idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0;
|
||||
|
||||
if (val == 0) {
|
||||
rgbArray[idx] = bgArgb;
|
||||
} else if (!isTriplePlane) {
|
||||
rgbArray[idx] = fgArgb;
|
||||
} else {
|
||||
// Triple-Plane RGB composite:
|
||||
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
||||
int r = (val & 0x01) != 0 ? 255 : 0;
|
||||
int g = (val & 0x02) != 0 ? 255 : 0;
|
||||
int b = (val & 0x04) != 0 ? 255 : 0;
|
||||
rgbArray[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedRgbArray = rgbArray;
|
||||
this.cachedFgRgb = fgArgb;
|
||||
this.cachedBgRgb = bgArgb;
|
||||
return rgbArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+249
-60
@@ -1,11 +1,11 @@
|
||||
package org.lib3270j.input;
|
||||
package haus.nightmare.lib3270j.input;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import org.lib3270j.telnet.TelnetFSM;
|
||||
import org.lib3270j.protocol.TelnetConstants;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.logging.Logger;
|
||||
@@ -31,6 +31,21 @@ public class InputProcessor {
|
||||
this.fsm = fsm;
|
||||
}
|
||||
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane;
|
||||
private haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder;
|
||||
|
||||
public void setGraphicsPlane(haus.nightmare.lib3270j.graphics.GraphicsPlane gp) {
|
||||
this.graphicsPlane = gp;
|
||||
}
|
||||
|
||||
public void setGocaDecoder(haus.nightmare.lib3270j.graphics.GocaDecoder gd) {
|
||||
this.gocaDecoder = gd;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return gocaDecoder;
|
||||
}
|
||||
|
||||
public enum OiaStatus {
|
||||
NOT_CONNECTED("OFFLINE"),
|
||||
X_SYSTEM("X SYSTEM"),
|
||||
@@ -80,8 +95,10 @@ public class InputProcessor {
|
||||
public void typeCharacter(char ch) {
|
||||
if (keyboardLocked) return;
|
||||
|
||||
int baddr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
int baddr = screen.getCursorAddress();
|
||||
baddr = ((baddr % size) + size) % size;
|
||||
|
||||
// Check if cursor is at a field attribute or in a protected field
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
@@ -105,8 +122,10 @@ public class InputProcessor {
|
||||
// Insert mode: shift characters right from cursor to end of field
|
||||
// Find end of field
|
||||
int endAddr = baddr;
|
||||
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute()) {
|
||||
int count = 0;
|
||||
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute() && count < size) {
|
||||
endAddr = screen.incrementAddress(endAddr);
|
||||
count++;
|
||||
if (endAddr == baddr) break; // wrapped around (unformatted)
|
||||
}
|
||||
// Check if last position is non-null (field overflow)
|
||||
@@ -116,11 +135,13 @@ public class InputProcessor {
|
||||
}
|
||||
// Shift right from endAddr-1 down to baddr
|
||||
int dst = endAddr;
|
||||
while (dst != baddr) {
|
||||
int shiftCount = 0;
|
||||
while (dst != baddr && shiftCount < size) {
|
||||
int src = screen.decrementAddress(dst);
|
||||
screen.getCell(dst).ec = screen.getCell(src).ec;
|
||||
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
|
||||
dst = src;
|
||||
shiftCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,20 +159,28 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
// Advance cursor
|
||||
int startAdvance = baddr;
|
||||
baddr = (baddr + 1) % size;
|
||||
// Skip over field attributes
|
||||
while (screen.getCell(baddr).isFieldAttribute()) {
|
||||
int advCount = 0;
|
||||
// Skip over field attributes safely
|
||||
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance && advCount < size) {
|
||||
baddr = (baddr + 1) % size;
|
||||
advCount++;
|
||||
}
|
||||
screen.setCursorAddress(baddr);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
|
||||
*/
|
||||
public void sendAid(int aidCode) {
|
||||
if (keyboardLocked && aidCode != AID_CLEAR) return;
|
||||
System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked);
|
||||
if (keyboardLocked && aidCode != AID_CLEAR) {
|
||||
System.err.println("Keyboard locked, dropping AID");
|
||||
return;
|
||||
}
|
||||
|
||||
lastAid = aidCode;
|
||||
setKeyboardLocked(true);
|
||||
@@ -159,6 +188,9 @@ public class InputProcessor {
|
||||
if (aidCode == AID_CLEAR) {
|
||||
screen.clear();
|
||||
screen.markAllChanged();
|
||||
if (graphicsPlane != null) {
|
||||
graphicsPlane.clear();
|
||||
}
|
||||
// Send just the AID
|
||||
byte[] data = new byte[] { (byte) aidCode };
|
||||
sendAidResponse(data);
|
||||
@@ -196,20 +228,25 @@ public class InputProcessor {
|
||||
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
||||
screen.setCursorAddress(nextRowAddr);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
setKeyboardLocked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
||||
// PA keys: send AID + cursor address only (no modified data)
|
||||
// PA keys: send AID + optional PID + cursor address only (no modified data)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(aidCode);
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
byte[] data = new byte[] { (byte) aidCode, caddr[0], caddr[1] };
|
||||
sendAidResponse(data);
|
||||
out.write(caddr[0] & 0xFF);
|
||||
out.write(caddr[1] & 0xFF);
|
||||
sendAidResponse(out.toByteArray());
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter, PF keys: send AID + cursor address + modified field data
|
||||
// Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
out.write(aidCode);
|
||||
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
@@ -218,57 +255,42 @@ public class InputProcessor {
|
||||
|
||||
if (screen.isFormatted()) {
|
||||
// Send modified fields with SBA
|
||||
// Per 3270 Data Stream Architecture: strip trailing NULLs (0x00) from field data
|
||||
// Per 3270 Data Stream Architecture: suppress NULLs (0x00) from field data
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute ea = screen.getCell(i);
|
||||
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
||||
int fieldStart = (i + 1) % size;
|
||||
|
||||
// First, collect field data and find last non-null byte
|
||||
ByteArrayOutputStream fieldData = new ByteArrayOutputStream();
|
||||
int pos = fieldStart;
|
||||
int lastNonNull = -1;
|
||||
int fieldLen = 0;
|
||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||
int b = screen.getCell(pos).ec & 0xFF;
|
||||
fieldData.write(b);
|
||||
if (b != 0x00) {
|
||||
lastNonNull = fieldLen;
|
||||
}
|
||||
fieldLen++;
|
||||
pos = (pos + 1) % size;
|
||||
if (pos == fieldStart) break;
|
||||
}
|
||||
|
||||
// Always send SBA and address
|
||||
// Always send SBA and address of first character in field
|
||||
out.write(ORDER_SBA);
|
||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||
out.write(addr[0] & 0xFF);
|
||||
out.write(addr[1] & 0xFF);
|
||||
|
||||
// Only send data if there is any (strip trailing nulls)
|
||||
if (lastNonNull >= 0) {
|
||||
byte[] allData = fieldData.toByteArray();
|
||||
out.write(allData, 0, lastNonNull + 1);
|
||||
// Send all non-null characters in field (suppressing 0x00)
|
||||
int pos = fieldStart;
|
||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||
int b = screen.getCell(pos).ec & 0xFF;
|
||||
if (b != 0x00) {
|
||||
out.write(b);
|
||||
}
|
||||
pos = (pos + 1) % size;
|
||||
if (pos == fieldStart) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unformatted screen in 3270 mode:
|
||||
// Send character data from address 0 up to last non-null character
|
||||
// Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line
|
||||
// or we can just send everything up to the last non-null on the screen.
|
||||
// IBM spec: "all alphanumeric characters... Nulls are suppressed."
|
||||
// Actually, the simplest is to send everything, but suppress nulls.
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
int lastNonNull = -1;
|
||||
for (int i = size - 1; i >= 0; i--) {
|
||||
if (screen.getCell(i).ec != 0) {
|
||||
lastNonNull = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastNonNull >= 0) {
|
||||
for (int i = 0; i <= lastNonNull; i++) {
|
||||
int b = screen.getCell(i).ec & 0xFF;
|
||||
out.write(b != 0 ? b : 0x40);
|
||||
for (int i = 0; i < size; i++) {
|
||||
int b = screen.getCell(i).ec & 0xFF;
|
||||
if (b != 0x00) {
|
||||
out.write(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,13 +306,127 @@ public class InputProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G.
|
||||
*/
|
||||
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||
if (fsm == null || !fsm.getConnectionState().isFullSession()) {
|
||||
return;
|
||||
}
|
||||
if (isKeyboardLocked()) {
|
||||
return;
|
||||
}
|
||||
setKeyboardLocked(true);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
||||
int gx = gocaDecoder.getGraphicCursorX();
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
|
||||
int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
|
||||
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
||||
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
||||
int row = cursorAddr / cols;
|
||||
int col = cursorAddr % cols;
|
||||
|
||||
if (gocaDecoder.getGraphicsPlane() != null) {
|
||||
int px = gocaDecoder.getGraphicsPlane().mapX(gx);
|
||||
int py = gocaDecoder.getGraphicsPlane().mapY(gy);
|
||||
int canvasW = gocaDecoder.getGraphicsPlane().getCanvasWidth();
|
||||
int canvasH = gocaDecoder.getGraphicsPlane().getCanvasHeight();
|
||||
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 43;
|
||||
if (canvasH > 0) row = (py * numRows) / canvasH;
|
||||
if (canvasW > 0) col = (px * cols) / canvasW;
|
||||
}
|
||||
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
gx, gy, row, col, button, true, isShift, isCtrl, pickedSeg, pickTag
|
||||
);
|
||||
System.err.println(String.format(
|
||||
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
|
||||
gx, gy, row, col, pickedSeg, pickTag, button, isShift, isCtrl
|
||||
));
|
||||
out.write(AID_SF);
|
||||
try {
|
||||
out.write(sf);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
} else {
|
||||
out.write(aidCode);
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
out.write(caddr[0] & 0xFF);
|
||||
out.write(caddr[1] & 0xFF);
|
||||
}
|
||||
|
||||
sendAidResponse(out.toByteArray());
|
||||
}
|
||||
|
||||
// ========== Cursor movement ==========
|
||||
|
||||
/**
|
||||
* Simulate a Text Light Pen selection.
|
||||
*/
|
||||
public boolean lightPenSelect(int address) {
|
||||
if (screen == null || !screen.isFormatted()) {
|
||||
System.out.println("LP: screen null or unformatted");
|
||||
return false;
|
||||
}
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return false;
|
||||
address = ((address % size) + size) % size;
|
||||
int faPos = screen.findFieldAttribute(address);
|
||||
if (faPos < 0) {
|
||||
System.out.println("LP: no FA found for addr=" + address);
|
||||
return false;
|
||||
}
|
||||
|
||||
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||
int fa = faCell.fa & 0xFF;
|
||||
System.out.println("LP: addr=" + address + " faPos=" + faPos + " fa=0x" + String.format("%02X", fa)
|
||||
+ " selectable=" + haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa));
|
||||
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int designatorPos = (faPos + 1) % size;
|
||||
ExtendedAttribute desCell = screen.getCell(designatorPos);
|
||||
int ebcdic = desCell.ec & 0xFF;
|
||||
char ascii = (char) desCell.ucs4;
|
||||
|
||||
screen.setCursorAddress(designatorPos);
|
||||
|
||||
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || (ebcdic != 0x50 && ascii != '&' && ebcdic != 0x6F && ascii != '?' && ebcdic != 0x6E && ascii != '>')) {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||
return true;
|
||||
} else if (ebcdic == 0x50 || ascii == '&') {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
return true;
|
||||
} else if (ebcdic == 0x6F || ascii == '?') {
|
||||
desCell.ec = (byte) 0x6E;
|
||||
desCell.ucs4 = '>';
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
} else if (ebcdic == 0x6E || ascii == '>') {
|
||||
desCell.ec = (byte) 0x6F;
|
||||
desCell.ucs4 = '?';
|
||||
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
} else {
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void cursorUp() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr -= screen.getCols();
|
||||
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void cursorDown() {
|
||||
@@ -298,18 +434,21 @@ public class InputProcessor {
|
||||
addr += screen.getCols();
|
||||
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void cursorLeft() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr = screen.decrementAddress(addr);
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void cursorRight() {
|
||||
int addr = screen.getCursorAddress();
|
||||
addr = screen.incrementAddress(addr);
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void cursorHome() {
|
||||
@@ -318,28 +457,43 @@ public class InputProcessor {
|
||||
} else {
|
||||
screen.setCursorAddress(0);
|
||||
}
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void tab() {
|
||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public int getLastAid() { return lastAid; }
|
||||
public void setLastAid(int aid) { this.lastAid = aid; }
|
||||
|
||||
public void setCursorAddress(int baddr) {
|
||||
screen.setCursorAddress(baddr);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void backTab() {
|
||||
// Find previous unprotected field
|
||||
int addr = screen.getCursorAddress();
|
||||
if (!screen.isFormatted()) return;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
int addr = screen.getCursorAddress();
|
||||
addr = ((addr % size) + size) % size;
|
||||
int start = screen.decrementAddress(addr);
|
||||
addr = start;
|
||||
int count = 0;
|
||||
do {
|
||||
addr = screen.decrementAddress(addr);
|
||||
if (screen.getCell(addr).isFieldAttribute()) {
|
||||
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
||||
screen.setCursorAddress(screen.incrementAddress(addr));
|
||||
screen.updateDisplaySnapshot();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} while (addr != start);
|
||||
count++;
|
||||
} while (addr != start && count < size);
|
||||
}
|
||||
|
||||
public void eraseEof() {
|
||||
@@ -352,19 +506,24 @@ public class InputProcessor {
|
||||
ea.ucs4 = 0;
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return;
|
||||
}
|
||||
int addr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
addr = ((addr % size) + size) % size;
|
||||
byte faVal = screen.getFieldAttributeAt(addr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
// Erase from cursor to end of field
|
||||
while (!screen.getCell(addr).isFieldAttribute()) {
|
||||
int count = 0;
|
||||
while (!screen.getCell(addr).isFieldAttribute() && count < size) {
|
||||
ExtendedAttribute ea = screen.getCell(addr);
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = 0;
|
||||
addr = screen.incrementAddress(addr);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Set MDT
|
||||
@@ -373,18 +532,26 @@ public class InputProcessor {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void deleteChar() {
|
||||
if (!screen.isFormatted()) return;
|
||||
int addr = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
addr = ((addr % size) + size) % size;
|
||||
|
||||
// Cannot delete a field attribute
|
||||
if (screen.getCell(addr).isFieldAttribute()) return;
|
||||
|
||||
byte faVal = screen.getFieldAttributeAt(addr);
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
// Shift characters left within the field
|
||||
int shiftAddr = addr;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
while (true) {
|
||||
int count = 0;
|
||||
while (count < size) {
|
||||
int next = screen.incrementAddress(shiftAddr);
|
||||
if (screen.getCell(next).isFieldAttribute()) {
|
||||
screen.getCell(shiftAddr).ec = 0;
|
||||
@@ -394,6 +561,7 @@ public class InputProcessor {
|
||||
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
|
||||
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
|
||||
shiftAddr = next;
|
||||
count++;
|
||||
}
|
||||
|
||||
int faAddr = screen.findFieldAttribute(addr);
|
||||
@@ -401,11 +569,23 @@ public class InputProcessor {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void backspace() {
|
||||
if (screen.getCursorAddress() == 0) return;
|
||||
cursorLeft();
|
||||
int addr = screen.getCursorAddress();
|
||||
int prev = screen.decrementAddress(addr);
|
||||
if (screen.isFormatted()) {
|
||||
// If previous position is a field attribute or in protected field, do not back up over it
|
||||
if (screen.getCell(prev).isFieldAttribute()) {
|
||||
return;
|
||||
}
|
||||
byte faVal = screen.getFieldAttributeAt(prev);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
screen.setCursorAddress(prev);
|
||||
deleteChar();
|
||||
}
|
||||
|
||||
@@ -414,6 +594,7 @@ public class InputProcessor {
|
||||
if (keyboardLocked) return;
|
||||
screen.eraseAllUnprotected();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
|
||||
@@ -430,6 +611,7 @@ public class InputProcessor {
|
||||
}
|
||||
screen.setCursorAddress(target);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Insert Duplicate (DUP) code and advance to next field. */
|
||||
@@ -447,6 +629,8 @@ public class InputProcessor {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
tab();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Insert Field Mark (FM) code. */
|
||||
@@ -470,6 +654,7 @@ public class InputProcessor {
|
||||
}
|
||||
screen.setCursorAddress(baddr);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Attention key (sends Telnet IP). */
|
||||
@@ -546,6 +731,7 @@ public class InputProcessor {
|
||||
screen.getCell(i).ucs4 = 0;
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -570,11 +756,13 @@ public class InputProcessor {
|
||||
int fieldLen = 0;
|
||||
int pos = fieldStart;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||
int count = 0;
|
||||
while (!screen.getCell(pos).isFieldAttribute() && count < size) {
|
||||
screen.getCell(pos).ec = 0;
|
||||
screen.getCell(pos).ucs4 = 0;
|
||||
fieldLen++;
|
||||
pos = screen.incrementAddress(pos);
|
||||
count++;
|
||||
if (pos == fieldStart) break;
|
||||
}
|
||||
|
||||
@@ -583,6 +771,7 @@ public class InputProcessor {
|
||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||
}
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return fieldLen;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package org.lib3270j.listener;
|
||||
package haus.nightmare.lib3270j.listener;
|
||||
|
||||
import org.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
|
||||
/**
|
||||
* Listener for connection state changes and errors.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.listener;
|
||||
package haus.nightmare.lib3270j.listener;
|
||||
|
||||
/**
|
||||
* Listener for screen buffer changes.
|
||||
+11
-2
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.protocol;
|
||||
package haus.nightmare.lib3270j.protocol;
|
||||
|
||||
/**
|
||||
* 3270 Data Stream protocol constants.
|
||||
@@ -202,16 +202,25 @@ public final class DS3270Constants {
|
||||
public static final int QR_SUMMARY = 0x80;
|
||||
public static final int QR_USABLE_AREA = 0x81;
|
||||
public static final int QR_IMAGE = 0x82;
|
||||
public static final int QR_TEXT_PART = 0x83;
|
||||
public static final int QR_TEXT_PART = 0x83;
|
||||
public static final int QR_ALPHA_PART = 0x84;
|
||||
public static final int QR_CHARSETS = 0x85;
|
||||
public static final int QR_COLOR = 0x86;
|
||||
public static final int QR_HIGHLIGHTING = 0x87;
|
||||
public static final int QR_REPLY_MODES = 0x88;
|
||||
public static final int QR_SAVE_RESTORE = 0x8c;
|
||||
public static final int QR_DBCS_ASIA = 0x91;
|
||||
public static final int QR_DDM = 0x95;
|
||||
public static final int QR_TRANSPARENCY = 0x99;
|
||||
public static final int QR_RPQNAMES = 0xa1;
|
||||
public static final int QR_IMP_PART = 0xa6;
|
||||
public static final int QR_RPQ_NAMES = 0xa8;
|
||||
public static final int QR_GRAPHICS = 0xb0; // 3270-PC Vector Graphics
|
||||
public static final int QR_GIMAGE = 0xb1; // Image / Graphics Planes
|
||||
public static final int QR_AUX_DEV = 0xb2; // Auxiliary Device
|
||||
public static final int QR_OEM_FMT = 0xb3; // OEM Format
|
||||
public static final int QR_GCOLOR = 0xb4; // Graphic Color Table
|
||||
public static final int QR_GSYMBOLS = 0xb6; // Graphic Symbol Sets
|
||||
public static final int QR_NULL = 0xff;
|
||||
|
||||
// ========== Screen model sizes ==========
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.protocol;
|
||||
package haus.nightmare.lib3270j.protocol;
|
||||
|
||||
/**
|
||||
* TN3270E protocol constants per RFC 2355.
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.protocol;
|
||||
package haus.nightmare.lib3270j.protocol;
|
||||
|
||||
/**
|
||||
* Telnet protocol constants from RFC 854 and extensions.
|
||||
@@ -76,6 +76,7 @@ public final class TelnetConstants {
|
||||
case TELOPT_BINARY: return "BINARY";
|
||||
case TELOPT_ECHO: return "ECHO";
|
||||
case TELOPT_SGA: return "SGA";
|
||||
case TELOPT_TM: return "TIMING-MARK";
|
||||
case TELOPT_TTYPE: return "TTYPE";
|
||||
case TELOPT_EOR: return "EOR";
|
||||
case TELOPT_NAWS: return "NAWS";
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.screen;
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
/**
|
||||
* Extended attribute structure for a single screen buffer position.
|
||||
+110
-15
@@ -1,8 +1,8 @@
|
||||
package org.lib3270j.screen;
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import org.lib3270j.TerminalModel;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* The 3270 screen buffer.
|
||||
@@ -24,6 +24,9 @@ public class ScreenBuffer {
|
||||
private boolean screenAlt; // Using alternate screen?
|
||||
private boolean formatted; // Screen has at least one field attribute?
|
||||
private byte replyMode = SF_SRM_FIELD;
|
||||
|
||||
private int activePartition = 0; // 0 = implicit partition
|
||||
private boolean explicitPartitionActive = false;
|
||||
|
||||
// Change tracking
|
||||
private boolean screenChanged;
|
||||
@@ -36,8 +39,12 @@ public class ScreenBuffer {
|
||||
private byte defaultGr = 0x00;
|
||||
private byte defaultCs = 0x00;
|
||||
private byte defaultIc = 0x00;
|
||||
|
||||
private final EbcdicTranslator translator;
|
||||
private final Object renderLock = new Object();
|
||||
|
||||
public Object getRenderLock() {
|
||||
return renderLock;
|
||||
}
|
||||
|
||||
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
|
||||
this.translator = translator;
|
||||
@@ -56,6 +63,7 @@ public class ScreenBuffer {
|
||||
defaultFA.ic = 1;
|
||||
|
||||
allocateBuffers();
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
private void allocateBuffers() {
|
||||
@@ -79,6 +87,50 @@ public class ScreenBuffer {
|
||||
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 ==========
|
||||
public int getRows() { return rows; }
|
||||
public int getCols() { return cols; }
|
||||
@@ -91,7 +143,7 @@ public class ScreenBuffer {
|
||||
public boolean isScreenAlt() { return screenAlt; }
|
||||
|
||||
/** 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;
|
||||
this.altRows = newAltRows;
|
||||
this.altCols = newAltCols;
|
||||
@@ -101,11 +153,15 @@ public class ScreenBuffer {
|
||||
this.maxCols = Math.max(maxCols, newAltCols);
|
||||
allocateBuffers();
|
||||
}
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
// ========== Cursor ==========
|
||||
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 getCursorCol() { return cursorAddress % cols; }
|
||||
|
||||
@@ -114,32 +170,54 @@ public class ScreenBuffer {
|
||||
|
||||
public byte getReplyMode() { return replyMode; }
|
||||
public void setReplyMode(byte mode) { this.replyMode = mode; }
|
||||
|
||||
public int getActivePartition() { return activePartition; }
|
||||
public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = true; }
|
||||
public boolean isExplicitPartitionActive() { return explicitPartitionActive; }
|
||||
|
||||
// ========== Screen erase ==========
|
||||
|
||||
/**
|
||||
* Perform an erase, optionally using the alternate screen size.
|
||||
*/
|
||||
public void erase(boolean alt) {
|
||||
public synchronized void erase(boolean alt) {
|
||||
clear();
|
||||
int newRows = alt ? altRows : defRows;
|
||||
int newCols = alt ? altCols : defCols;
|
||||
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
||||
updateDisplaySnapshot();
|
||||
return;
|
||||
}
|
||||
rows = newRows;
|
||||
cols = newCols;
|
||||
screenAlt = alt;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void setFieldAttribute(int pos, byte fa) {
|
||||
ExtendedAttribute ea = buffer[pos];
|
||||
ea.clear();
|
||||
ea.fa = fa;
|
||||
if (!formatted) {
|
||||
System.err.println("SCREEN BECAME FORMATTED at pos " + pos);
|
||||
}
|
||||
formatted = true;
|
||||
screenChanged = true;
|
||||
// The display logic needs to render this attribute character
|
||||
ea.ec = 0x00; // Typically null character for the attribute space itself
|
||||
}
|
||||
|
||||
/** Clear the entire buffer. */
|
||||
public void clear() {
|
||||
public synchronized void clear() {
|
||||
for (ExtendedAttribute ea : buffer) {
|
||||
ea.clear();
|
||||
}
|
||||
cursorAddress = 0;
|
||||
bufferAddress = 0;
|
||||
formatted = false;
|
||||
replyMode = SF_SRM_FIELD;
|
||||
activePartition = 0;
|
||||
explicitPartitionActive = false;
|
||||
screenChanged = true;
|
||||
|
||||
defaultFg = 0x00;
|
||||
@@ -148,12 +226,13 @@ public class ScreenBuffer {
|
||||
defaultCs = 0x00;
|
||||
defaultIc = 0x00;
|
||||
replyMode = SF_SRM_FIELD;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase all unprotected fields.
|
||||
*/
|
||||
public void eraseAllUnprotected() {
|
||||
public synchronized void eraseAllUnprotected() {
|
||||
int size = rows * cols;
|
||||
boolean inUnprotected = false;
|
||||
|
||||
@@ -181,6 +260,7 @@ public class ScreenBuffer {
|
||||
// Move cursor to first unprotected field
|
||||
cursorAddress = findNextUnprotected(0);
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
// ========== Field attribute navigation ==========
|
||||
@@ -192,13 +272,17 @@ public class ScreenBuffer {
|
||||
public int findFieldAttribute(int baddr) {
|
||||
if (!formatted) return -1;
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return -1;
|
||||
baddr = ((baddr % size) + size) % size;
|
||||
int start = baddr;
|
||||
int count = 0;
|
||||
do {
|
||||
if (buffer[baddr].isFieldAttribute()) {
|
||||
return baddr;
|
||||
}
|
||||
baddr = (baddr > 0) ? baddr - 1 : size - 1;
|
||||
} while (baddr != start);
|
||||
count++;
|
||||
} while (baddr != start && count < size);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -216,8 +300,12 @@ public class ScreenBuffer {
|
||||
* Returns 0 if none found.
|
||||
*/
|
||||
public int findNextUnprotected(int baddr) {
|
||||
if (!formatted) return 0;
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return 0;
|
||||
baddr = ((baddr % size) + size) % size;
|
||||
int start = baddr;
|
||||
int count = 0;
|
||||
do {
|
||||
int next = (baddr + 1) % size;
|
||||
if (buffer[baddr].isFieldAttribute()
|
||||
@@ -226,7 +314,8 @@ public class ScreenBuffer {
|
||||
return next;
|
||||
}
|
||||
baddr = next;
|
||||
} while (baddr != start);
|
||||
count++;
|
||||
} while (baddr != start && count < size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -249,12 +338,17 @@ public class ScreenBuffer {
|
||||
|
||||
/** Increment buffer address (wrapping). */
|
||||
public int incrementAddress(int addr) {
|
||||
return (addr + 1) % (rows * cols);
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return 0;
|
||||
return ((addr % size) + size + 1) % size;
|
||||
}
|
||||
|
||||
/** Decrement buffer address (wrapping). */
|
||||
public int decrementAddress(int addr) {
|
||||
return (addr > 0) ? addr - 1 : (rows * cols) - 1;
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return 0;
|
||||
int norm = ((addr % size) + size) % size;
|
||||
return (norm > 0) ? norm - 1 : size - 1;
|
||||
}
|
||||
|
||||
/** Convert buffer address to row. */
|
||||
@@ -274,7 +368,7 @@ public class ScreenBuffer {
|
||||
|
||||
// ========== 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) {
|
||||
this.maxRows = maxRows;
|
||||
this.maxCols = maxCols;
|
||||
@@ -285,6 +379,7 @@ public class ScreenBuffer {
|
||||
this.rows = defRows;
|
||||
this.cols = defCols;
|
||||
allocateBuffers();
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void translateToUnicode() {
|
||||
+41
-18
@@ -1,15 +1,15 @@
|
||||
package org.lib3270j.telnet;
|
||||
package haus.nightmare.lib3270j.telnet;
|
||||
|
||||
import org.lib3270j.ConnectionState;
|
||||
import org.lib3270j.ConnectionConfig;
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.lib3270j.protocol.TelnetConstants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
|
||||
|
||||
/**
|
||||
* Manages the raw TCP socket connection to a TN3270 host.
|
||||
@@ -28,26 +28,42 @@ public class TelnetConnection {
|
||||
private final TelnetFSM fsm;
|
||||
private final ConnectionConfig config;
|
||||
|
||||
private javax.net.ssl.SSLSession sslSession;
|
||||
|
||||
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||
this.config = config;
|
||||
this.fsm = fsm;
|
||||
}
|
||||
|
||||
public javax.net.ssl.SSLSession getSslSession() {
|
||||
return sslSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the host. Blocks until connection is established or fails.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
if (config.isUseTls()) {
|
||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort());
|
||||
javax.net.ssl.SSLSocketFactory ssf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||
sslSocket.setKeepAlive(true);
|
||||
sslSocket.setOOBInline(true);
|
||||
sslSocket.setTcpNoDelay(true);
|
||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
|
||||
" (verifyCert=" + config.isTlsVerifyCert() + ")");
|
||||
try {
|
||||
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||
sslSocket.setKeepAlive(true);
|
||||
sslSocket.setTcpNoDelay(true);
|
||||
sslSocket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
|
||||
config.getConnectTimeoutMs());
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
sslSession = sslSocket.getSession();
|
||||
log.info("TLS session active: protocol=" + sslSession.getProtocol() +
|
||||
" cipher=" + sslSession.getCipherSuite());
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("TLS setup failure: " + e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
|
||||
socket = new Socket();
|
||||
@@ -132,10 +148,12 @@ public class TelnetConnection {
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
fsm.feedByte(buf[i] & 0xFF);
|
||||
try {
|
||||
fsm.feedBytes(buf, 0, n);
|
||||
fsm.endOfNetworkData();
|
||||
} catch (Throwable t) {
|
||||
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
|
||||
}
|
||||
fsm.endOfNetworkData();
|
||||
}
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
@@ -148,6 +166,11 @@ public class TelnetConnection {
|
||||
log.log(Level.WARNING, "Read error", e);
|
||||
fsm.onError("Read error: " + e.getMessage());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
if (running) {
|
||||
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
|
||||
fsm.onError("Network loop error: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+268
-45
@@ -1,20 +1,21 @@
|
||||
package org.lib3270j.telnet;
|
||||
package haus.nightmare.lib3270j.telnet;
|
||||
|
||||
import org.lib3270j.*;
|
||||
import org.lib3270j.datastream.DataStreamProcessor;
|
||||
import org.lib3270j.protocol.*;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.listener.*;
|
||||
import haus.nightmare.lib3270j.*;
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||
import haus.nightmare.lib3270j.protocol.*;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.listener.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.lib3270j.protocol.TelnetConstants.*;
|
||||
import static org.lib3270j.protocol.TN3270EConstants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
|
||||
|
||||
/**
|
||||
* Telnet finite state machine.
|
||||
@@ -58,6 +59,33 @@ public class TelnetFSM {
|
||||
private int eXmitSeq;
|
||||
private int responseRequired = RSF_NO_RESPONSE;
|
||||
private boolean deferredWillTtype;
|
||||
private boolean tn3270eDeviceTypeSent;
|
||||
private int ttypeIndex = 0;
|
||||
|
||||
private List<String> getCandidateTerminalTypes() {
|
||||
List<String> list = new ArrayList<>();
|
||||
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
|
||||
list.add(config.getTerminalName().trim());
|
||||
return list;
|
||||
}
|
||||
TerminalModel model = config.getModel();
|
||||
list.add(model.getTerminalType());
|
||||
list.add(model.getBaseTerminalType());
|
||||
if (model.isColor()) {
|
||||
try {
|
||||
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
|
||||
list.add(mono.getTerminalType());
|
||||
list.add(mono.getBaseTerminalType());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
if (model.getModelNumber() != 2) {
|
||||
list.add("IBM-3279-2-E");
|
||||
list.add("IBM-3279-2");
|
||||
list.add("IBM-3278-2");
|
||||
}
|
||||
list.add("UNKNOWN");
|
||||
return list;
|
||||
}
|
||||
|
||||
// Connection references
|
||||
private TelnetConnection connection;
|
||||
@@ -103,10 +131,12 @@ public class TelnetFSM {
|
||||
java.util.Arrays.fill(hisOpts, false);
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
tn3270eNegotiated = false;
|
||||
tn3270eDeviceTypeSent = false;
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
tn3270eBound = false;
|
||||
eXmitSeq = 0;
|
||||
deferredWillTtype = false;
|
||||
ttypeIndex = 0;
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
|
||||
@@ -114,10 +144,44 @@ public class TelnetFSM {
|
||||
eFuncs[FUNC_BIND_IMAGE] = true;
|
||||
eFuncs[FUNC_RESPONSES] = true;
|
||||
eFuncs[FUNC_SYSREQ] = true;
|
||||
eFuncs[FUNC_SNA_SENSE] = true;
|
||||
eFuncs[FUNC_DATA_STREAM_CTL] = true;
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
|
||||
|
||||
changeState(ConnectionState.TELNET_PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -172,8 +236,8 @@ public class TelnetFSM {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
|
||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e()) {
|
||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
ibuf.write(c);
|
||||
}
|
||||
// NVT data would go to NVT processor (not implemented in initial version)
|
||||
@@ -189,7 +253,7 @@ public class TelnetFSM {
|
||||
break;
|
||||
case EOR: // End of record — process accumulated 3270 data
|
||||
log.fine("RCVD EOR");
|
||||
if (connectionState.is3270() || connectionState.isTn3270e()) {
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
processEndOfRecord();
|
||||
}
|
||||
ibuf.reset();
|
||||
@@ -207,6 +271,11 @@ public class TelnetFSM {
|
||||
log.fine("RCVD GA");
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case AYT:
|
||||
log.info("RCVD AYT - sending acknowledgment");
|
||||
sendBytes(new byte[] { (byte) IAC, (byte) NOP });
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case NOP:
|
||||
log.fine("RCVD NOP");
|
||||
state = TNS_DATA;
|
||||
@@ -234,8 +303,15 @@ public class TelnetFSM {
|
||||
}
|
||||
break;
|
||||
|
||||
case TELOPT_TM:
|
||||
// RFC 860: Timing Mark - reply with DO TM
|
||||
sendCommand(DO, opt);
|
||||
break;
|
||||
|
||||
case TELOPT_TN3270E:
|
||||
if (!hisOpts[opt]) {
|
||||
if (!config.isTn3270eEnabled()) {
|
||||
sendCommand(DONT, opt);
|
||||
} else if (!hisOpts[opt]) {
|
||||
hisOpts[opt] = true;
|
||||
sendCommand(DO, opt);
|
||||
}
|
||||
@@ -275,10 +351,15 @@ public class TelnetFSM {
|
||||
}
|
||||
break;
|
||||
|
||||
case TELOPT_TM:
|
||||
// RFC 860: Timing Mark - reply with WILL TM
|
||||
sendCommand(WILL, opt);
|
||||
break;
|
||||
|
||||
case TELOPT_TTYPE:
|
||||
if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
if (hisOpts[TELOPT_TN3270E]) {
|
||||
if (config.isTn3270eEnabled() && hisOpts[TELOPT_TN3270E]) {
|
||||
// Defer TTYPE response until TN3270E negotiation completes
|
||||
deferredWillTtype = true;
|
||||
} else {
|
||||
@@ -288,11 +369,16 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case TELOPT_TN3270E:
|
||||
if (!myOpts[opt]) {
|
||||
if (!config.isTn3270eEnabled()) {
|
||||
sendCommand(WONT, opt);
|
||||
} else if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
sendCommand(WILL, opt);
|
||||
// Start TN3270E sub-negotiation: send device type request
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
if (!tn3270eDeviceTypeSent) {
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
tn3270eDeviceTypeSent = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -386,8 +472,12 @@ public class TelnetFSM {
|
||||
|
||||
private void handleTTypeSB(byte[] data) {
|
||||
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
|
||||
// Host asks for terminal type
|
||||
String termType = config.getEffectiveTerminalType();
|
||||
// Host asks for terminal type — cycle per RFC 1091
|
||||
List<String> candidates = getCandidateTerminalTypes();
|
||||
String termType = candidates.get(Math.min(ttypeIndex, candidates.size() - 1));
|
||||
if (ttypeIndex < candidates.size() - 1) {
|
||||
ttypeIndex++;
|
||||
}
|
||||
log.info("RCVD SB TTYPE SEND - Responding with: " + termType);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
@@ -401,7 +491,7 @@ public class TelnetFSM {
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
log.info("SENT SB TTYPE IS " + termType + " SE");
|
||||
log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +518,10 @@ public class TelnetFSM {
|
||||
switch (op) {
|
||||
case OP_SEND:
|
||||
// Host asks us to send device-type request
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
if (!tn3270eDeviceTypeSent) {
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
tn3270eDeviceTypeSent = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case OP_DEVICE_TYPE:
|
||||
@@ -475,7 +568,7 @@ public class TelnetFSM {
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -488,14 +581,19 @@ public class TelnetFSM {
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Check if REJECT
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
|
||||
// Rejection
|
||||
pos++;
|
||||
int reason = -1;
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
|
||||
pos++;
|
||||
if (pos < data.length) {
|
||||
reason = data[pos] & 0xFF;
|
||||
int reason = (pos < data.length) ? (data[pos] & 0xFF) : REASON_UNSUPPORTED_REQ;
|
||||
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
|
||||
// Try fallback model 2 if we were requesting something else
|
||||
if (config.getModel() != TerminalModel.IBM_3278_2 &&
|
||||
config.getModel() != TerminalModel.IBM_3279_4) {
|
||||
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));
|
||||
@@ -563,33 +661,86 @@ public class TelnetFSM {
|
||||
log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE");
|
||||
}
|
||||
|
||||
private void handleTN3270EFunctions(byte[] data) {
|
||||
// Parse: TN3270E FUNCTIONS IS [func...]
|
||||
int pos = 2; // Skip TN3270E, FUNCTIONS
|
||||
private void sendTN3270EFunctionsIs() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
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) {
|
||||
pos++; // Skip IS
|
||||
StringBuilder funcNames = new StringBuilder();
|
||||
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
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
StringBuilder funcNames = new StringBuilder();
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
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) {
|
||||
int func = data[pos] & 0xFF;
|
||||
if (func <= FUNC_SNA_SENSE) {
|
||||
eFuncs[func] = true;
|
||||
if (funcNames.length() > 0) funcNames.append(" ");
|
||||
funcNames.append(TN3270EConstants.functionName(func));
|
||||
hostFuncs[func] = true;
|
||||
}
|
||||
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;
|
||||
log.info("TN3270E functions IS: " + funcNames);
|
||||
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
|
||||
log.info("TN3270E negotiation complete");
|
||||
|
||||
// Move to CONNECTED_UNBOUND or CONNECTED_SSCP
|
||||
changeState(ConnectionState.CONNECTED_UNBOUND);
|
||||
// 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);
|
||||
} else {
|
||||
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
@@ -597,12 +748,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 ==========
|
||||
|
||||
private void processEndOfRecord() {
|
||||
byte[] data = ibuf.toByteArray();
|
||||
ibuf.reset();
|
||||
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) {
|
||||
// TN3270E mode: data starts with 5-byte header
|
||||
processTN3270ERecord(data);
|
||||
@@ -701,11 +869,11 @@ public class TelnetFSM {
|
||||
bindRa = screenBuffer.getMaxRows();
|
||||
bindCa = screenBuffer.getMaxCols();
|
||||
break;
|
||||
case 0x7e:
|
||||
case 0x7E:
|
||||
// Both default and alternate = specified values
|
||||
bindRa = bindRd; bindCa = bindCd;
|
||||
break;
|
||||
case 0x7f:
|
||||
case 0x7F:
|
||||
// Default and alternate are both specified separately
|
||||
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
|
||||
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
|
||||
@@ -762,17 +930,47 @@ public class TelnetFSM {
|
||||
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
||||
break;
|
||||
|
||||
case DT_REQUEST:
|
||||
log.info("Received DT_REQUEST requestFlag=" + requestFlag);
|
||||
if ((requestFlag & RQF_KEYBOARD_RESTORE) != 0) {
|
||||
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
|
||||
dsProcessor.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
}
|
||||
if ((requestFlag & RQF_SIGNAL) != 0) {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onSoundAlarm();
|
||||
}
|
||||
}
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_RESPONSE:
|
||||
log.fine("Received response, seq=" + seqNumber);
|
||||
break;
|
||||
|
||||
default:
|
||||
log.info("Unhandled TN3270E data type: " + dataType);
|
||||
// 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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendTN3270EPositiveResponse(int seqNumber) {
|
||||
public void sendTN3270EPositiveResponse(int seqNumber) {
|
||||
byte[] resp = new byte[EH_SIZE + 1];
|
||||
resp[0] = (byte) DT_RESPONSE;
|
||||
resp[1] = 0;
|
||||
@@ -784,13 +982,38 @@ public class TelnetFSM {
|
||||
sendRecord(resp);
|
||||
}
|
||||
|
||||
public void sendTN3270ENegativeResponse(int seqNumber, int negCode) {
|
||||
byte[] resp = new byte[EH_SIZE + 1];
|
||||
resp[0] = (byte) DT_RESPONSE;
|
||||
resp[1] = 0;
|
||||
resp[2] = (byte) RSF_NEGATIVE_RESPONSE;
|
||||
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seqNumber & 0xFF);
|
||||
resp[5] = (byte) (negCode & 0xFF);
|
||||
|
||||
sendRecord(resp);
|
||||
}
|
||||
|
||||
public void sendTN3270ESnaSenseResponse(int seqNumber, int sense1, int sense2) {
|
||||
byte[] resp = new byte[EH_SIZE + 2];
|
||||
resp[0] = (byte) DT_RESPONSE;
|
||||
resp[1] = 0;
|
||||
resp[2] = (byte) RSF_SNA_SENSE;
|
||||
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
|
||||
resp[4] = (byte) (seqNumber & 0xFF);
|
||||
resp[5] = (byte) (sense1 & 0xFF);
|
||||
resp[6] = (byte) (sense2 & 0xFF);
|
||||
|
||||
sendRecord(resp);
|
||||
}
|
||||
|
||||
// ========== Check if we should transition to 3270 mode ==========
|
||||
|
||||
private void checkIn3270() {
|
||||
if (connectionState != ConnectionState.TELNET_PENDING) return;
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package haus.nightmare.lib3270j.tls;
|
||||
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* Callback interface for validating TLS server certificates.
|
||||
* Used when standard certificate path validation fails or when custom verification is needed.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TlsCertificateVerifier {
|
||||
|
||||
/**
|
||||
* Determine whether to trust an unverified server certificate chain.
|
||||
*
|
||||
* @param chain The peer certificate chain presented by the server.
|
||||
* @param authType The key exchange algorithm (e.g., "RSA", "ECDHE_RSA").
|
||||
* @param exception The CertificateException thrown by standard validation (or null if called proactively).
|
||||
* @return true to trust the certificate and proceed with the connection; false to abort.
|
||||
*/
|
||||
boolean shouldTrust(X509Certificate[] chain, String authType, CertificateException exception);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package haus.nightmare.lib3270j.tls;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Custom X509TrustManager that supports:
|
||||
* 1. Standard certificate verification using the JVM default TrustManager.
|
||||
* 2. Unverified / trust-all mode when tlsVerifyCert is false.
|
||||
* 3. Interactive/custom certificate verifier callbacks (e.g. GUI prompts for self-signed certificates).
|
||||
*/
|
||||
public class TlsTrustManager implements X509TrustManager {
|
||||
|
||||
private static final Logger log = Logger.getLogger(TlsTrustManager.class.getName());
|
||||
|
||||
private final ConnectionConfig config;
|
||||
private X509TrustManager defaultTrustManager;
|
||||
|
||||
public TlsTrustManager(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
try {
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null);
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
this.defaultTrustManager = (X509TrustManager) tm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Failed to initialize default TrustManagerFactory", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if (defaultTrustManager != null) {
|
||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if (config != null && !config.isTlsVerifyCert()) {
|
||||
log.fine("Certificate verification bypassed (tlsVerifyCert=false)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chain == null || chain.length == 0) {
|
||||
CertificateException ex = new CertificateException("null or zero-length certificate chain");
|
||||
if (config != null && config.getCertificateVerifier() != null) {
|
||||
if (config.getCertificateVerifier().shouldTrust(chain, authType, ex)) {
|
||||
log.info("Server certificate accepted via TlsCertificateVerifier callback");
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
try {
|
||||
if (defaultTrustManager != null) {
|
||||
defaultTrustManager.checkServerTrusted(chain, authType);
|
||||
} else {
|
||||
throw new CertificateException("No default X509TrustManager available");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
CertificateException certEx = (ex instanceof CertificateException)
|
||||
? (CertificateException) ex
|
||||
: new CertificateException("Certificate validation failed: " + ex.getMessage(), ex);
|
||||
|
||||
log.log(Level.FINE, "Standard certificate validation failed: " + certEx.getMessage(), certEx);
|
||||
|
||||
if (config != null && config.getCertificateVerifier() != null) {
|
||||
boolean accepted = config.getCertificateVerifier().shouldTrust(chain, authType, certEx);
|
||||
if (accepted) {
|
||||
log.info("Server certificate accepted via TlsCertificateVerifier callback");
|
||||
return;
|
||||
} else {
|
||||
log.warning("Server certificate rejected by TlsCertificateVerifier callback");
|
||||
throw new CertificateException("Certificate rejected by user/verifier: " + certEx.getMessage(), certEx);
|
||||
}
|
||||
}
|
||||
throw certEx;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
if (defaultTrustManager != null) {
|
||||
return defaultTrustManager.getAcceptedIssuers();
|
||||
}
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an initialized SSLContext configured for the given ConnectionConfig.
|
||||
*/
|
||||
public static SSLContext createSSLContext(ConnectionConfig config) throws Exception {
|
||||
String protocol = (config != null && config.getSslProtocol() != null)
|
||||
? config.getSslProtocol()
|
||||
: "TLS";
|
||||
SSLContext sslContext = SSLContext.getInstance(protocol);
|
||||
TlsTrustManager trustManager = new TlsTrustManager(config);
|
||||
sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom());
|
||||
return sslContext;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package org.lib3270j;
|
||||
|
||||
/**
|
||||
* Configuration for a 3270 terminal connection.
|
||||
*/
|
||||
public class ConnectionConfig {
|
||||
|
||||
private String host;
|
||||
private int port = 23;
|
||||
private TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
private String luName = null;
|
||||
private boolean extendedDataStream = true;
|
||||
private boolean useTls = false;
|
||||
private int connectTimeoutMs = 15000;
|
||||
private int nopIntervalSeconds = 0;
|
||||
private String terminalName = null; // override terminal type string
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
|
||||
public TerminalModel getModel() { return model; }
|
||||
public void setModel(TerminalModel model) { this.model = model; }
|
||||
|
||||
public String getLuName() { return luName; }
|
||||
public void setLuName(String luName) { this.luName = luName; }
|
||||
|
||||
public boolean isExtendedDataStream() { return extendedDataStream; }
|
||||
public void setExtendedDataStream(boolean ext) { this.extendedDataStream = ext; }
|
||||
|
||||
public boolean isUseTls() { return useTls; }
|
||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||
|
||||
public int getConnectTimeoutMs() { return connectTimeoutMs; }
|
||||
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
|
||||
|
||||
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
||||
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
||||
|
||||
public String getTerminalName() { return terminalName; }
|
||||
public void setTerminalName(String name) { this.terminalName = name; }
|
||||
|
||||
/**
|
||||
* Get the effective terminal type string to send during negotiation.
|
||||
*/
|
||||
public String getEffectiveTerminalType() {
|
||||
if (terminalName != null) {
|
||||
return terminalName;
|
||||
}
|
||||
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
package org.lib3270j.datastream;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||
* Equivalent to the do_qr_* functions in sf.c.
|
||||
*/
|
||||
public class QueryReplyBuilder {
|
||||
|
||||
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
|
||||
|
||||
// Canned values from 3279-2 (matching sf.c)
|
||||
private static final int SW_3279_2 = 0x09;
|
||||
private static final int SH_3279_2 = 0x0c;
|
||||
private static final int Xr_3279_2 = 0x000a02e5;
|
||||
private static final int Yr_3279_2 = 0x0002006f;
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
|
||||
// Supported query reply codes (must match what we send in summary)
|
||||
private static final int[] SUPPORTED_QR = {
|
||||
QR_SUMMARY, // 0x80 — summary must list itself
|
||||
QR_USABLE_AREA, // 0x81
|
||||
QR_ALPHA_PART, // 0x84
|
||||
QR_CHARSETS, // 0x85
|
||||
QR_COLOR, // 0x86
|
||||
QR_HIGHLIGHTING, // 0x87
|
||||
QR_REPLY_MODES, // 0x88
|
||||
QR_DDM, // 0x95 - Distributed Data Management (file transfer)
|
||||
QR_IMP_PART, // 0xa6
|
||||
};
|
||||
|
||||
public QueryReplyBuilder(ScreenBuffer screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build all query replies as a single AID_SF + structured field response.
|
||||
*/
|
||||
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
||||
|
||||
// AID byte for structured field
|
||||
out.write(AID_SF);
|
||||
|
||||
// Summary
|
||||
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
||||
|
||||
// Usable Area
|
||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||
|
||||
// Alpha Partitions
|
||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||
|
||||
// Character Sets
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
|
||||
// Color
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
|
||||
// Highlighting
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
|
||||
// Reply Modes
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
|
||||
// Distributed Data Management (DFT File Transfer)
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
|
||||
// Implicit Partition
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
log.info("Built " + out.size() + " bytes of all query replies");
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build specific query replies in response to a Read Partition Query List (SF_RPQ_LIST).
|
||||
* For any unsupported requested query code, emits a QR_NULL (0xFF) structured field
|
||||
* matching x3270 sf.c behavior.
|
||||
*/
|
||||
public byte[] buildQueryReplies(byte[] requestedCodes, int maxCols, int maxRows, int bufferSize) {
|
||||
if (requestedCodes == null || requestedCodes.length == 0) {
|
||||
return buildAllQueryReplies(maxCols, maxRows, bufferSize);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
|
||||
out.write(AID_SF);
|
||||
|
||||
for (byte codeByte : requestedCodes) {
|
||||
int code = codeByte & 0xFF;
|
||||
switch (code) {
|
||||
case QR_SUMMARY:
|
||||
appendQueryReply(out, QR_SUMMARY, buildSummary());
|
||||
break;
|
||||
case QR_USABLE_AREA:
|
||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||
break;
|
||||
case QR_ALPHA_PART:
|
||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||
break;
|
||||
case QR_CHARSETS:
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
break;
|
||||
case QR_COLOR:
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
break;
|
||||
case QR_HIGHLIGHTING:
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
break;
|
||||
case QR_REPLY_MODES:
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
break;
|
||||
case QR_DDM:
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
break;
|
||||
case QR_IMP_PART:
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
break;
|
||||
case QR_RPQNAMES:
|
||||
appendQueryReply(out, QR_RPQNAMES, new byte[0]);
|
||||
break;
|
||||
default:
|
||||
// Unsupported query reply code — emit QR_NULL
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Built " + out.size() + " bytes of requested query replies for " + requestedCodes.length + " codes");
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private void appendQueryReply(ByteArrayOutputStream out, int code, byte[] data) {
|
||||
// Length includes the 2-byte length field + SFID_QREPLY + code + data
|
||||
int len = 4 + data.length;
|
||||
out.write((len >> 8) & 0xFF);
|
||||
out.write(len & 0xFF);
|
||||
out.write(SFID_QREPLY);
|
||||
out.write(code);
|
||||
out.write(data, 0, data.length);
|
||||
}
|
||||
|
||||
private byte[] buildSummary() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
for (int code : SUPPORTED_QR) {
|
||||
out.write(code);
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
|
||||
out.write(0x01); // 12/14-bit addressing
|
||||
out.write(0x00); // no special character features
|
||||
out.write((maxCols >> 8) & 0xFF); // usable width high
|
||||
out.write(maxCols & 0xFF); // usable width low
|
||||
out.write((maxRows >> 8) & 0xFF); // usable height high
|
||||
out.write(maxRows & 0xFF); // usable height low
|
||||
out.write(0x01); // units (mm)
|
||||
// Xr (4 bytes) - canned from 3279-2
|
||||
out.write((Xr_3279_2 >> 24) & 0xFF);
|
||||
out.write((Xr_3279_2 >> 16) & 0xFF);
|
||||
out.write((Xr_3279_2 >> 8) & 0xFF);
|
||||
out.write(Xr_3279_2 & 0xFF);
|
||||
// Yr (4 bytes) - canned from 3279-2
|
||||
out.write((Yr_3279_2 >> 24) & 0xFF);
|
||||
out.write((Yr_3279_2 >> 16) & 0xFF);
|
||||
out.write((Yr_3279_2 >> 8) & 0xFF);
|
||||
out.write(Yr_3279_2 & 0xFF);
|
||||
out.write(SW_3279_2); // AW
|
||||
out.write(SH_3279_2); // AH
|
||||
int buf = maxCols * maxRows;
|
||||
out.write((buf >> 8) & 0xFF); // buffer size high
|
||||
out.write(buf & 0xFF); // buffer size low
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildAlphaPartitions(int maxRows) {
|
||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||
out.write(0x00); // max partitions (1 partition)
|
||||
out.write((bufSize >> 8) & 0xFF); // total partition storage high
|
||||
out.write(bufSize & 0xFF); // total partition storage low
|
||||
out.write(0x00); // no special features
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildCharsets() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(32);
|
||||
out.write(0x82); // flags: GE, CGCSGID present
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW - default char width
|
||||
out.write(SH_3279_2); // SDH - default char height
|
||||
out.write(0x00); // Load PS format types supported: none
|
||||
out.write(0x00); // Load PS device type (high)
|
||||
out.write(0x00); // Load PS device type (low)
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x07); // DL = 7 bytes per descriptor (non-DBCS)
|
||||
// Descriptor 1 (SET 0): default character set
|
||||
out.write(0x00); // SET 0
|
||||
out.write(0x10); // FLAGS: non-loadable, single-plane, single-byte, no compare
|
||||
out.write(0x00); // LCID 0
|
||||
out.write(0x02); // CGCSGID (4 bytes) = 0x02b90025 (CGEN|CSET)
|
||||
out.write(0xb9);
|
||||
out.write(0x00);
|
||||
out.write(0x25);
|
||||
// Descriptor 2 (SET 1): APL/GE character set
|
||||
out.write(0x01); // SET 1
|
||||
out.write(0x00); // FLAGS: non-loadable, single-plane, single-byte, no compare
|
||||
out.write(0xf1); // LCID 0xf1
|
||||
out.write(0x03); // CGCSGID: 3179-style APL2 = 0x03c30136
|
||||
out.write(0xc3);
|
||||
out.write(0x01);
|
||||
out.write(0x36);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildColor() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(36);
|
||||
int colorMax = 16;
|
||||
out.write(0x00); // no options
|
||||
out.write(colorMax); // number of colors
|
||||
out.write(0x00); // default color pair: attribute
|
||||
out.write(0xf0 + HOST_COLOR_GREEN); // default color: green
|
||||
for (int i = 0xf1; i < 0xf1 + colorMax - 1; i++) {
|
||||
out.write(i); // color attribute value
|
||||
out.write(i); // maps to itself (color mode)
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildHighlighting() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(11);
|
||||
out.write(5); // 5 pairs
|
||||
out.write(XAH_DEFAULT); out.write(XAH_NORMAL);
|
||||
out.write(XAH_BLINK); out.write(XAH_BLINK);
|
||||
out.write(XAH_REVERSE); out.write(XAH_REVERSE);
|
||||
out.write(XAH_UNDERSCORE); out.write(XAH_UNDERSCORE);
|
||||
out.write(XAH_INTENSIFY); out.write(XAH_INTENSIFY);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildReplyModes() {
|
||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||
}
|
||||
|
||||
private byte[] buildDdm(int bufferSize) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(8);
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x00); // reserved
|
||||
out.write((bufferSize >> 8) & 0xFF); // inbound length limit (INLIM)
|
||||
out.write(bufferSize & 0xFF);
|
||||
out.write((bufferSize >> 8) & 0xFF); // outbound length limit (OUTLIM)
|
||||
out.write(bufferSize & 0xFF);
|
||||
out.write(0x01); // NSS = 01
|
||||
out.write(0x01); // DDMSS = 01
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
|
||||
// Implicit partition sizes, 2 self-defining parameters
|
||||
|
||||
// SDP 1: Default screen size
|
||||
out.write(0x00); // flags
|
||||
out.write(0x00); // reserved
|
||||
out.write(0x0b); // SDP length
|
||||
out.write(0x01); // SDP type: implicit partition sizes
|
||||
out.write(0x00); // reserved
|
||||
// Default
|
||||
out.write((MODEL_2_COLS >> 8) & 0xFF);
|
||||
out.write(MODEL_2_COLS & 0xFF);
|
||||
out.write((MODEL_2_ROWS >> 8) & 0xFF);
|
||||
out.write(MODEL_2_ROWS & 0xFF);
|
||||
// Alternate
|
||||
out.write((maxCols >> 8) & 0xFF);
|
||||
out.write(maxCols & 0xFF);
|
||||
out.write((maxRows >> 8) & 0xFF);
|
||||
out.write(maxRows & 0xFF);
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.charset;
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
+22
-6
@@ -1,12 +1,12 @@
|
||||
package org.lib3270j.datastream;
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lib3270j.TerminalModel;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class DataStreamProcessorTest {
|
||||
|
||||
@@ -66,4 +66,20 @@ public class DataStreamProcessorTest {
|
||||
assertEquals('A', screen.getCell(i).ucs4, "Cell " + i + " should be 'A'");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBufferPropagatesLastAid() {
|
||||
haus.nightmare.lib3270j.input.InputProcessor input = new haus.nightmare.lib3270j.input.InputProcessor(screen, translator, null);
|
||||
processor.setInputProcessor(input);
|
||||
input.setLastAid(AID_ENTER);
|
||||
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sentData = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
processor.setOutputSender(sentData::set);
|
||||
|
||||
byte[] rbRecord = new byte[] { (byte) CMD_RB };
|
||||
processor.processRecord(rbRecord, 0, rbRecord.length, true);
|
||||
|
||||
assertNotNull(sentData.get());
|
||||
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class QueryReplyBuilderTest {
|
||||
|
||||
private final EbcdicTranslator translator = new EbcdicTranslator();
|
||||
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesDefaultBoth() {
|
||||
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);
|
||||
assertNotNull(replies);
|
||||
assertTrue(replies.length > 0);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// In GraphicsMode.NONE, Vector Graphics QR 0xB0 must NOT 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;
|
||||
}
|
||||
}
|
||||
assertFalse(hasB0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesWithVectorGraphics() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.VECTOR_GRAPHICS);
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
|
||||
// Vector Graphics QR 0xB0 and 0xB4 must be present
|
||||
boolean hasB0 = false;
|
||||
boolean hasB4 = 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_GCOLOR) {
|
||||
hasB4 = true;
|
||||
}
|
||||
}
|
||||
assertTrue(hasB0);
|
||||
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
|
||||
public void testBuildFilteredQueryRepliesWithSupportedCodes() {
|
||||
byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// First SF should be DDM: length (2 bytes), SFID_QREPLY (0x81), QR_DDM (0x95)
|
||||
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||
assertEquals(0x81, replies[3] & 0xFF);
|
||||
assertEquals(QR_DDM, replies[4] & 0xFF);
|
||||
|
||||
// Second SF should be Usable Area
|
||||
int pos2 = 1 + len1;
|
||||
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
||||
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildFilteredQueryRepliesWithUnsupportedCode() {
|
||||
byte[] requested = new byte[] { (byte) 0x77 }; // Unsupported code
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
// Should emit QR_NULL (0xFF)
|
||||
assertEquals(0x81, replies[3] & 0xFF);
|
||||
assertEquals(QR_NULL, replies[4] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImplicitPartitionModel4() {
|
||||
byte[] requested = new byte[] { (byte) QR_IMP_PART };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
int len = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
||||
assertEquals(QR_IMP_PART, replies[4] & 0xFF); // 0xA6
|
||||
|
||||
// Payload starts at index 5: 22 bytes total
|
||||
// Default size = 80 x 24
|
||||
int defCols = ((replies[10] & 0xFF) << 8) | (replies[11] & 0xFF);
|
||||
int defRows = ((replies[12] & 0xFF) << 8) | (replies[13] & 0xFF);
|
||||
assertEquals(80, defCols);
|
||||
assertEquals(24, defRows);
|
||||
|
||||
// Alt size = 80 x 43
|
||||
int altCols = ((replies[14] & 0xFF) << 8) | (replies[15] & 0xFF);
|
||||
int altRows = ((replies[16] & 0xFF) << 8) | (replies[17] & 0xFF);
|
||||
assertEquals(80, altCols);
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryReplyImageAndRpqNames() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
|
||||
byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_RPQNAMES, (byte) QR_GIMAGE };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// First SF should be QR_NULL (0xFF) because 0x82 is 3270 Image SF (not supported on 3179G)
|
||||
int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF);
|
||||
assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY
|
||||
assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF
|
||||
|
||||
// Second SF should be QR_RPQNAMES (0xA1) matching requested code
|
||||
int pos2 = 1 + len1;
|
||||
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
||||
assertEquals(QR_RPQNAMES, replies[pos2 + 3] & 0xFF); // 0xA1
|
||||
|
||||
// Third SF should be QR_GIMAGE (0xB1)
|
||||
int pos3 = pos2 + len2;
|
||||
assertEquals(0x81, replies[pos3 + 2] & 0xFF);
|
||||
assertEquals(QR_GIMAGE, replies[pos3 + 3] & 0xFF); // 0xB1
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j.ft;
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
@@ -0,0 +1,803 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class GocaDecoderTest {
|
||||
|
||||
@Test
|
||||
public void testBasicLineAndColorOrders() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
assertFalse(plane.hasContent());
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Color to Red (GOCA Color 2) - 2 bytes: order, color
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x02); // Red
|
||||
|
||||
// Line from (1000, 1000) to (2000, 2000) - long order: order, len, x1, y1, x2, y2
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x08); // 8 bytes: (x1, y1, x2, y2)
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAreaAndPatternFill() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Pattern to Solid - 2 bytes: order, pattern
|
||||
out.write(GocaConstants.G_GSPT);
|
||||
out.write(GocaConstants.PT_SOLID);
|
||||
|
||||
// Begin Area - 2 bytes: order, flags
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x00);
|
||||
|
||||
// Polyline forming a triangle: (500, 500) -> (1500, 500) -> (1000, 1500) -> (500, 500)
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x10); // 4 points * 4 bytes = 16 bytes
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1500 >> 8) & 0xFF); out.write(1500 & 0xFF);
|
||||
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
out.write((500 >> 8) & 0xFF); out.write(500 & 0xFF);
|
||||
|
||||
// End Area - 1 byte
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMarkerAndVectorText() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Marker Type to Circle - 2 bytes: order, type
|
||||
out.write(GocaConstants.G_GSMT);
|
||||
out.write(GocaConstants.MK_CIRCLE);
|
||||
|
||||
// Draw Marker at (2000, 2000)
|
||||
out.write(GocaConstants.G_GMRK);
|
||||
out.write(0x04);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
out.write((2000 >> 8) & 0xFF); out.write(2000 & 0xFF);
|
||||
|
||||
// Draw Vector Stroked Character String: "IBM" (EBCDIC: 0xC9, 0xC2, 0xD4)
|
||||
out.write(GocaConstants.G_GCHST);
|
||||
out.write(0x07); // 4 bytes pos + 3 bytes chars
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write((1000 >> 8) & 0xFF); out.write(1000 & 0xFF);
|
||||
out.write(0xC9); out.write(0xC2); out.write(0xD4);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcedureOrdersAndErase() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Draw something first
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x08);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x64);
|
||||
out.write(0x00); out.write(0x64);
|
||||
byte[] drawStream = out.toByteArray();
|
||||
decoder.decodeStream(drawStream, 0, drawStream.length);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
// Process procedure order 0x0A (Erase presentation space)
|
||||
byte[] procOrders = new byte[] { (byte) GocaConstants.P_ERASE };
|
||||
decoder.processProcedureOrders(procOrders, 0, procOrders.length);
|
||||
assertFalse(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSegmentAndRelativeLineOrders() {
|
||||
GraphicsPlane plane = new GraphicsPlane(720, 688);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// 0x70: Begin Segment (14 bytes total)
|
||||
out.write(GocaConstants.G_BEGSEGM);
|
||||
out.write(0x0C);
|
||||
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x01); // Seg ID 1
|
||||
out.write(0x74); out.write(0x70);
|
||||
out.write(0x00); out.write(0x1E); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
|
||||
// 0x3E: End Prologue
|
||||
out.write(GocaConstants.G_ENDPROLOGUE);
|
||||
out.write(0x00);
|
||||
|
||||
// 0x0A: Color (7 = White)
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x07);
|
||||
|
||||
// 0xE1: Relative Line (Absolute Start (300, 200), deltas: (+10, -5), (-10, +5))
|
||||
out.write(GocaConstants.G_GRLINE);
|
||||
out.write(0x08); // 4 bytes start coord + 4 bytes (2 deltas) = 8 bytes
|
||||
out.write(0x01); out.write(0x2C); // Start X = 300
|
||||
out.write(0x00); out.write(0xC8); // Start Y = 200
|
||||
out.write(0x0A); out.write(0xFB); // dx = +10, dy = -5
|
||||
out.write(0xF6); out.write(0x05); // dx = -10, dy = +5
|
||||
|
||||
// 0x71: End Segment
|
||||
out.write(GocaConstants.G_ENDSEGM);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialOrderBufferingAcrossStreams() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// First packet sends G_GLINE order code and half of its coordinates
|
||||
ByteArrayOutputStream chunk1 = new ByteArrayOutputStream();
|
||||
chunk1.write(GocaConstants.G_GLINE);
|
||||
chunk1.write(0x08); // 8 bytes: 2 points
|
||||
chunk1.write(0x00); chunk1.write(0x64); // x1 = 100
|
||||
chunk1.write(0x00); chunk1.write(0x64); // y1 = 100
|
||||
|
||||
byte[] b1 = chunk1.toByteArray();
|
||||
decoder.decodeStream(b1, 0, b1.length);
|
||||
// Order is not complete yet, so no line drawn yet
|
||||
assertFalse(plane.hasContent());
|
||||
|
||||
// Second packet sends the rest of the order: x2 = 200, y2 = 200
|
||||
ByteArrayOutputStream chunk2 = new ByteArrayOutputStream();
|
||||
chunk2.write(0x00); chunk2.write(0xC8); // x2 = 200
|
||||
chunk2.write(0x00); chunk2.write(0xC8); // y2 = 200
|
||||
|
||||
byte[] b2 = chunk2.toByteArray();
|
||||
decoder.decodeStream(b2, 0, b2.length);
|
||||
// Now the combined order executes!
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetCurrentPositionAndRelativeLine() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// 0x21: Set Current Position to (50, 50)
|
||||
out.write(GocaConstants.G_GSCP);
|
||||
out.write(0x04); // len = 4
|
||||
out.write(0x00); out.write(0x32); // x = 50
|
||||
out.write(0x00); out.write(0x32); // y = 50
|
||||
|
||||
// 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10)
|
||||
out.write(GocaConstants.G_GCRLIN);
|
||||
out.write(0x04); // len = 4 (2 deltas * 2 bytes)
|
||||
out.write(0x0A); out.write(0x14); // dx = +10, dy = +20
|
||||
out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
assertEquals(55, decoder.getCurX());
|
||||
assertEquals(60, decoder.getCurY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test3179GCoordinateMapping() {
|
||||
GraphicsPlane plane = new GraphicsPlane(1000, 750);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
|
||||
// Screen center (0, 0) should map to canvas center
|
||||
assertEquals(500, plane.mapX(0));
|
||||
assertEquals(374, plane.mapY(0));
|
||||
|
||||
// Left edge (-xMax) should map to 0
|
||||
assertEquals(0, plane.mapX(-plane.getXMax()));
|
||||
// Right edge (+xMax) should map to 1000
|
||||
assertEquals(1000, plane.mapX(plane.getXMax()));
|
||||
|
||||
// Top edge (+yMax) should map to 0
|
||||
assertEquals(0, plane.mapY(plane.getYMax()));
|
||||
// Bottom edge (yMax - totalHeight) should map to 750
|
||||
assertEquals(750, plane.mapY(plane.getYMax() - plane.getTotalHeight()));
|
||||
|
||||
// Bidirectional roundtrip mapping must be exact
|
||||
assertEquals(0, plane.unmapX(plane.mapX(0)));
|
||||
assertEquals(0, plane.unmapY(plane.mapY(0)));
|
||||
assertEquals(100, plane.unmapY(plane.mapY(100)));
|
||||
assertEquals(-200, plane.unmapY(plane.mapY(-200)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImageOrdersGbimgGimdGeimg() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// G_GBIMG (0xD1): Begin Image: order, len=8, x=100, y=100, w=16, h=2
|
||||
out.write(GocaConstants.G_GBIMG);
|
||||
out.write(0x08);
|
||||
out.write(0x00); out.write(0x64); // x = 100
|
||||
out.write(0x00); out.write(0x64); // y = 100
|
||||
out.write(0x00); out.write(0x10); // w = 16
|
||||
out.write(0x00); out.write(0x02); // h = 2
|
||||
|
||||
// G_GIMD (0x92): Image Data: order, len=4, 4 bytes of bitmap (16x2 pixels = 32 bits = 4 bytes)
|
||||
out.write(GocaConstants.G_GIMD);
|
||||
out.write(0x04);
|
||||
out.write(0xFF); out.write(0x00);
|
||||
out.write(0xAA); out.write(0x55);
|
||||
|
||||
// G_GEIMG (0x91): End Image
|
||||
out.write(GocaConstants.G_GEIMG);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent(), "Expected plane to have content after image decoding");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirectObjectControlSf24() {
|
||||
haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(
|
||||
haus.nightmare.lib3270j.TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
||||
haus.nightmare.lib3270j.datastream.DataStreamProcessor dsp = new haus.nightmare.lib3270j.datastream.DataStreamProcessor(
|
||||
sb, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
||||
|
||||
// First draw something on graphics plane
|
||||
dsp.getGraphicsPlane().setPixel(10, 10, 0xFFFFFFFF);
|
||||
assertTrue(dsp.getGraphicsPlane().hasContent());
|
||||
|
||||
// Send WSF with SF_OBJCNTL (0x24) containing P_ERASE (0x0A)
|
||||
byte[] wsf = new byte[] {
|
||||
(byte) haus.nightmare.lib3270j.protocol.DS3270Constants.CMD_WSF,
|
||||
0x00, 0x04, // Field length = 4
|
||||
(byte) haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL, // 0x24
|
||||
(byte) haus.nightmare.lib3270j.graphics.GocaConstants.P_ERASE // 0x0A
|
||||
};
|
||||
dsp.processRecord(wsf, 0, wsf.length, false);
|
||||
|
||||
assertFalse(dsp.getGraphicsPlane().hasContent(), "Expected graphics plane to be cleared after SF_OBJCNTL P_ERASE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGcallAndGscsWithLengthBytes() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Construct Segment 2: Begin Segment (id=2), Set Color, draw line, End Segment
|
||||
ByteArrayOutputStream s2 = new ByteArrayOutputStream();
|
||||
s2.write(GocaConstants.G_BEGSEGM);
|
||||
s2.write(0x0C);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x02); // id=2
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(GocaConstants.G_GSCS); s2.write(0x01); s2.write(0x40); // charSet = 0x40
|
||||
s2.write(GocaConstants.G_GSCOL); s2.write(0x02); // Color = Red
|
||||
s2.write(GocaConstants.G_GLINE); s2.write(0x08);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(0x00); s2.write(0x32); s2.write(0x00); s2.write(0x32);
|
||||
s2.write(GocaConstants.G_ENDSEGM); s2.write(0x00);
|
||||
|
||||
byte[] s2Bytes = s2.toByteArray();
|
||||
decoder.decodeStream(s2Bytes, 0, s2Bytes.length);
|
||||
|
||||
// Segment 1 calls Segment 2 via GCALL (0x2A len=4 segId=2)
|
||||
ByteArrayOutputStream s1 = new ByteArrayOutputStream();
|
||||
s1.write(GocaConstants.G_BEGSEGM);
|
||||
s1.write(0x0C);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x01); // id=1
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
|
||||
// GCALL (0x2A len=4 segId=2)
|
||||
s1.write(GocaConstants.G_GCALL); s1.write(0x04);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x02);
|
||||
s1.write(GocaConstants.G_ENDSEGM); s1.write(0x00);
|
||||
|
||||
byte[] s1Bytes = s1.toByteArray();
|
||||
decoder.decodeStream(s1Bytes, 0, s1Bytes.length);
|
||||
|
||||
assertEquals(0x40, decoder.getCharSet());
|
||||
assertTrue(plane.hasContent(), "Expected plane to have content after GCALL segment execution");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAntialiasedLineRendering() {
|
||||
GraphicsPlane plane = new GraphicsPlane(100, 100);
|
||||
plane.clear();
|
||||
int red = 0xFFFF0000;
|
||||
|
||||
// Draw a diagonal line with Xiaolin Wu anti-aliasing
|
||||
plane.drawLine(10.0, 10.0, 50.0, 30.0, red, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int y = 0; y < 100; y++) {
|
||||
for (int x = 0; x < 100; x++) {
|
||||
int pixel = buffer[y * 100 + x];
|
||||
if (pixel != 0) {
|
||||
nonZeroPixels++;
|
||||
int alpha = (pixel >>> 24) & 0xFF;
|
||||
int r = (pixel >>> 16) & 0xFF;
|
||||
assertEquals(255, r, "Red channel must be preserved");
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(nonZeroPixels > 30, "Expected non-zero pixels along the line");
|
||||
assertTrue(hasIntermediateAlpha, "Expected Xiaolin Wu anti-aliasing to produce fractional alpha coverage");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubpixelArcAndAlphaBlending() {
|
||||
GraphicsPlane plane = new GraphicsPlane(100, 100);
|
||||
plane.clear();
|
||||
int green = 0xFF00FF00;
|
||||
|
||||
plane.drawArc(50.0, 50.0, 30.0, 30.0, 0.0, 360.0, green, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int p : buffer) {
|
||||
if (p != 0) {
|
||||
nonZeroPixels++;
|
||||
int alpha = (p >>> 24) & 0xFF;
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(nonZeroPixels > 50, "Expected arc pixels");
|
||||
assertTrue(hasIntermediateAlpha, "Expected anti-aliased arc edges with smooth alpha");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVectorTextAntialiasedDrawing() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 100);
|
||||
plane.clear();
|
||||
int yellow = 0xFFFFFF00;
|
||||
|
||||
plane.drawVectorText(10.0, 10.0, "8% 1985 TAX", yellow, 12.0, 20.0, GocaConstants.CD_LR, 0.0);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int p : buffer) {
|
||||
if (p != 0) {
|
||||
nonZeroPixels++;
|
||||
int alpha = (p >>> 24) & 0xFF;
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(nonZeroPixels > 40, "Expected stroked vector text pixels");
|
||||
assertTrue(hasIntermediateAlpha, "Expected anti-aliased vector text strokes with fractional alpha");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTextRendererPrecisionSwitching() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 100);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
final boolean[] textRendererCalled = new boolean[1];
|
||||
plane.setTextRenderer((p, x, y, text, color, cw, ch, dir, angle) -> {
|
||||
textRendererCalled[0] = true;
|
||||
});
|
||||
|
||||
// Test String Precision (Default): GCHST (0xC3) with "TAX"
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GCHST);
|
||||
out.write(0x07); // length = 7 (4 bytes pos + 3 bytes text)
|
||||
out.write(0x00); out.write(0x00); // x = 0
|
||||
out.write(0x00); out.write(0x00); // y = 0
|
||||
out.write(0xE3); // 'T' in EBCDIC
|
||||
out.write(0xC1); // 'A' in EBCDIC
|
||||
out.write(0xE7); // 'X' in EBCDIC
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(textRendererCalled[0], "Expected pluggable TextRenderer to be called for String precision");
|
||||
|
||||
// Now set Stroke precision (G_GSCC with 3):
|
||||
textRendererCalled[0] = false;
|
||||
ByteArrayOutputStream outStroke = new ByteArrayOutputStream();
|
||||
outStroke.write(GocaConstants.G_GSCC);
|
||||
outStroke.write(0x01);
|
||||
outStroke.write(GocaConstants.CP_STROKE); // 3
|
||||
outStroke.write(GocaConstants.G_GCHST);
|
||||
outStroke.write(0x07);
|
||||
outStroke.write(0x00); outStroke.write(0x00);
|
||||
outStroke.write(0x00); outStroke.write(0x00);
|
||||
outStroke.write(0xE3); outStroke.write(0xC1); outStroke.write(0xE7);
|
||||
|
||||
byte[] strokeStream = outStroke.toByteArray();
|
||||
decoder.decodeStream(strokeStream, 0, strokeStream.length);
|
||||
|
||||
assertFalse(textRendererCalled[0], "Expected drawVectorText (not textRenderer) when precision is CP_STROKE");
|
||||
assertTrue(plane.hasContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThickLineRendering() {
|
||||
GraphicsPlane plane = new GraphicsPlane(100, 100);
|
||||
plane.clear();
|
||||
|
||||
plane.drawLine(10.0, 10.0, 50.0, 50.0, 0xFF00FF00, GocaConstants.LT_SOLID, GocaConstants.LW_THICK);
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
int nonZero = 0;
|
||||
for (int p : buffer) {
|
||||
if (p != 0) nonZero++;
|
||||
}
|
||||
assertTrue(nonZero > 60, "Expected thick line to occupy more pixels than standard 1px line");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortFormAttributeOrderParsing() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 200);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Sequence: GBAR (0x68) short form with 0x80 flag (boundary=true, fill=false),
|
||||
// followed immediately by GLINE (0xC1 len=12 for 3 points) and GEAR (0x60)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x80); // Short 2-byte form with 0x80 flag! Must NOT be treated as length=128!
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x0C); // 3 points = 12 bytes
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x32);
|
||||
out.write(0x00); out.write(0x00);
|
||||
out.write(0x00); out.write(0x32);
|
||||
out.write(0x00); out.write(0x32);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent(), "Expected GLINE and GEAR inside GBAR short-form 0x80 to be decoded properly");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiPolygonAreaFilling() {
|
||||
GraphicsPlane plane = new GraphicsPlane(1000, 750);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA)
|
||||
// Polygon 1 (e.g. Letter 'T' bar): (10,10) to (30,10) to (30,20) to (10,20) to (10,10)
|
||||
// Polygon 2 (e.g. Letter 'T' stem): (18,20) to (22,20) to (22,40) to (18,40) to (18,20)
|
||||
// GEAR (0x60)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(GocaConstants.G_GSPT);
|
||||
out.write(GocaConstants.PT_SOLID);
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x80);
|
||||
|
||||
// Polygon 1
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x14); // 5 points = 20 bytes
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(30); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(30); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
|
||||
// Polygon 2 (disconnected start -> triggers new subpath)
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x14); // 5 points = 20 bytes
|
||||
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(22); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(22); out.write(0x00); out.write(40);
|
||||
out.write(0x00); out.write(18); out.write(0x00); out.write(40);
|
||||
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
|
||||
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
out.write(0x00);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
int nonZero = 0;
|
||||
for (int p : buffer) {
|
||||
if (p != 0) nonZero++;
|
||||
}
|
||||
assertTrue(nonZero > 50, "Expected both filled subpath polygons to render filled pixels");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPatternSetAndPatternSymbolSeparation() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 200);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Color to Blue (Color 1)
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x01); // Blue
|
||||
|
||||
// Set Pattern Set to 0 (default standard patterns)
|
||||
out.write(GocaConstants.G_GSPS);
|
||||
out.write(0x00);
|
||||
|
||||
// Set Pattern Symbol to 5 (D5 50% checkerboard)
|
||||
out.write(GocaConstants.G_GSPT);
|
||||
out.write(0x05);
|
||||
|
||||
// Begin Area with boundary (0x80)
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x80);
|
||||
|
||||
// Draw rectangle (0,0) to (50,0) to (50,50) to (0,50) to (0,0)
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x14); // 5 points * 4 bytes = 20 bytes
|
||||
out.write(0x00); out.write(0); out.write(0x00); out.write(0);
|
||||
out.write(0x00); out.write(50); out.write(0x00); out.write(0);
|
||||
out.write(0x00); out.write(50); out.write(0x00); out.write(50);
|
||||
out.write(0x00); out.write(0); out.write(0x00); out.write(50);
|
||||
out.write(0x00); out.write(0); out.write(0x00); out.write(0);
|
||||
|
||||
// End Area
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
// Verify that D5 checkerboard pattern is NOT solid fill (it should have transparent gaps)
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
int bluePixels = 0;
|
||||
int zeroPixels = 0;
|
||||
int blueArgb = GocaConstants.GOCA_COLORS[1];
|
||||
for (int p : buffer) {
|
||||
if (p == blueArgb) bluePixels++;
|
||||
else if (p == 0) zeroPixels++;
|
||||
}
|
||||
assertTrue(bluePixels > 0, "Expected blue pixels in patterned area");
|
||||
assertTrue(zeroPixels > 0, "Expected transparent zero pixels in patterned area due to D5 checkerboard gaps");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyPatternAreaDrawsOnlyBoundary() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 200);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Set Color to Red (Color 2)
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x02);
|
||||
|
||||
// Set Pattern Symbol to PT_EMPTY (15)
|
||||
out.write(GocaConstants.G_GSPT);
|
||||
out.write(GocaConstants.PT_EMPTY);
|
||||
|
||||
// Begin Area with boundary (0x80)
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x80);
|
||||
|
||||
// Draw rectangle
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x14);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(40); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(40); out.write(0x00); out.write(40);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(40);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
|
||||
// End Area
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
// Verify that center of the box is transparent (not filled)
|
||||
int centerX = plane.mapX(25);
|
||||
int centerY = plane.mapY(25);
|
||||
int centerPixel = plane.getRgbBuffer()[centerY * plane.getCanvasWidth() + centerX];
|
||||
assertEquals(0, centerPixel, "Expected interior of PT_EMPTY area to remain transparent");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGearFollowedByNopOrder() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 200);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Area with PT_EMPTY
|
||||
out.write(GocaConstants.G_GSPT);
|
||||
out.write(GocaConstants.PT_EMPTY);
|
||||
out.write(GocaConstants.G_GBAR);
|
||||
out.write(0x80);
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x10);
|
||||
out.write(0x00); out.write(0); out.write(0x00); out.write(0);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(0);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(0); out.write(0x00); out.write(0);
|
||||
out.write(GocaConstants.G_GEAR); // 0x60
|
||||
out.write(GocaConstants.G_NOP1); // 0x00 NOP - must not be consumed as part of GEAR!
|
||||
|
||||
// Followed by a line
|
||||
out.write(GocaConstants.G_GSCOL);
|
||||
out.write(0x03); // Pink
|
||||
out.write(GocaConstants.G_GLINE);
|
||||
out.write(0x08);
|
||||
out.write(0x00); out.write(20); out.write(0x00); out.write(20);
|
||||
out.write(0x00); out.write(40); out.write(0x00); out.write(40);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
int pinkArgb = GocaConstants.GOCA_COLORS[3];
|
||||
boolean foundPink = false;
|
||||
for (int p : plane.getRgbBuffer()) {
|
||||
if (p == pinkArgb) {
|
||||
foundPink = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(foundPink, "Expected pink line after GEAR + NOP to be drawn");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdmopsSlidePreviewSequence() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 200);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
// 1. Draw Blue stippled slide background (Pattern 6, Blue color 1)
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x01); // Blue
|
||||
out.write(GocaConstants.G_GSPT); out.write(0x06); // Pattern 6
|
||||
out.write(GocaConstants.G_GBAR); out.write(0x80);
|
||||
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(90); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(90); out.write(0x00); out.write(90);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(90);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
// 2. Draw Red banner at top (Solid Pattern 16, Red color 2)
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red
|
||||
out.write(GocaConstants.G_GSPT); out.write(0x10); // Solid
|
||||
out.write(GocaConstants.G_GBAR); out.write(0x80);
|
||||
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(90); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(90); out.write(0x00); out.write(30);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(30);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
// 3. Draw Yellow bullet text (Color 6, default modal solid pattern)
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x06); // Yellow
|
||||
out.write(GocaConstants.G_GBAR); out.write(0x80);
|
||||
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
|
||||
out.write(0x00); out.write(20); out.write(0x00); out.write(40);
|
||||
out.write(0x00); out.write(80); out.write(0x00); out.write(40);
|
||||
out.write(0x00); out.write(80); out.write(0x00); out.write(50);
|
||||
out.write(0x00); out.write(20); out.write(0x00); out.write(50);
|
||||
out.write(0x00); out.write(20); out.write(0x00); out.write(40);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
// 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, GSCOL White inside GBAR)
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x08); // Black (background)
|
||||
out.write(GocaConstants.G_GBAR); out.write(0x80);
|
||||
out.write(GocaConstants.G_GSCOL); out.write(0x07); // White line color
|
||||
out.write(GocaConstants.G_GSLT); out.write(GocaConstants.LT_DOT); // Dotted line
|
||||
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(90); out.write(0x00); out.write(10);
|
||||
out.write(0x00); out.write(90); out.write(0x00); out.write(90);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(90);
|
||||
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
byte[] stream = out.toByteArray();
|
||||
decoder.decodeStream(stream, 0, stream.length);
|
||||
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
// Verify that the red banner (e.g. at (50, 20)) was NOT overwritten by black/white!
|
||||
int bannerX = plane.mapX(50);
|
||||
int bannerY = plane.mapY(20);
|
||||
int bannerPixel = plane.getRgbBuffer()[bannerY * plane.getCanvasWidth() + bannerX];
|
||||
int redArgb = GocaConstants.GOCA_COLORS[2];
|
||||
assertEquals(redArgb, bannerPixel, "Red header banner must be solid red and preserved");
|
||||
|
||||
// Verify that the yellow text (e.g. at (50, 45)) was NOT overwritten!
|
||||
int textX = plane.mapX(50);
|
||||
int textY = plane.mapY(45);
|
||||
int textPixel = plane.getRgbBuffer()[textY * plane.getCanvasWidth() + textX];
|
||||
int yellowArgb = GocaConstants.GOCA_COLORS[6];
|
||||
assertEquals(yellowArgb, textPixel, "Yellow bullet text must be solid yellow and preserved");
|
||||
|
||||
// Verify that the blue stipple (in body region (20..80, 60..80)) contains blue pixels and was NOT overwritten by white!
|
||||
int blueArgb = GocaConstants.GOCA_COLORS[1];
|
||||
boolean foundBlue = false;
|
||||
int yMin = Math.min(plane.mapY(60), plane.mapY(80));
|
||||
int yMax = Math.max(plane.mapY(60), plane.mapY(80));
|
||||
int xMin = Math.min(plane.mapX(20), plane.mapX(80));
|
||||
int xMax = Math.max(plane.mapX(20), plane.mapX(80));
|
||||
for (int y = yMin; y <= yMax; y++) {
|
||||
for (int x = xMin; x <= xMax; x++) {
|
||||
int pix = plane.getRgbBuffer()[y * plane.getCanvasWidth() + x];
|
||||
if (pix == blueArgb) {
|
||||
foundBlue = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundBlue) break;
|
||||
}
|
||||
assertTrue(foundBlue, "Slide body must contain stippled blue pixels");
|
||||
|
||||
// Verify that the boundary (e.g. at X=10) is drawn in White!
|
||||
int whiteArgb = GocaConstants.GOCA_COLORS[7];
|
||||
int borderX = plane.mapX(10);
|
||||
boolean foundWhite = false;
|
||||
int y1 = Math.min(plane.mapY(10), plane.mapY(90));
|
||||
int y2 = Math.max(plane.mapY(10), plane.mapY(90));
|
||||
for (int y = y1; y <= y2; y++) {
|
||||
if (plane.getRgbBuffer()[y * plane.getCanvasWidth() + borderX] == whiteArgb) {
|
||||
foundWhite = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(foundWhite, "Slide border outline must be drawn in White");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class GraphicsModeTest {
|
||||
|
||||
@Test
|
||||
public void testDefaultsAndParsing() {
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(null));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString(""));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("NONE"));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("off"));
|
||||
assertEquals(GraphicsMode.NONE, GraphicsMode.fromString("false"));
|
||||
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("ps"));
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("programmed_symbols"));
|
||||
assertEquals(GraphicsMode.PROGRAMMED_SYMBOLS, GraphicsMode.fromString("apl"));
|
||||
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector"));
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("goca"));
|
||||
assertEquals(GraphicsMode.VECTOR_GRAPHICS, GraphicsMode.fromString("vector_graphics"));
|
||||
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("both"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("all"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("true"));
|
||||
assertEquals(GraphicsMode.BOTH, GraphicsMode.fromString("on"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlags() {
|
||||
assertFalse(GraphicsMode.NONE.isProgrammedSymbolsEnabled());
|
||||
assertFalse(GraphicsMode.NONE.isVectorGraphicsEnabled());
|
||||
|
||||
assertTrue(GraphicsMode.PROGRAMMED_SYMBOLS.isProgrammedSymbolsEnabled());
|
||||
assertFalse(GraphicsMode.PROGRAMMED_SYMBOLS.isVectorGraphicsEnabled());
|
||||
|
||||
assertFalse(GraphicsMode.VECTOR_GRAPHICS.isProgrammedSymbolsEnabled());
|
||||
assertTrue(GraphicsMode.VECTOR_GRAPHICS.isVectorGraphicsEnabled());
|
||||
|
||||
assertTrue(GraphicsMode.BOTH.isProgrammedSymbolsEnabled());
|
||||
assertTrue(GraphicsMode.BOTH.isVectorGraphicsEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectionConfigDefault() {
|
||||
ConnectionConfig config = new ConnectionConfig();
|
||||
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(GraphicsMode.NONE);
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(null);
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ProgramSymbolManagerTest {
|
||||
|
||||
@Test
|
||||
public void testSetAllocationAndLookup() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
// Loadable set can be queried after loadps or clearAll
|
||||
assertNull(manager.getSymbolSet(0x40));
|
||||
assertNull(manager.getSymbol(0x40, 0x41));
|
||||
|
||||
manager.clearAll();
|
||||
assertNull(manager.getSymbolSet(0x40));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSinglePlaneLoadPsFormat1() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Build Format 1 Load PS payload for LCID 0x40, code point 0x41 ('A')
|
||||
// Format 1 header:
|
||||
// byte 0: flags (0x01: format 1, single plane)
|
||||
// byte 1: LCID (0x40)
|
||||
// byte 2: Start code point (0x41)
|
||||
// byte 3: RWS (0x02: loadable slot 2)
|
||||
// followed by 18 bytes per symbol for 9x16 cell:
|
||||
// Byte 0-1: Col 0 for rows 0..15 (0x80 = bit for row 0 col 0)
|
||||
// Bytes 2-17: Cols 1..8 for rows 0..15 (0xFF = bits for row 0 cols 1..8)
|
||||
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; // Row 0 Col 0 bit
|
||||
payload[5] = (byte) 0x00; // Rows 8..15 Col 0
|
||||
for (int r = 0; r < 16; r++) {
|
||||
payload[6 + r] = (byte) 0xFF; // Cols 1..8 on all rows
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x40);
|
||||
assertNotNull(set);
|
||||
assertEquals(0x40, set.getLcid());
|
||||
assertFalse(set.isTriplePlane());
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||
assertNotNull(slot);
|
||||
assertEquals(9, slot.getWidth());
|
||||
assertEquals(12, slot.getHeight());
|
||||
assertFalse(slot.isTriplePlane());
|
||||
|
||||
// Verify pixel data (Row 0 Col 0 is 1)
|
||||
byte[] pixels = slot.getPixelData();
|
||||
assertNotNull(pixels);
|
||||
assertEquals(1, pixels[0]); // Row 0 Col 0 is 1
|
||||
|
||||
// Test rendering into 32-bit ARGB pixels
|
||||
int fg = 0xFF00FF00; // Green
|
||||
int bg = 0xFF000000; // Black
|
||||
int[] rgb = slot.getRgbPixels(fg, bg);
|
||||
assertNotNull(rgb);
|
||||
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
|
||||
public void testTriplePlaneMultiColorComposite() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Build Format 1 Load PS payload for LCID 0x42, triple plane
|
||||
// flags = 0x01 (Format 1)
|
||||
// RWS = 4 (Triple Plane)
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x01; // Format 1
|
||||
payload[1] = 0x42; // LCID 0x42
|
||||
payload[2] = 0x45; // Code point 0x45
|
||||
payload[3] = 0x04; // RWS 4 (Triple Plane)
|
||||
|
||||
// Set pixel at row 0 col 0
|
||||
payload[4] = (byte) 0x80; // Row 0 Col 0
|
||||
payload[5] = (byte) 0x00;
|
||||
for (int r = 0; r < 16; r++) {
|
||||
payload[6 + r] = (byte) 0x00;
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x42);
|
||||
assertNotNull(set);
|
||||
assertTrue(set.isTriplePlane());
|
||||
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x42, 0x45);
|
||||
assertNotNull(slot);
|
||||
assertTrue(slot.isTriplePlane());
|
||||
|
||||
// When loaded with colorPlane=0 (default), all 3 planes are set (val 7 = white composite)
|
||||
byte[] pixels = slot.getPixelData();
|
||||
assertEquals(7, pixels[0] & 0xFF);
|
||||
|
||||
// Render ARGB pixels
|
||||
int[] rgb = slot.getRgbPixels(0xFFFFFFFF, 0xFF000000);
|
||||
assertNotNull(rgb);
|
||||
|
||||
// Pixel (0,0) should be white composite (0xFFFFFFFF)
|
||||
int pixelArgb = rgb[0];
|
||||
int r = (pixelArgb >> 16) & 0xFF;
|
||||
int g = (pixelArgb >> 8) & 0xFF;
|
||||
int b = pixelArgb & 0xFF;
|
||||
assertEquals(255, r);
|
||||
assertEquals(255, g);
|
||||
assertEquals(255, b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormat3Bitstream() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
// Format 3 payload
|
||||
// byte 0: 0x03 (Format 3)
|
||||
// byte 1: LCID (0x44)
|
||||
// byte 2: code point (0x43)
|
||||
// byte 3: RWS (2)
|
||||
// followed by (9 * 16 + 7) / 8 = 18 bytes
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x03; // Format 3
|
||||
payload[1] = 0x44; // LCID 0x44
|
||||
payload[2] = 0x43; // Code point 0x43
|
||||
payload[3] = 0x02; // RWS 2
|
||||
payload[4] = (byte) 0x80; // First pixel is set
|
||||
|
||||
manager.loadps(payload);
|
||||
|
||||
ProgramSymbolSet set = manager.getSymbolSet(0x44);
|
||||
assertNotNull(set);
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x44, 0x43);
|
||||
assertNotNull(slot);
|
||||
assertEquals(1, slot.getPixelData()[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package haus.nightmare.lib3270j.input;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class InputProcessorTest {
|
||||
|
||||
private final EbcdicTranslator translator = new EbcdicTranslator();
|
||||
private final ScreenBuffer screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
|
||||
@Test
|
||||
public void testKybdPrimeOnUnformattedScreen() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
int cap = input.kybdPrime();
|
||||
assertEquals(screen.getRows() * screen.getCols(), cap);
|
||||
assertEquals(0, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKybdPrimeOnFormattedScreen() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) FA_PRINTABLE); // Unprotected field at 0 (length = 19)
|
||||
screen.setCellFA(20, (byte) (FA_PRINTABLE | FA_PROTECT)); // Protected field at 20
|
||||
|
||||
int cap = input.kybdPrime();
|
||||
assertEquals(19, cap);
|
||||
assertEquals(1, screen.getCursorAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEraseInput() {
|
||||
InputProcessor input = new InputProcessor(screen, translator, null);
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) FA_PRINTABLE);
|
||||
screen.setCursorAddress(1);
|
||||
input.typeCharacter('A');
|
||||
input.typeCharacter('B');
|
||||
|
||||
assertEquals('A', screen.getCell(1).ucs4);
|
||||
assertEquals('B', screen.getCell(2).ucs4);
|
||||
assertEquals('A', screen.getDisplayCell(1).ucs4);
|
||||
assertEquals('B', screen.getDisplayCell(2).ucs4);
|
||||
|
||||
input.eraseInput();
|
||||
|
||||
assertEquals(0, screen.getCell(1).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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGraphicInputBuilderStructure() {
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
|
||||
assertEquals(56, sf.length);
|
||||
assertEquals(0x00, sf[0]);
|
||||
assertEquals(0x38, sf[1]); // Length = 56 bytes (0x0038)
|
||||
assertEquals(0x0F, sf[2]); // SF ID = 0x0F
|
||||
assertEquals(0x0F, sf[3]); // SF ID = 0x0F
|
||||
|
||||
// Coordinates
|
||||
int x = (sf[24] << 8) | (sf[25] & 0xFF);
|
||||
int y = (sf[26] << 8) | (sf[27] & 0xFF);
|
||||
assertEquals(100, (short) x);
|
||||
assertEquals(-50, (short) y);
|
||||
|
||||
// Keyboard AID
|
||||
assertEquals(0x07, sf[31]);
|
||||
assertEquals(0x07, sf[33]);
|
||||
assertEquals((byte) 0xFF, sf[34]);
|
||||
assertEquals((byte) AID_ENTER, sf[35]);
|
||||
|
||||
// Mouse Button 1 with picked segment 2 and tag 5
|
||||
byte[] sfMouse = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
294, 192, 1, true, false, false, 2, 5
|
||||
);
|
||||
assertEquals(56, sfMouse.length);
|
||||
assertEquals(0x00, sfMouse[0]);
|
||||
assertEquals(0x38, sfMouse[1]);
|
||||
int mx = (sfMouse[24] << 8) | (sfMouse[25] & 0xFF);
|
||||
int my = (sfMouse[26] << 8) | (sfMouse[27] & 0xFF);
|
||||
assertEquals(294, (short) mx);
|
||||
assertEquals(192, (short) my);
|
||||
int segId = ((sfMouse[28] & 0xFF) << 24) | ((sfMouse[29] & 0xFF) << 16) |
|
||||
((sfMouse[30] & 0xFF) << 8) | (sfMouse[31] & 0xFF);
|
||||
assertEquals(2, segId);
|
||||
int tag = ((sfMouse[32] & 0xFF) << 8) | (sfMouse[33] & 0xFF);
|
||||
assertEquals(5, tag);
|
||||
assertEquals(0x00, sfMouse[34]);
|
||||
assertEquals(0x01, sfMouse[35]); // Button 1
|
||||
|
||||
// Mouse Button 2 (Action) with no segment (0)
|
||||
byte[] sfMouse2 = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
0, 0, 2, true, true, false, 0, 0
|
||||
);
|
||||
int segId2 = ((sfMouse2[28] & 0xFF) << 24) | ((sfMouse2[29] & 0xFF) << 16) |
|
||||
((sfMouse2[30] & 0xFF) << 8) | (sfMouse2[31] & 0xFF);
|
||||
assertEquals(0, segId2);
|
||||
int tag2 = ((sfMouse2[32] & 0xFF) << 8) | (sfMouse2[33] & 0xFF);
|
||||
assertEquals(0, tag2);
|
||||
assertEquals((byte) 0x80, sfMouse2[34]); // Shift modifier
|
||||
assertEquals(0x02, sfMouse2[35]); // Button 2
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAidSuppressesNullsInModifiedField() {
|
||||
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||
@Override
|
||||
public void sendAid(int aidCode) {
|
||||
// Call super logic by hooking via fake FSM or overriding sendAid
|
||||
super.sendAid(aidCode);
|
||||
}
|
||||
};
|
||||
// Setup screen: field attribute at 0 (unprotected, modified)
|
||||
screen.erase(false);
|
||||
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
// Pos 1 = null (0x00)
|
||||
screen.getCell(1).ec = 0x00;
|
||||
// Pos 2 = 'Q' (0xD8)
|
||||
screen.getCell(2).ec = (byte) 0xD8;
|
||||
// Pos 3 = 'U' (0xE4)
|
||||
screen.getCell(3).ec = (byte) 0xE4;
|
||||
// Pos 4 = 'I' (0xC9)
|
||||
screen.getCell(4).ec = (byte) 0xC9;
|
||||
// Pos 5 = 'T' (0xE3)
|
||||
screen.getCell(5).ec = (byte) 0xE3;
|
||||
// Next field at 10 (protected)
|
||||
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Use custom callback or inspection
|
||||
// Let's verify DataStreamProcessor ReadModified behavior with the same buffer
|
||||
DataStreamProcessor dsp = new DataStreamProcessor(screen, translator);
|
||||
dsp.setInputProcessor(input);
|
||||
dsp.setOutputSender(sent::set);
|
||||
|
||||
byte[] rmRecord = new byte[] { (byte) CMD_RM };
|
||||
dsp.processRecord(rmRecord, 0, rmRecord.length, true);
|
||||
|
||||
byte[] result = sent.get();
|
||||
assertNotNull(result);
|
||||
// Result format: AID (1 byte), Cursor (2 bytes), SBA (1 byte), Addr (2 bytes), Data ('Q', 'U', 'I', 'T' - 4 bytes)
|
||||
assertEquals(10, result.length, "Result should be AID(1) + Cursor(2) + SBA(1) + Addr(2) + Data(4)");
|
||||
assertEquals((byte) ORDER_SBA, result[3]);
|
||||
assertEquals((byte) 0xD8, result[6]); // 'Q'
|
||||
assertEquals((byte) 0xE4, result[7]); // 'U'
|
||||
assertEquals((byte) 0xC9, result[8]); // 'I'
|
||||
assertEquals((byte) 0xE3, result[9]); // 'T'
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package org.lib3270j.protocol;
|
||||
package haus.nightmare.lib3270j.protocol;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class DS3270ConstantsTest {
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
package org.lib3270j.screen;
|
||||
package haus.nightmare.lib3270j.screen;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lib3270j.TerminalModel;
|
||||
import org.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
public class ScreenBufferTest {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user