Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
2e9b6d325a
|
|||
|
540a8dfd1d
|
|||
|
ebc0b5ae6f
|
|||
|
bcfd4ba2e0
|
|||
|
2584f4289f
|
|||
|
3526e682a1
|
|||
|
7eebd2107c
|
|||
|
36b93df986
|
|||
|
dbe0cdaa89
|
|||
|
b5a4978b24
|
|||
| d2bdb9bb0a | |||
| e28b7dc30a | |||
| d9df1052a1 | |||
| 0837de8db3 | |||
| 42eef519a6 | |||
|
b57d8f960d
|
|||
|
09600260b5
|
|||
|
00c7b0770c
|
@@ -1,4 +1,8 @@
|
|||||||
j3270.log.*
|
j3270.log.*
|
||||||
|
*.txt
|
||||||
|
*.py
|
||||||
|
*.pcap
|
||||||
|
build/*
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.jar
|
*.jar
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
||||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||||
[](https://adoptium.net)
|
[](https://adoptium.net)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E.
|
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E.
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ The project includes self-contained, portable build scripts:
|
|||||||
# Build executable JAR in 2 seconds:
|
# Build executable JAR in 2 seconds:
|
||||||
sh ./build_all.sh
|
sh ./build_all.sh
|
||||||
|
|
||||||
# Run all 56 automated unit tests in ~500ms:
|
# Run all 57 automated unit tests in ~500ms:
|
||||||
sh ./test_all.sh
|
sh ./test_all.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -90,12 +90,6 @@ The resulting standalone JAR is created at `build/j3270.jar`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📜 Acknowledgements
|
## 📜 Acknowledgements
|
||||||
|
|
||||||
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|
||||||
+3
-1
@@ -1,5 +1,5 @@
|
|||||||
allprojects {
|
allprojects {
|
||||||
group = 'org.j3270'
|
group = 'haus.nightmare.j3270'
|
||||||
version = '0.1.0'
|
version = '0.1.0'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -10,8 +10,10 @@ allprojects {
|
|||||||
subprojects {
|
subprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
|
|
||||||
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
|||||||
@@ -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
|
# 4. Package executable JAR
|
||||||
echo "Packaging j3270.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" \
|
"$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
|
||||||
-C "$BUILD_DIR/lib3270j" . \
|
-C "$BUILD_DIR/lib3270j" . \
|
||||||
@@ -87,3 +87,60 @@ echo "=== Build Complete ==="
|
|||||||
echo "Executable JAR created: $BUILD_DIR/j3270.jar"
|
echo "Executable JAR created: $BUILD_DIR/j3270.jar"
|
||||||
ls -lh "$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
|
#!/bin/sh
|
||||||
exec "$(dirname "$0")/build_all.sh"
|
exec "$(dirname "$0")/build_all.sh" "$@"
|
||||||
|
|||||||
+2
-2
@@ -7,12 +7,12 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass = 'org.pubvm.j3270.J3270App'
|
mainClass = 'haus.nightmare.j3270.J3270App'
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': 'org.pubvm.j3270.J3270App',
|
attributes 'Main-Class': 'haus.nightmare.j3270.J3270App',
|
||||||
'Implementation-Title': 'j3270',
|
'Implementation-Title': 'j3270',
|
||||||
'Implementation-Version': archiveVersion
|
'Implementation-Version': archiveVersion
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-45
@@ -1,13 +1,13 @@
|
|||||||
package org.pubvm.j3270;
|
package haus.nightmare.j3270;
|
||||||
|
|
||||||
import org.lib3270j.*;
|
import haus.nightmare.lib3270j.*;
|
||||||
import org.lib3270j.listener.ConnectionListener;
|
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||||
import org.lib3270j.listener.ScreenUpdateListener;
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||||
import org.pubvm.j3270.ui.ConnectDialog;
|
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||||
import org.pubvm.j3270.ui.StatusBar;
|
import haus.nightmare.j3270.ui.StatusBar;
|
||||||
import org.pubvm.j3270.ui.TerminalPanel;
|
import haus.nightmare.j3270.ui.TerminalPanel;
|
||||||
import org.pubvm.j3270.ft.FileTransfer;
|
import haus.nightmare.j3270.ft.FileTransfer;
|
||||||
import org.pubvm.j3270.ft.FileTransferDialog;
|
import haus.nightmare.j3270.ft.FileTransferDialog;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@@ -120,7 +120,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
// Actions menu
|
// Actions menu
|
||||||
JMenu actionsMenu = createMenu("Actions");
|
JMenu actionsMenu = createMenu("Actions");
|
||||||
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_L, () -> {
|
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
|
||||||
if (client != null)
|
if (client != null)
|
||||||
client.sendClear();
|
client.sendClear();
|
||||||
terminalPanel.repaint();
|
terminalPanel.repaint();
|
||||||
@@ -131,6 +131,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
terminalPanel.repaint();
|
terminalPanel.repaint();
|
||||||
}));
|
}));
|
||||||
actionsMenu.addSeparator();
|
actionsMenu.addSeparator();
|
||||||
|
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
|
||||||
|
terminalPanel.toggleLightPen();
|
||||||
|
}, true));
|
||||||
|
actionsMenu.addSeparator();
|
||||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
||||||
menuBar.add(actionsMenu);
|
menuBar.add(actionsMenu);
|
||||||
|
|
||||||
@@ -149,18 +153,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) {
|
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
|
||||||
JMenuItem item = new JMenuItem(text);
|
JMenuItem item = new JMenuItem(name);
|
||||||
item.setBackground(new Color(40, 40, 40));
|
item.setBackground(new Color(40, 40, 40));
|
||||||
item.setForeground(new Color(200, 200, 200));
|
item.setForeground(new Color(200, 200, 200));
|
||||||
if (acceleratorKey > 0) {
|
if (mnemonic > 0) {
|
||||||
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey,
|
int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||||
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
|
item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
|
||||||
}
|
}
|
||||||
item.addActionListener(e -> action.run());
|
item.addActionListener(e -> action.run());
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action) {
|
||||||
|
return createMenuItem(name, mnemonic, action, false);
|
||||||
|
}
|
||||||
|
|
||||||
private void showFileTransferDialog() {
|
private void showFileTransferDialog() {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
||||||
@@ -228,12 +236,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
private void showConnectDialog() {
|
private void showConnectDialog() {
|
||||||
ConnectDialog dialog = new ConnectDialog(this);
|
ConnectDialog dialog = new ConnectDialog(this);
|
||||||
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : org.pubvm.j3270.config.Settings.getAutoConnectHost();
|
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||||
int initialPort = lastPort > 0 ? lastPort : org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||||
dialog.setInitialHost(initialHost);
|
dialog.setInitialHost(initialHost);
|
||||||
dialog.setInitialPort(initialPort);
|
dialog.setInitialPort(initialPort);
|
||||||
dialog.setInitialTls(org.pubvm.j3270.config.Settings.getAutoConnectTls());
|
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
||||||
dialog.setInitialVerifyCert(org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert());
|
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
||||||
dialog.setVisible(true);
|
dialog.setVisible(true);
|
||||||
|
|
||||||
if (dialog.isConfirmed()) {
|
if (dialog.isConfirmed()) {
|
||||||
@@ -245,7 +253,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showSettingsDialog() {
|
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);
|
dialog.setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,10 +266,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
lastPort = config.getPort();
|
lastPort = config.getPort();
|
||||||
|
|
||||||
// Save for auto-connect
|
// Save for auto-connect
|
||||||
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||||
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||||
org.pubvm.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||||
org.pubvm.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||||
|
|
||||||
// Disconnect existing connection
|
// Disconnect existing connection
|
||||||
disconnect();
|
disconnect();
|
||||||
@@ -269,7 +277,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
// Wire interactive certificate verifier if none configured
|
// Wire interactive certificate verifier if none configured
|
||||||
if (config.getCertificateVerifier() == null) {
|
if (config.getCertificateVerifier() == null) {
|
||||||
config.setCertificateVerifier((chain, authType, exception) ->
|
config.setCertificateVerifier((chain, authType, exception) ->
|
||||||
org.pubvm.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
|
haus.nightmare.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +286,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
client.addScreenUpdateListener(this);
|
client.addScreenUpdateListener(this);
|
||||||
|
|
||||||
terminalPanel.setClient(client);
|
terminalPanel.setClient(client);
|
||||||
statusBar.setClient(client);
|
statusBar.setClient(client, terminalPanel);
|
||||||
|
|
||||||
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
||||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
||||||
@@ -312,7 +320,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
client.disconnect();
|
client.disconnect();
|
||||||
client = null;
|
client = null;
|
||||||
terminalPanel.setClient(null);
|
terminalPanel.setClient(null);
|
||||||
statusBar.setClient(null);
|
statusBar.setClient(null, terminalPanel);
|
||||||
terminalPanel.repaint();
|
terminalPanel.repaint();
|
||||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||||
}
|
}
|
||||||
@@ -447,7 +455,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
boolean cliTls = false;
|
boolean cliTls = false;
|
||||||
boolean cliNoVerifyCert = false;
|
boolean cliNoVerifyCert = false;
|
||||||
Boolean cliTn3270e = null;
|
Boolean cliTn3270e = null;
|
||||||
org.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
haus.nightmare.lib3270j.graphics.GraphicsMode cliGraphicsMode = null;
|
||||||
String configFile = null;
|
String configFile = null;
|
||||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
@@ -462,11 +470,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
} else if ("--tn3270e".equals(args[i])) {
|
} else if ("--tn3270e".equals(args[i])) {
|
||||||
cliTn3270e = true;
|
cliTn3270e = true;
|
||||||
} else if (args[i].startsWith("--graphics=")) {
|
} else if (args[i].startsWith("--graphics=")) {
|
||||||
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[i].substring(11));
|
cliGraphicsMode = 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("-")) {
|
} else if (("--graphics".equals(args[i]) || "-g".equals(args[i])) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||||
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
|
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(args[++i]);
|
||||||
} else if ("--no-graphics".equals(args[i])) {
|
} else if ("--no-graphics".equals(args[i])) {
|
||||||
cliGraphicsMode = org.lib3270j.graphics.GraphicsMode.NONE;
|
cliGraphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
|
||||||
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
||||||
configFile = args[++i];
|
configFile = args[++i];
|
||||||
} else {
|
} else {
|
||||||
@@ -484,7 +492,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
java.util.logging.Filter appFilter = record -> {
|
java.util.logging.Filter appFilter = record -> {
|
||||||
String name = record.getLoggerName();
|
String name = record.getLoggerName();
|
||||||
return name != null && (name.startsWith("org.lib3270j") || name.startsWith("org.pubvm.j3270"));
|
return name != null && (name.startsWith("haus.nightmare.lib3270j") || name.startsWith("haus.nightmare.j3270"));
|
||||||
};
|
};
|
||||||
|
|
||||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||||
@@ -493,9 +501,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
consoleHandler.setFilter(appFilter);
|
consoleHandler.setFilter(appFilter);
|
||||||
globalRoot.addHandler(consoleHandler);
|
globalRoot.addHandler(consoleHandler);
|
||||||
|
|
||||||
Logger.getLogger("org.pubvm").setLevel(logLevel);
|
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||||
Logger.getLogger("org.pubvm.j3270").setLevel(logLevel);
|
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||||
Logger.getLogger("org.lib3270j").setLevel(logLevel);
|
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||||
@@ -517,7 +525,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
// Load INI config file if specified
|
// Load INI config file if specified
|
||||||
if (configFile != null) {
|
if (configFile != null) {
|
||||||
try {
|
try {
|
||||||
org.pubvm.j3270.config.Settings.loadFromIniFile(configFile);
|
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("Failed to load config file: " + configFile);
|
System.err.println("Failed to load config file: " + configFile);
|
||||||
System.err.println(e.getMessage());
|
System.err.println(e.getMessage());
|
||||||
@@ -539,7 +547,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
final boolean finalTls = cliTls;
|
final boolean finalTls = cliTls;
|
||||||
final boolean finalNoVerify = cliNoVerifyCert;
|
final boolean finalNoVerify = cliNoVerifyCert;
|
||||||
final Boolean finalTn3270e = cliTn3270e;
|
final Boolean finalTn3270e = cliTn3270e;
|
||||||
final org.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
final haus.nightmare.lib3270j.graphics.GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
J3270App app = new J3270App();
|
J3270App app = new J3270App();
|
||||||
@@ -576,22 +584,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
if (finalGraphicsMode != null) {
|
if (finalGraphicsMode != null) {
|
||||||
config.setGraphicsMode(finalGraphicsMode);
|
config.setGraphicsMode(finalGraphicsMode);
|
||||||
} else {
|
} else {
|
||||||
config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode());
|
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||||
}
|
}
|
||||||
app.connect(config);
|
app.connect(config);
|
||||||
} else {
|
} 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();
|
.getStartupBehavior();
|
||||||
switch (behavior) {
|
switch (behavior) {
|
||||||
case SHOW_CONNECT:
|
case SHOW_CONNECT:
|
||||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||||
break;
|
break;
|
||||||
case AUTO_CONNECT:
|
case AUTO_CONNECT:
|
||||||
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
|
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||||
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||||
boolean tls = org.pubvm.j3270.config.Settings.getAutoConnectTls();
|
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
|
||||||
boolean verify = org.pubvm.j3270.config.Settings.getAutoConnectVerifyCert();
|
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert();
|
||||||
boolean tn3270e = org.pubvm.j3270.config.Settings.getAutoConnectTn3270e();
|
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e();
|
||||||
if (host != null && !host.isEmpty()) {
|
if (host != null && !host.isEmpty()) {
|
||||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||||
config.setTlsVerifyCert(verify);
|
config.setTlsVerifyCert(verify);
|
||||||
@@ -599,7 +607,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
if (finalGraphicsMode != null) {
|
if (finalGraphicsMode != null) {
|
||||||
config.setGraphicsMode(finalGraphicsMode);
|
config.setGraphicsMode(finalGraphicsMode);
|
||||||
} else {
|
} else {
|
||||||
config.setGraphicsMode(org.pubvm.j3270.config.Settings.getGraphicsMode());
|
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||||
}
|
}
|
||||||
app.connect(config);
|
app.connect(config);
|
||||||
} else {
|
} else {
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package org.pubvm.j3270.config;
|
package haus.nightmare.j3270.config;
|
||||||
|
|
||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
@@ -86,13 +86,13 @@ public class Settings {
|
|||||||
prefs.putBoolean("autoConnectTn3270e", tn3270e);
|
prefs.putBoolean("autoConnectTn3270e", tn3270e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static org.lib3270j.graphics.GraphicsMode getGraphicsMode() {
|
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
|
||||||
String modeStr = prefs.get("graphicsMode", org.lib3270j.graphics.GraphicsMode.BOTH.name());
|
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
|
||||||
return org.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
|
public static void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||||
prefs.put("graphicsMode", (mode != null ? mode : org.lib3270j.graphics.GraphicsMode.BOTH).name());
|
prefs.put("graphicsMode", (mode != null ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH).name());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color getColorOverride(int index, Color defaultColor) {
|
public static Color getColorOverride(int index, Color defaultColor) {
|
||||||
@@ -280,7 +280,7 @@ public class Settings {
|
|||||||
|
|
||||||
case "graphics":
|
case "graphics":
|
||||||
if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) {
|
if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) {
|
||||||
setGraphicsMode(org.lib3270j.graphics.GraphicsMode.fromString(value));
|
setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(value));
|
||||||
} else {
|
} else {
|
||||||
log.warning("Unknown graphics key: " + key);
|
log.warning("Unknown graphics key: " + key);
|
||||||
}
|
}
|
||||||
+21
-6
@@ -1,10 +1,10 @@
|
|||||||
package org.pubvm.j3270.ft;
|
package haus.nightmare.j3270.ft;
|
||||||
|
|
||||||
import org.lib3270j.Telnet3270Client;
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
import org.lib3270j.ft.FTConfig;
|
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||||
import org.lib3270j.ft.FTConstants.FTState;
|
import haus.nightmare.lib3270j.ft.FTConstants.FTState;
|
||||||
import org.lib3270j.ft.FTCut;
|
import haus.nightmare.lib3270j.ft.FTCut;
|
||||||
import org.lib3270j.ft.FTDft;
|
import haus.nightmare.lib3270j.ft.FTDft;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -121,6 +121,21 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
|||||||
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
|
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
|
||||||
log.info("User cancelled transfer");
|
log.info("User cancelled transfer");
|
||||||
setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance
|
setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance
|
||||||
|
// Start a safety timeout in case the host does not send further packets to trigger abort
|
||||||
|
cancelTimeout();
|
||||||
|
timeoutTimer = new Timer("FTCancelTimeout", true);
|
||||||
|
timeoutTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
if (state != FTState.NONE) {
|
||||||
|
log.warning("Transfer cancel timeout — force resetting state");
|
||||||
|
completeTransfer("Transfer cancelled by user.");
|
||||||
|
callback.onTransferAborted("Cancelled by user.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
} else if (state != FTState.NONE) {
|
} else if (state != FTState.NONE) {
|
||||||
log.info("Forcing cancel from state " + state);
|
log.info("Forcing cancel from state " + state);
|
||||||
completeTransfer("Transfer cancelled.");
|
completeTransfer("Transfer cancelled.");
|
||||||
+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.*;
|
||||||
import javax.swing.border.EmptyBorder;
|
import javax.swing.border.EmptyBorder;
|
||||||
+8
-8
@@ -1,7 +1,7 @@
|
|||||||
package org.pubvm.j3270.ui;
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
import org.lib3270j.ConnectionConfig;
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
import org.lib3270j.TerminalModel;
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@@ -14,7 +14,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
private JTextField hostField;
|
private JTextField hostField;
|
||||||
private JTextField portField;
|
private JTextField portField;
|
||||||
private JComboBox<TerminalModel> modelCombo;
|
private JComboBox<TerminalModel> modelCombo;
|
||||||
private JComboBox<org.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
||||||
private JTextField luField;
|
private JTextField luField;
|
||||||
private JCheckBox tlsCheckBox;
|
private JCheckBox tlsCheckBox;
|
||||||
private JCheckBox verifyCertCheckBox;
|
private JCheckBox verifyCertCheckBox;
|
||||||
@@ -107,8 +107,8 @@ public class ConnectDialog extends JDialog {
|
|||||||
mainPanel.add(graphicsLabel, gbc);
|
mainPanel.add(graphicsLabel, gbc);
|
||||||
gbc.gridx = 1;
|
gbc.gridx = 1;
|
||||||
gbc.weightx = 1.0;
|
gbc.weightx = 1.0;
|
||||||
graphicsCombo = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values());
|
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||||
graphicsCombo.setSelectedItem(org.pubvm.j3270.config.Settings.getGraphicsMode());
|
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||||
graphicsCombo.setBackground(new Color(45, 45, 45));
|
graphicsCombo.setBackground(new Color(45, 45, 45));
|
||||||
graphicsCombo.setForeground(fg);
|
graphicsCombo.setForeground(fg);
|
||||||
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||||
@@ -154,7 +154,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
|
tn3270eCheckBox.setBackground(new Color(30, 30, 30));
|
||||||
tn3270eCheckBox.setForeground(fg);
|
tn3270eCheckBox.setForeground(fg);
|
||||||
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||||
tn3270eCheckBox.setSelected(org.pubvm.j3270.config.Settings.getAutoConnectTn3270e());
|
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
||||||
tn3270eCheckBox.setFocusPainted(false);
|
tn3270eCheckBox.setFocusPainted(false);
|
||||||
mainPanel.add(tn3270eCheckBox, gbc);
|
mainPanel.add(tn3270eCheckBox, gbc);
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
if (!lu.isEmpty()) {
|
if (!lu.isEmpty()) {
|
||||||
result.setLuName(lu);
|
result.setLuName(lu);
|
||||||
}
|
}
|
||||||
result.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
||||||
result.setUseTls(tlsCheckBox.isSelected());
|
result.setUseTls(tlsCheckBox.isSelected());
|
||||||
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
||||||
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
|
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
|
||||||
+24
-16
@@ -1,9 +1,9 @@
|
|||||||
package org.pubvm.j3270.ui;
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
|
||||||
import org.pubvm.j3270.J3270App;
|
import haus.nightmare.j3270.J3270App;
|
||||||
import org.pubvm.j3270.config.Settings;
|
import haus.nightmare.j3270.config.Settings;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.*;
|
import java.awt.event.*;
|
||||||
@@ -31,7 +31,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
// Appearance tab
|
// Appearance tab
|
||||||
private JComboBox<String> fontBox;
|
private JComboBox<String> fontBox;
|
||||||
private JSpinner fontSizeSpinner;
|
private JSpinner fontSizeSpinner;
|
||||||
private JComboBox<org.lib3270j.graphics.GraphicsMode> graphicsModeBox;
|
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
|
||||||
|
|
||||||
// Behavior tab
|
// Behavior tab
|
||||||
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
||||||
@@ -272,7 +272,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
JLabel graphicsLabel = new JLabel("Graphics Mode:");
|
JLabel graphicsLabel = new JLabel("Graphics Mode:");
|
||||||
panel.add(graphicsLabel, gbc);
|
panel.add(graphicsLabel, gbc);
|
||||||
|
|
||||||
graphicsModeBox = new JComboBox<>(org.lib3270j.graphics.GraphicsMode.values());
|
graphicsModeBox = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||||
graphicsModeBox.setSelectedItem(Settings.getGraphicsMode());
|
graphicsModeBox.setSelectedItem(Settings.getGraphicsMode());
|
||||||
gbc.gridx = 1;
|
gbc.gridx = 1;
|
||||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
@@ -370,7 +370,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors"));
|
hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors"));
|
||||||
|
|
||||||
for (int i = 0; i < 16; i++) {
|
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;
|
tempHostColors[i] = init;
|
||||||
JPanel cb = new JPanel();
|
JPanel cb = new JPanel();
|
||||||
cb.setName("colorSwatch");
|
cb.setName("colorSwatch");
|
||||||
@@ -402,7 +402,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
|
|
||||||
for (int i = 0; i < monoKeys.length; i++) {
|
for (int i = 0; i < monoKeys.length; i++) {
|
||||||
String mk = monoKeys[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);
|
tempMonoColors.put(mk, init);
|
||||||
|
|
||||||
JPanel p = new JPanel(new BorderLayout());
|
JPanel p = new JPanel(new BorderLayout());
|
||||||
@@ -453,7 +453,8 @@ public class SettingsDialog extends JDialog {
|
|||||||
JPanel main = new JPanel(new BorderLayout());
|
JPanel main = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||||
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"};
|
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR",
|
||||||
|
"ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL"};
|
||||||
|
|
||||||
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
|
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
|
||||||
@Override
|
@Override
|
||||||
@@ -465,8 +466,15 @@ public class SettingsDialog extends JDialog {
|
|||||||
// Populate table from Settings or Defaults
|
// Populate table from Settings or Defaults
|
||||||
for(String act : actions) {
|
for(String act : actions) {
|
||||||
String def = act;
|
String def = act;
|
||||||
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example
|
if (def.equals("ERASE_INPUT")) def = "alt E";
|
||||||
String current = org.pubvm.j3270.config.Settings.getKeyBinding(act, def);
|
else if (def.equals("NEWLINE")) def = "shift ENTER";
|
||||||
|
else if (def.equals("DUP")) def = "alt D";
|
||||||
|
else if (def.equals("FIELD_MARK")) def = "alt M";
|
||||||
|
else if (def.equals("ATTN")) def = "alt A";
|
||||||
|
else if (def.equals("SYSREQ")) def = "alt S";
|
||||||
|
else if (def.equals("CURSEL")) def = "alt Q";
|
||||||
|
else if (def.equals("CLEAR")) def = "alt C";
|
||||||
|
String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def);
|
||||||
tempKeyBindings.put(act, current);
|
tempKeyBindings.put(act, current);
|
||||||
keymapModel.addRow(new Object[]{act, current});
|
keymapModel.addRow(new Object[]{act, current});
|
||||||
}
|
}
|
||||||
@@ -478,13 +486,13 @@ public class SettingsDialog extends JDialog {
|
|||||||
defaultBinding = "shift F" + (i - 12);
|
defaultBinding = "shift F" + (i - 12);
|
||||||
}
|
}
|
||||||
String pf = "PF"+i;
|
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);
|
tempKeyBindings.put(pf, cur);
|
||||||
keymapModel.addRow(new Object[]{pf, cur});
|
keymapModel.addRow(new Object[]{pf, cur});
|
||||||
}
|
}
|
||||||
for(int i=1;i<=3;i++){
|
for(int i=1;i<=3;i++){
|
||||||
String pa = "PA"+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);
|
tempKeyBindings.put(pa, curPa);
|
||||||
keymapModel.addRow(new Object[]{pa, curPa});
|
keymapModel.addRow(new Object[]{pa, curPa});
|
||||||
}
|
}
|
||||||
@@ -626,7 +634,7 @@ public class SettingsDialog extends JDialog {
|
|||||||
}
|
}
|
||||||
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
|
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
|
||||||
if (graphicsModeBox.getSelectedItem() != null) {
|
if (graphicsModeBox.getSelectedItem() != null) {
|
||||||
Settings.setGraphicsMode((org.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem());
|
Settings.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply Behavior
|
// Apply Behavior
|
||||||
@@ -648,15 +656,15 @@ public class SettingsDialog extends JDialog {
|
|||||||
// Propagate visual changes to the app
|
// Propagate visual changes to the app
|
||||||
// Save Colors
|
// Save Colors
|
||||||
for (int i=0; i<16; i++) {
|
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()) {
|
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
|
// Save Keybindings
|
||||||
for (Map.Entry<String, String> entry : tempKeyBindings.entrySet()) {
|
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();
|
parentApp.getTerminalPanel().reloadSettings();
|
||||||
+33
-5
@@ -1,8 +1,8 @@
|
|||||||
package org.pubvm.j3270.ui;
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
import org.lib3270j.ConnectionState;
|
import haus.nightmare.lib3270j.ConnectionState;
|
||||||
import org.lib3270j.Telnet3270Client;
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@@ -20,8 +20,10 @@ public class StatusBar extends JPanel {
|
|||||||
private final JLabel luName;
|
private final JLabel luName;
|
||||||
private final JLabel lockStatus;
|
private final JLabel lockStatus;
|
||||||
private final JLabel modelInfo;
|
private final JLabel modelInfo;
|
||||||
|
private final JButton lpButton;
|
||||||
|
|
||||||
private Telnet3270Client client;
|
private Telnet3270Client client;
|
||||||
|
private TerminalPanel terminalPanel;
|
||||||
|
|
||||||
// OIA colors
|
// OIA colors
|
||||||
private static final Color OIA_BG = new Color(20, 20, 20);
|
private static final Color OIA_BG = new Color(20, 20, 20);
|
||||||
@@ -44,6 +46,19 @@ public class StatusBar extends JPanel {
|
|||||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||||
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
|
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
|
||||||
|
|
||||||
|
lpButton = new JButton("LightPen: OFF");
|
||||||
|
lpButton.setFont(oiaFont);
|
||||||
|
lpButton.setForeground(OIA_FG);
|
||||||
|
lpButton.setBackground(OIA_BG);
|
||||||
|
lpButton.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
|
||||||
|
lpButton.setFocusable(false);
|
||||||
|
lpButton.addActionListener(e -> {
|
||||||
|
if (terminalPanel != null) {
|
||||||
|
terminalPanel.toggleLightPen();
|
||||||
|
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
add(Box.createHorizontalStrut(6));
|
add(Box.createHorizontalStrut(6));
|
||||||
add(connectionStatus);
|
add(connectionStatus);
|
||||||
add(Box.createHorizontalStrut(10));
|
add(Box.createHorizontalStrut(10));
|
||||||
@@ -52,6 +67,8 @@ public class StatusBar extends JPanel {
|
|||||||
add(luName);
|
add(luName);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(12));
|
||||||
add(lockStatus);
|
add(lockStatus);
|
||||||
|
add(Box.createHorizontalStrut(12));
|
||||||
|
add(lpButton);
|
||||||
add(Box.createHorizontalGlue());
|
add(Box.createHorizontalGlue());
|
||||||
add(modelInfo);
|
add(modelInfo);
|
||||||
add(Box.createHorizontalStrut(12));
|
add(Box.createHorizontalStrut(12));
|
||||||
@@ -66,8 +83,13 @@ public class StatusBar extends JPanel {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClient(Telnet3270Client client) {
|
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
this.terminalPanel = terminalPanel;
|
||||||
|
if (terminalPanel != null) {
|
||||||
|
terminalPanel.setOnLightPenToggle(this::updateStatus);
|
||||||
|
}
|
||||||
|
updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateStatus() {
|
public void updateStatus() {
|
||||||
@@ -160,5 +182,11 @@ public class StatusBar extends JPanel {
|
|||||||
int row = sb.getCursorRow() + 1;
|
int row = sb.getCursorRow() + 1;
|
||||||
int col = sb.getCursorCol() + 1;
|
int col = sb.getCursorCol() + 1;
|
||||||
cursorPosition.setText(String.format("%03d/%03d", row, col));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+332
-64
@@ -1,15 +1,16 @@
|
|||||||
package org.pubvm.j3270.ui;
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
import org.lib3270j.Telnet3270Client;
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
import org.lib3270j.screen.ExtendedAttribute;
|
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.datatransfer.*;
|
import java.awt.datatransfer.*;
|
||||||
import java.awt.event.*;
|
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.
|
* Custom JPanel that renders the 3270 screen buffer.
|
||||||
@@ -62,6 +63,9 @@ public class TerminalPanel extends JPanel {
|
|||||||
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
|
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
|
||||||
|
|
||||||
// ========== Resize guard ==========
|
// ========== Resize guard ==========
|
||||||
|
private boolean lightPenMode = false;
|
||||||
|
private long lastGraphicsUpdateCount = -1;
|
||||||
|
private java.awt.image.BufferedImage cachedGraphicsImage = null;
|
||||||
private boolean resizeGuard = false;
|
private boolean resizeGuard = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,7 +74,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
*/
|
*/
|
||||||
private int getRenderOffsetX() {
|
private int getRenderOffsetX() {
|
||||||
int termCols = 80;
|
int termCols = 80;
|
||||||
if (client != null) termCols = client.getScreenBuffer().getCols();
|
if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
|
||||||
int gridW = termCols * cellWidth;
|
int gridW = termCols * cellWidth;
|
||||||
int extra = getWidth() - gridW;
|
int extra = getWidth() - gridW;
|
||||||
return Math.max(padding, extra / 2);
|
return Math.max(padding, extra / 2);
|
||||||
@@ -78,10 +82,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the vertical render offset to center the grid within the panel.
|
* Compute the vertical render offset to center the grid within the panel.
|
||||||
|
* Must use getDisplayRows() to match paintComponent's iteration.
|
||||||
*/
|
*/
|
||||||
private int getRenderOffsetY() {
|
private int getRenderOffsetY() {
|
||||||
int termRows = 24;
|
int termRows = 24;
|
||||||
if (client != null) termRows = client.getScreenBuffer().getRows();
|
if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
|
||||||
int gridH = termRows * cellHeight;
|
int gridH = termRows * cellHeight;
|
||||||
int extra = getHeight() - gridH;
|
int extra = getHeight() - gridH;
|
||||||
return Math.max(padding, extra / 2);
|
return Math.max(padding, extra / 2);
|
||||||
@@ -130,7 +135,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
setupFont();
|
setupFont();
|
||||||
setupCursorBlink();
|
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
|
// Handle mouse clicks to position cursor, selection, and grab focus
|
||||||
MouseAdapter mouseHandler = new MouseAdapter() {
|
MouseAdapter mouseHandler = new MouseAdapter() {
|
||||||
@@ -148,16 +153,37 @@ public class TerminalPanel extends JPanel {
|
|||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
int ox = getRenderOffsetX();
|
int ox = getRenderOffsetX();
|
||||||
int oy = getRenderOffsetY();
|
int oy = getRenderOffsetY();
|
||||||
|
int displayCols = sb.getDisplayCols();
|
||||||
|
int displayRows = sb.getDisplayRows();
|
||||||
int col = (e.getX() - ox) / cellWidth;
|
int col = (e.getX() - ox) / cellWidth;
|
||||||
int row = (e.getY() - oy) / cellHeight;
|
int row = (e.getY() - oy) / cellHeight;
|
||||||
col = Math.max(0, Math.min(col, sb.getCols() - 1));
|
col = Math.max(0, Math.min(col, displayCols - 1));
|
||||||
row = Math.max(0, Math.min(row, sb.getRows() - 1));
|
row = Math.max(0, Math.min(row, displayRows - 1));
|
||||||
|
|
||||||
// Start selection
|
// 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 / Selectable Field mode (per IBM Host On-Demand MouseMgr.java / PS3270.java),
|
||||||
|
// clicks on detectable fields (faIsSelectable) trigger field selection or cursor select.
|
||||||
|
// 3. In standard alphanumeric mode, click-drag selects text for copy-paste.
|
||||||
|
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
||||||
|
boolean isSelectableField = false;
|
||||||
|
if (sb.isFormatted()) {
|
||||||
|
int faPos = sb.findFieldAttribute(row * displayCols + col);
|
||||||
|
if (faPos >= 0) {
|
||||||
|
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||||
|
isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||||
|
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isGraphic || lightPenMode || isSelectableField) {
|
||||||
|
clearSelection();
|
||||||
|
} else {
|
||||||
selectionStartRow = row;
|
selectionStartRow = row;
|
||||||
selectionStartCol = col;
|
selectionStartCol = col;
|
||||||
selectionEndRow = row;
|
selectionEndRow = row;
|
||||||
selectionEndCol = col;
|
selectionEndCol = col;
|
||||||
|
}
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
@@ -166,30 +192,137 @@ public class TerminalPanel extends JPanel {
|
|||||||
@Override
|
@Override
|
||||||
public void mouseDragged(MouseEvent e) {
|
public void mouseDragged(MouseEvent e) {
|
||||||
if (isDragging && client != null && client.getConnectionState().isFullSession()) {
|
if (isDragging && client != null && client.getConnectionState().isFullSession()) {
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
|
||||||
int ox = getRenderOffsetX();
|
int ox = getRenderOffsetX();
|
||||||
int oy = getRenderOffsetY();
|
int oy = getRenderOffsetY();
|
||||||
|
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 col = (e.getX() - ox) / cellWidth;
|
||||||
int row = (e.getY() - oy) / cellHeight;
|
int row = (e.getY() - oy) / cellHeight;
|
||||||
col = Math.max(0, Math.min(col, sb.getCols() - 1));
|
col = Math.max(0, Math.min(col, sb.getDisplayCols() - 1));
|
||||||
row = Math.max(0, Math.min(row, sb.getRows() - 1));
|
row = Math.max(0, Math.min(row, sb.getDisplayRows() - 1));
|
||||||
|
|
||||||
selectionEndRow = row;
|
selectionEndRow = row;
|
||||||
selectionEndCol = col;
|
selectionEndCol = col;
|
||||||
|
}
|
||||||
repaint();
|
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
|
@Override
|
||||||
public void mouseReleased(MouseEvent e) {
|
public void mouseReleased(MouseEvent e) {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
// If start == end, treat as a click (position cursor, clear selection)
|
|
||||||
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
|
|
||||||
if (client != null && client.getConnectionState().isFullSession()) {
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
|
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();
|
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 hitSeg = client.getGocaDecoder().findPickedSegment(gx, gy);
|
||||||
|
int hitTag = client.getGocaDecoder().getSegmentTag(hitSeg);
|
||||||
|
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) hitSeg=%d hitTag=%d btn=%d",
|
||||||
|
e.getX(), e.getY(), ox, oy, gridW, gridH, px, py, gx, gy, hitSeg, hitTag, button
|
||||||
|
));
|
||||||
|
|
||||||
|
client.getInputProcessor().sendGraphicMouseAid(
|
||||||
|
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
||||||
|
button,
|
||||||
|
e.isShiftDown(),
|
||||||
|
e.isControlDown()
|
||||||
|
);
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatic Light-Pen / Selectable Field detection on mouse click:
|
||||||
|
// Per IBM Host On-Demand (MouseMgr.java / PS3270.java) and 3270 specifications:
|
||||||
|
// If the clicked screen location belongs to a detectable field (faIsSelectable and not faIsZero),
|
||||||
|
// automatically process light-pen / cursor selection (e.g. immediate selection, Enter, or toggling ? -> >).
|
||||||
|
if (sb.isFormatted()) {
|
||||||
|
int faPos = sb.findFieldAttribute(clickAddr);
|
||||||
|
if (faPos >= 0) {
|
||||||
|
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||||
|
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||||
|
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
||||||
|
clearSelection();
|
||||||
|
if (client.lightPenSelect(clickAddr)) {
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightPenMode) {
|
||||||
|
clearSelection();
|
||||||
|
sb.setCursorAddress(clickAddr);
|
||||||
|
boolean result = client.lightPenSelect(clickAddr);
|
||||||
|
if (result) {
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
|
||||||
|
sb.setCursorAddress(clickAddr);
|
||||||
|
clearSelection();
|
||||||
|
refreshScreen();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
repaint();
|
repaint();
|
||||||
@@ -338,7 +471,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
||||||
blockModeItem.addActionListener(ev -> {
|
blockModeItem.addActionListener(ev -> {
|
||||||
blockSelectMode = blockModeItem.isSelected();
|
blockSelectMode = blockModeItem.isSelected();
|
||||||
org.pubvm.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
|
haus.nightmare.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
|
||||||
});
|
});
|
||||||
popup.add(blockModeItem);
|
popup.add(blockModeItem);
|
||||||
|
|
||||||
@@ -390,8 +523,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
int termRows = 24;
|
int termRows = 24;
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
ScreenBuffer sb = client.getScreenBuffer();
|
ScreenBuffer sb = client.getScreenBuffer();
|
||||||
termCols = sb.getCols();
|
termCols = sb.getDisplayCols();
|
||||||
termRows = sb.getRows();
|
termRows = sb.getDisplayRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use minimal padding for the fit calculation
|
// Use minimal padding for the fit calculation
|
||||||
@@ -416,7 +549,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
if (bestSize != currentFontSize) {
|
if (bestSize != currentFontSize) {
|
||||||
currentFontSize = bestSize;
|
currentFontSize = bestSize;
|
||||||
org.pubvm.j3270.config.Settings.setFontSize(bestSize);
|
haus.nightmare.j3270.config.Settings.setFontSize(bestSize);
|
||||||
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
||||||
updateCellSize();
|
updateCellSize();
|
||||||
}
|
}
|
||||||
@@ -473,7 +606,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
||||||
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
||||||
for (String key : navKeys) {
|
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);
|
bindKeyToMap(im, key, binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +619,7 @@ public class TerminalPanel extends JPanel {
|
|||||||
} else {
|
} else {
|
||||||
defaultBinding = "shift F" + (i - 12);
|
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);
|
bindKeyToMap(im, "PF" + i, binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,25 +627,27 @@ public class TerminalPanel extends JPanel {
|
|||||||
String pa1Def = "alt 1";
|
String pa1Def = "alt 1";
|
||||||
String pa2Def = "alt 2";
|
String pa2Def = "alt 2";
|
||||||
String pa3Def = "alt 3";
|
String pa3Def = "alt 3";
|
||||||
bindKeyToMap(im, "PA1", org.pubvm.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
|
bindKeyToMap(im, "PA1", haus.nightmare.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
|
||||||
bindKeyToMap(im, "PA2", org.pubvm.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
|
bindKeyToMap(im, "PA2", haus.nightmare.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
|
||||||
bindKeyToMap(im, "PA3", org.pubvm.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
|
bindKeyToMap(im, "PA3", haus.nightmare.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
|
||||||
|
|
||||||
// Clear
|
// 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
|
// Extended 3270 functions
|
||||||
bindKeyToMap(im, "ERASE_INPUT", org.pubvm.j3270.config.Settings.getKeyBinding("ERASE_INPUT", "alt E"));
|
bindKeyToMap(im, "ERASE_INPUT", haus.nightmare.j3270.config.Settings.getKeyBinding("ERASE_INPUT", "alt E"));
|
||||||
bindKeyToMap(im, "NEWLINE", org.pubvm.j3270.config.Settings.getKeyBinding("NEWLINE", "shift ENTER"));
|
bindKeyToMap(im, "NEWLINE", haus.nightmare.j3270.config.Settings.getKeyBinding("NEWLINE", "shift ENTER"));
|
||||||
bindKeyToMap(im, "DUP", org.pubvm.j3270.config.Settings.getKeyBinding("DUP", "alt D"));
|
bindKeyToMap(im, "DUP", haus.nightmare.j3270.config.Settings.getKeyBinding("DUP", "alt D"));
|
||||||
bindKeyToMap(im, "FIELD_MARK", org.pubvm.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
|
bindKeyToMap(im, "FIELD_MARK", haus.nightmare.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
|
||||||
bindKeyToMap(im, "ATTN", org.pubvm.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
|
bindKeyToMap(im, "ATTN", haus.nightmare.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
|
||||||
bindKeyToMap(im, "SYSREQ", org.pubvm.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
||||||
|
bindKeyToMap(im, "CURSEL", haus.nightmare.j3270.config.Settings.getKeyBinding("CURSEL", "alt Q"));
|
||||||
|
|
||||||
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
|
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
|
||||||
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
|
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
|
||||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
|
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
|
||||||
|
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN");
|
||||||
|
|
||||||
// Create actions for all bound keys
|
// Create actions for all bound keys
|
||||||
am.put("j3270-ENTER", createAction(this::handleEnter));
|
am.put("j3270-ENTER", createAction(this::handleEnter));
|
||||||
@@ -537,10 +672,12 @@ public class TerminalPanel extends JPanel {
|
|||||||
am.put("j3270-FIELD_MARK", createAction(this::handleFieldMark));
|
am.put("j3270-FIELD_MARK", createAction(this::handleFieldMark));
|
||||||
am.put("j3270-ATTN", createAction(this::handleAttn));
|
am.put("j3270-ATTN", createAction(this::handleAttn));
|
||||||
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
|
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
|
||||||
|
am.put("j3270-CURSEL", createAction(this::handleCursorSelect));
|
||||||
|
|
||||||
// Copy/Paste actions
|
// Copy/Paste actions
|
||||||
am.put("j3270-COPY", createAction(this::copySelection));
|
am.put("j3270-COPY", createAction(this::copySelection));
|
||||||
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||||
|
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
|
||||||
|
|
||||||
for (int i = 1; i <= 24; i++) {
|
for (int i = 1; i <= 24; i++) {
|
||||||
final int pf = i;
|
final int pf = i;
|
||||||
@@ -722,7 +859,21 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleCursorSelect() {
|
||||||
|
if (client != null && client.getConnectionState().isFullSession()) {
|
||||||
|
boolean selected = client.cursorSelect();
|
||||||
|
if (selected) {
|
||||||
|
refreshScreen();
|
||||||
|
} else {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void refreshScreen() {
|
private void refreshScreen() {
|
||||||
|
if (client != null) {
|
||||||
|
client.getScreenBuffer().updateDisplaySnapshot();
|
||||||
|
}
|
||||||
repaint();
|
repaint();
|
||||||
// Notify parent to update status bar too
|
// Notify parent to update status bar too
|
||||||
Container parent = getParent();
|
Container parent = getParent();
|
||||||
@@ -736,8 +887,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupFont() {
|
private void setupFont() {
|
||||||
currentFontSize = org.pubvm.j3270.config.Settings.getFontSize();
|
currentFontSize = haus.nightmare.j3270.config.Settings.getFontSize();
|
||||||
String fontFamily = org.pubvm.j3270.config.Settings.getFontFamily();
|
String fontFamily = haus.nightmare.j3270.config.Settings.getFontFamily();
|
||||||
|
|
||||||
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
||||||
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
||||||
@@ -749,13 +900,13 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
private void setupColors() {
|
private void setupColors() {
|
||||||
for (int i = 0; i < 16; i++) {
|
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);
|
monoNormal = haus.nightmare.j3270.config.Settings.getMonoColorOverride("NORMAL", DEFAULT_MONO_NORMAL);
|
||||||
monoIntensify = org.pubvm.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY);
|
monoIntensify = haus.nightmare.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY);
|
||||||
monoProtected = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED);
|
monoProtected = haus.nightmare.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED);
|
||||||
monoProtectedHigh = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH);
|
monoProtectedHigh = haus.nightmare.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH);
|
||||||
bgColor = org.pubvm.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR);
|
bgColor = haus.nightmare.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR);
|
||||||
setBackground(bgColor);
|
setBackground(bgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,14 +914,14 @@ public class TerminalPanel extends JPanel {
|
|||||||
setupColors();
|
setupColors();
|
||||||
setupKeyBindings();
|
setupKeyBindings();
|
||||||
setupFont();
|
setupFont();
|
||||||
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
|
||||||
revalidate();
|
revalidate();
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFontSize(int size) {
|
public void setFontSize(int size) {
|
||||||
currentFontSize = size;
|
currentFontSize = size;
|
||||||
org.pubvm.j3270.config.Settings.setFontSize(size);
|
haus.nightmare.j3270.config.Settings.setFontSize(size);
|
||||||
terminalFont = terminalFont.deriveFont((float) size);
|
terminalFont = terminalFont.deriveFont((float) size);
|
||||||
updateCellSize();
|
updateCellSize();
|
||||||
resizeGuard = true;
|
resizeGuard = true;
|
||||||
@@ -801,6 +952,63 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
public void setClient(Telnet3270Client client) {
|
public void setClient(Telnet3270Client client) {
|
||||||
this.client = 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 = Math.min((int) Math.round(ch * 0.82), (int) Math.round(cw * 1.45));
|
||||||
|
if (fontSize < 9) fontSize = 9;
|
||||||
|
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
|
@Override
|
||||||
@@ -810,8 +1018,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
|
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
|
||||||
// When the host switches to alternate screen, onScreenSizeChanged fires
|
// When the host switches to alternate screen, onScreenSizeChanged fires
|
||||||
// and the frame re-packs.
|
// and the frame re-packs.
|
||||||
int displayCols = sb.getCols();
|
int displayCols = sb.getDisplayCols();
|
||||||
int displayRows = sb.getRows();
|
int displayRows = sb.getDisplayRows();
|
||||||
return new Dimension(displayCols * cellWidth + padding * 2,
|
return new Dimension(displayCols * cellWidth + padding * 2,
|
||||||
displayRows * cellHeight + padding * 2);
|
displayRows * cellHeight + padding * 2);
|
||||||
}
|
}
|
||||||
@@ -827,9 +1035,12 @@ public class TerminalPanel extends JPanel {
|
|||||||
|
|
||||||
Graphics2D g2 = (Graphics2D) g;
|
Graphics2D g2 = (Graphics2D) g;
|
||||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||||
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
|
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
|
// Clear entire panel with background color
|
||||||
g2.setColor(bgColor);
|
g2.setColor(bgColor);
|
||||||
@@ -844,6 +1055,28 @@ public class TerminalPanel extends JPanel {
|
|||||||
int cols = sb.getDisplayCols();
|
int cols = sb.getDisplayCols();
|
||||||
boolean isColorModel = client.getConfig().getModel().isColor();
|
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
|
// Track current field attribute for monochrome color decisions
|
||||||
byte currentFA = 0;
|
byte currentFA = 0;
|
||||||
ExtendedAttribute currentFieldEa = null;
|
ExtendedAttribute currentFieldEa = null;
|
||||||
@@ -933,9 +1166,10 @@ public class TerminalPanel extends JPanel {
|
|||||||
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
|
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
|
||||||
boolean drawnAsPs = false;
|
boolean drawnAsPs = false;
|
||||||
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
|
if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
|
||||||
org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
|
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
|
||||||
if (slot != null) {
|
if (slot != null) {
|
||||||
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), bgColor.getRGB());
|
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) {
|
if (img != null) {
|
||||||
g2.drawImage(img, x, y, null);
|
g2.drawImage(img, x, y, null);
|
||||||
drawnAsPs = true;
|
drawnAsPs = true;
|
||||||
@@ -949,10 +1183,11 @@ public class TerminalPanel extends JPanel {
|
|||||||
Font f = bold ? boldTerminalFont : terminalFont;
|
Font f = bold ? boldTerminalFont : terminalFont;
|
||||||
g2.setFont(f);
|
g2.setFont(f);
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
|
int textY = y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2);
|
||||||
if (ch < 128) {
|
if (ch < 128) {
|
||||||
g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent);
|
g2.drawString(CHAR_STRINGS[ch], x, textY);
|
||||||
} else {
|
} else {
|
||||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
g2.drawString(String.valueOf(ch), x, textY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -960,8 +1195,8 @@ public class TerminalPanel extends JPanel {
|
|||||||
// Draw underline
|
// Draw underline
|
||||||
if (underline) {
|
if (underline) {
|
||||||
g2.setColor(fgColor);
|
g2.setColor(fgColor);
|
||||||
g2.drawLine(x, y + cellHeight - fontDescent,
|
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
|
||||||
x + cellWidth - 1, y + cellHeight - fontDescent);
|
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw selection highlight
|
// Draw selection highlight
|
||||||
@@ -972,20 +1207,28 @@ public class TerminalPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Vector Graphics Plane overlay if present
|
// Draw Graphic Cursor (Light-Pen / interactive graphics pointer) if active
|
||||||
if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) {
|
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 gridW = cols * cellWidth;
|
||||||
int gridH = rows * cellHeight;
|
int gridH = rows * cellHeight;
|
||||||
client.getGraphicsPlane().resize(gridW, gridH);
|
int gWidth = client.getGraphicsPlane().getCanvasWidth();
|
||||||
int[] rgb = client.getGraphicsPlane().getRgbBuffer();
|
int gHeight = client.getGraphicsPlane().getCanvasHeight();
|
||||||
if (rgb != null) {
|
int px = ox + (gWidth > 0 ? (int)Math.round((double)canvasPx * gridW / gWidth) : canvasPx);
|
||||||
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gridW, gridH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
int py = oy + (gHeight > 0 ? (int)Math.round((double)canvasPy * gridH / gHeight) : canvasPy);
|
||||||
img.setRGB(0, 0, gridW, gridH, rgb, 0, gridW);
|
|
||||||
g2.drawImage(img, ox, oy, gridW, gridH, null);
|
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 cursor
|
// Draw 3270 text cursor
|
||||||
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
||||||
int curAddr = sb.getDisplayCursorAddress();
|
int curAddr = sb.getDisplayCursorAddress();
|
||||||
int curRow = curAddr / cols;
|
int curRow = curAddr / cols;
|
||||||
@@ -998,6 +1241,13 @@ public class TerminalPanel extends JPanel {
|
|||||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||||
g2.setPaintMode();
|
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) {
|
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
||||||
@@ -1038,4 +1288,22 @@ public class TerminalPanel extends JPanel {
|
|||||||
if (blinkTimer != null)
|
if (blinkTimer != null)
|
||||||
blinkTimer.stop();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.pubvm.j3270.ui;
|
package haus.nightmare.j3270.ui;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
+8
-8
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j;
|
package haus.nightmare.lib3270j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for a 3270 terminal connection.
|
* Configuration for a 3270 terminal connection.
|
||||||
@@ -12,13 +12,13 @@ public class ConnectionConfig {
|
|||||||
private boolean extendedDataStream = true;
|
private boolean extendedDataStream = true;
|
||||||
private boolean useTls = false;
|
private boolean useTls = false;
|
||||||
private boolean tlsVerifyCert = true;
|
private boolean tlsVerifyCert = true;
|
||||||
private org.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
|
private haus.nightmare.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
|
||||||
private String sslProtocol = "TLS";
|
private String sslProtocol = "TLS";
|
||||||
private int connectTimeoutMs = 15000;
|
private int connectTimeoutMs = 15000;
|
||||||
private int nopIntervalSeconds = 0;
|
private int nopIntervalSeconds = 0;
|
||||||
private String terminalName = null; // override terminal type string
|
private String terminalName = null; // override terminal type string
|
||||||
private boolean tn3270eEnabled = true;
|
private boolean tn3270eEnabled = true;
|
||||||
private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.BOTH;
|
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||||
|
|
||||||
public ConnectionConfig() {}
|
public ConnectionConfig() {}
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@ public class ConnectionConfig {
|
|||||||
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
|
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
|
||||||
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
|
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
|
||||||
|
|
||||||
public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
public haus.nightmare.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||||
public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
public void setCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
||||||
|
|
||||||
public String getSslProtocol() { return sslProtocol; }
|
public String getSslProtocol() { return sslProtocol; }
|
||||||
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
|
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
|
||||||
@@ -82,9 +82,9 @@ public class ConnectionConfig {
|
|||||||
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
||||||
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
||||||
|
|
||||||
public org.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
|
public haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
|
||||||
public void setGraphicsMode(org.lib3270j.graphics.GraphicsMode mode) {
|
public void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||||
this.graphicsMode = (mode != null) ? mode : org.lib3270j.graphics.GraphicsMode.NONE;
|
this.graphicsMode = (mode != null) ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTerminalName() { return terminalName; }
|
public String getTerminalName() { return terminalName; }
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j;
|
package haus.nightmare.lib3270j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connection state machine states.
|
* Connection state machine states.
|
||||||
+6
-6
@@ -1,8 +1,8 @@
|
|||||||
package org.lib3270j;
|
package haus.nightmare.lib3270j;
|
||||||
|
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import org.lib3270j.screen.ExtendedAttribute;
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
import org.lib3270j.listener.*;
|
import haus.nightmare.lib3270j.listener.*;
|
||||||
|
|
||||||
import java.util.logging.*;
|
import java.util.logging.*;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ public class DiagnosticClient {
|
|||||||
static Telnet3270Client client;
|
static Telnet3270Client client;
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
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);
|
rootLogger.setLevel(Level.ALL);
|
||||||
Handler handler = new ConsoleHandler();
|
Handler handler = new ConsoleHandler();
|
||||||
handler.setLevel(Level.ALL);
|
handler.setLevel(Level.ALL);
|
||||||
@@ -26,7 +26,7 @@ public class DiagnosticClient {
|
|||||||
String host = args.length >= 1 ? args[0] : "192.168.0.30";
|
String host = args.length >= 1 ? args[0] : "192.168.0.30";
|
||||||
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270;
|
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 = new Telnet3270Client(config);
|
||||||
|
|
||||||
client.addConnectionListener(new ConnectionListener() {
|
client.addConnectionListener(new ConnectionListener() {
|
||||||
+20
-15
@@ -1,13 +1,13 @@
|
|||||||
package org.lib3270j;
|
package haus.nightmare.lib3270j;
|
||||||
|
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import org.lib3270j.datastream.DataStreamProcessor;
|
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||||
import org.lib3270j.input.InputProcessor;
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
import org.lib3270j.listener.ConnectionListener;
|
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||||
import org.lib3270j.listener.ScreenUpdateListener;
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import org.lib3270j.telnet.TelnetConnection;
|
import haus.nightmare.lib3270j.telnet.TelnetConnection;
|
||||||
import org.lib3270j.telnet.TelnetFSM;
|
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -54,6 +54,7 @@ public class Telnet3270Client {
|
|||||||
dsProcessor.setOutputSender(fsm::send3270Data);
|
dsProcessor.setOutputSender(fsm::send3270Data);
|
||||||
dsProcessor.setInputProcessor(inputProcessor);
|
dsProcessor.setInputProcessor(inputProcessor);
|
||||||
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
||||||
|
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,7 +126,7 @@ public class Telnet3270Client {
|
|||||||
public ConnectionConfig getConfig() { return config; }
|
public ConnectionConfig getConfig() { return config; }
|
||||||
|
|
||||||
/** Set a custom or interactive TLS certificate verifier callback. */
|
/** Set a custom or interactive TLS certificate verifier callback. */
|
||||||
public void setTlsCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) {
|
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
|
||||||
config.setCertificateVerifier(verifier);
|
config.setCertificateVerifier(verifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ public class Telnet3270Client {
|
|||||||
|
|
||||||
/** Send Enter key. */
|
/** Send Enter key. */
|
||||||
public void sendEnter() {
|
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). */
|
/** Send a PF key (1-24). */
|
||||||
@@ -191,7 +192,7 @@ public class Telnet3270Client {
|
|||||||
|
|
||||||
/** Send Clear key. */
|
/** Send Clear key. */
|
||||||
public void sendClear() {
|
public void sendClear() {
|
||||||
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Move cursor up. */
|
/** Move cursor up. */
|
||||||
@@ -222,16 +223,20 @@ public class Telnet3270Client {
|
|||||||
public void sysReq() { inputProcessor.sysReq(); }
|
public void sysReq() { inputProcessor.sysReq(); }
|
||||||
/** Reset (unlock keyboard). */
|
/** Reset (unlock keyboard). */
|
||||||
public void reset() { inputProcessor.reset(); }
|
public void reset() { inputProcessor.reset(); }
|
||||||
|
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
|
||||||
|
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
|
||||||
|
/** Trigger Light Pen selection at the specified screen address. */
|
||||||
|
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
|
||||||
|
|
||||||
public org.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||||
return dsProcessor.getProgramSymbolManager();
|
return dsProcessor.getProgramSymbolManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
public org.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||||
return dsProcessor.getGraphicsPlane();
|
return dsProcessor.getGraphicsPlane();
|
||||||
}
|
}
|
||||||
|
|
||||||
public org.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||||
return dsProcessor.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.
|
* Terminal model definitions for IBM 3278 and 3279 terminals.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.charset;
|
package haus.nightmare.lib3270j.charset;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EBCDIC ↔ Unicode translator.
|
* EBCDIC ↔ Unicode translator.
|
||||||
+99
-68
@@ -1,10 +1,10 @@
|
|||||||
package org.lib3270j.datastream;
|
package haus.nightmare.lib3270j.datastream;
|
||||||
|
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import org.lib3270j.screen.ExtendedAttribute;
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import org.lib3270j.listener.ScreenUpdateListener;
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -34,15 +34,15 @@ public class DataStreamProcessor {
|
|||||||
private OutputSender outputSender;
|
private OutputSender outputSender;
|
||||||
|
|
||||||
// File Transfer DFT handler
|
// 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
|
// Input processor reference to manage keyboard locking state
|
||||||
private org.lib3270j.input.InputProcessor inputProcessor;
|
private haus.nightmare.lib3270j.input.InputProcessor inputProcessor;
|
||||||
|
|
||||||
// Graphics & Programmed Symbols
|
// Graphics & Programmed Symbols
|
||||||
private final org.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new org.lib3270j.graphics.ProgramSymbolManager();
|
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
|
||||||
private final org.lib3270j.graphics.GraphicsPlane graphicsPlane = new org.lib3270j.graphics.GraphicsPlane(800, 600);
|
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(720, 384);
|
||||||
private final org.lib3270j.graphics.GocaDecoder gocaDecoder = new org.lib3270j.graphics.GocaDecoder(graphicsPlane);
|
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 final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||||
private int currentGocaSubtype = 0;
|
private int currentGocaSubtype = 0;
|
||||||
|
|
||||||
@@ -59,21 +59,22 @@ public class DataStreamProcessor {
|
|||||||
this.outputBuffer = new byte[32768];
|
this.outputBuffer = new byte[32768];
|
||||||
this.outputPos = 0;
|
this.outputPos = 0;
|
||||||
this.gocaDecoder.setProgramSymbolManager(programSymbolManager);
|
this.gocaDecoder.setProgramSymbolManager(programSymbolManager);
|
||||||
|
this.graphicsPlane.setProgramSymbolManager(programSymbolManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryReplyBuilder getQueryReplyBuilder() {
|
public QueryReplyBuilder getQueryReplyBuilder() {
|
||||||
return qrBuilder;
|
return qrBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public org.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||||
return programSymbolManager;
|
return programSymbolManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public org.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||||
return graphicsPlane;
|
return graphicsPlane;
|
||||||
}
|
}
|
||||||
|
|
||||||
public org.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||||
return gocaDecoder;
|
return gocaDecoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,14 +82,18 @@ public class DataStreamProcessor {
|
|||||||
this.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;
|
this.ftDft = ftDft;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setInputProcessor(org.lib3270j.input.InputProcessor inputProcessor) {
|
public void setInputProcessor(haus.nightmare.lib3270j.input.InputProcessor inputProcessor) {
|
||||||
this.inputProcessor = inputProcessor;
|
this.inputProcessor = inputProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public haus.nightmare.lib3270j.input.InputProcessor getInputProcessor() {
|
||||||
|
return inputProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||||
screenListeners.add(l);
|
screenListeners.add(l);
|
||||||
}
|
}
|
||||||
@@ -175,6 +180,7 @@ public class DataStreamProcessor {
|
|||||||
case CMD_WSF:
|
case CMD_WSF:
|
||||||
case SNA_CMD_WSF:
|
case SNA_CMD_WSF:
|
||||||
processWriteStructuredField(data, offset, length);
|
processWriteStructuredField(data, offset, length);
|
||||||
|
keyboardRestore = true;
|
||||||
break;
|
break;
|
||||||
case CMD_NOP:
|
case CMD_NOP:
|
||||||
log.info(">>> NOP command");
|
log.info(">>> NOP command");
|
||||||
@@ -190,6 +196,10 @@ public class DataStreamProcessor {
|
|||||||
screen.updateDisplaySnapshot();
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyboardRestore && inputProcessor != null) {
|
||||||
|
inputProcessor.setKeyboardLocked(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: dump non-empty screen lines
|
// Debug: dump non-empty screen lines
|
||||||
if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) {
|
if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) {
|
||||||
int r = screen.getRows();
|
int r = screen.getRows();
|
||||||
@@ -204,7 +214,7 @@ public class DataStreamProcessor {
|
|||||||
ExtendedAttribute ea = screen.getCell(addr);
|
ExtendedAttribute ea = screen.getCell(addr);
|
||||||
byte fa = screen.getFieldAttributeAt(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) {
|
if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
|
||||||
line.append('*');
|
line.append('*');
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
@@ -653,8 +663,15 @@ public class DataStreamProcessor {
|
|||||||
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
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
|
// AID byte
|
||||||
outputWrite(AID_NO);
|
outputWrite(aid);
|
||||||
// Cursor address
|
// Cursor address
|
||||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
outputWrite(caddr[0] & 0xFF);
|
outputWrite(caddr[0] & 0xFF);
|
||||||
@@ -701,56 +718,36 @@ public class DataStreamProcessor {
|
|||||||
outputWrite(caddr[1] & 0xFF);
|
outputWrite(caddr[1] & 0xFF);
|
||||||
|
|
||||||
if (!screen.isFormatted()) {
|
if (!screen.isFormatted()) {
|
||||||
// Unformatted screen: send data up to last non-null
|
// Unformatted screen: send all non-null characters (null suppression)
|
||||||
int lastNonNull = -1;
|
for (int i = 0; i < size; i++) {
|
||||||
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;
|
int b = screen.getCell(i).ec & 0xFF;
|
||||||
outputWrite(b != 0 ? b : 0x40);
|
if (b != 0x00) {
|
||||||
|
outputWrite(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Formatted screen: send modified fields with trailing NULLs stripped
|
// Formatted screen: send modified fields with null suppression per 3270 spec
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
ExtendedAttribute ea = screen.getCell(i);
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
|
||||||
int fieldStart = (i + 1) % size;
|
int fieldStart = (i + 1) % size;
|
||||||
|
|
||||||
// Collect field data and find last non-null byte
|
// Always send SBA and address of first character position in field
|
||||||
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
|
|
||||||
outputWrite(ORDER_SBA);
|
outputWrite(ORDER_SBA);
|
||||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||||
outputWrite(addr[0] & 0xFF);
|
outputWrite(addr[0] & 0xFF);
|
||||||
outputWrite(addr[1] & 0xFF);
|
outputWrite(addr[1] & 0xFF);
|
||||||
|
|
||||||
// Send field data (strip trailing nulls)
|
// Send all non-null characters in field (suppressing 0x00)
|
||||||
if (lastNonNull >= 0) {
|
int pos = fieldStart;
|
||||||
byte[] allData = fieldData.toByteArray();
|
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||||
for (int k = 0; k <= lastNonNull; k++) {
|
int b = screen.getCell(pos).ec & 0xFF;
|
||||||
outputWrite(allData[k] & 0xFF);
|
if (b != 0x00) {
|
||||||
|
outputWrite(b);
|
||||||
}
|
}
|
||||||
|
pos = (pos + 1) % size;
|
||||||
|
if (pos == fieldStart)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -782,22 +779,27 @@ public class DataStreamProcessor {
|
|||||||
case SF_READ_PART:
|
case SF_READ_PART:
|
||||||
processSFReadPartition(data, pos, fieldLen);
|
processSFReadPartition(data, pos, fieldLen);
|
||||||
break;
|
break;
|
||||||
case SF_ERASE_RESET:
|
case SF_ERASE_RESET: {
|
||||||
if (fieldLen >= 4) {
|
boolean alt = (fieldLen >= 4) && ((data[pos + 3] & 0xFF) == SF_ER_ALT);
|
||||||
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT;
|
|
||||||
screen.erase(alt);
|
screen.erase(alt);
|
||||||
graphicsPlane.clear();
|
graphicsPlane.clear();
|
||||||
|
gocaDecoder.resetDefaults();
|
||||||
notifyScreenSizeChanged();
|
notifyScreenSizeChanged();
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case SF_SET_REPLY_MODE:
|
case SF_SET_REPLY_MODE:
|
||||||
if (fieldLen >= 5) {
|
if (fieldLen >= 5) {
|
||||||
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
|
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SF_CREATE_PART:
|
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();
|
graphicsPlane.clear();
|
||||||
|
gocaDecoder.resetDefaults();
|
||||||
break;
|
break;
|
||||||
case SF_OUTBOUND_DS:
|
case SF_OUTBOUND_DS:
|
||||||
if (fieldLen > 5) {
|
if (fieldLen > 5) {
|
||||||
@@ -812,32 +814,50 @@ public class DataStreamProcessor {
|
|||||||
log.fine("SF_TRANSFER_DATA received but ftDft is null");
|
log.fine("SF_TRANSFER_DATA received but ftDft is null");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_LOADPS_DIRECT: // 0x06: Load Programmed Symbols direct
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_DIRECT: // 0x06: Load Programmed Symbols direct
|
||||||
if (fieldLen > 3) {
|
if (fieldLen > 3) {
|
||||||
byte[] psData = new byte[fieldLen - 3];
|
byte[] psData = new byte[fieldLen - 3];
|
||||||
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
||||||
programSymbolManager.loadps(psData);
|
programSymbolManager.loadps(psData);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
||||||
if (fieldLen >= 4) {
|
if (fieldLen >= 4) {
|
||||||
int sfSubId = data[pos + 3] & 0xFF;
|
int sfSubId = data[pos + 3] & 0xFF;
|
||||||
switch (sfSubId) {
|
switch (sfSubId) {
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_LOADPS_SUB: // 0x06: Load Programmed Symbols
|
||||||
if (fieldLen > 4) {
|
if (fieldLen > 4) {
|
||||||
byte[] psData = new byte[fieldLen - 4];
|
byte[] psData = new byte[fieldLen - 4];
|
||||||
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
||||||
programSymbolManager.loadps(psData);
|
programSymbolManager.loadps(psData);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: // 0x10: Object Picture (Picture segments)
|
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Data (GOCA draw orders)
|
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
|
||||||
|
gocaDecoder.setGraphicsCursorActive(true);
|
||||||
|
notifyScreenUpdated();
|
||||||
|
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)
|
||||||
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
|
||||||
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
|
||||||
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
|
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());
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
|
int targetW = screen.getCols() * 9;
|
||||||
|
int targetH = screen.getRows() * 16;
|
||||||
|
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||||
|
graphicsPlane.resize(targetW, targetH);
|
||||||
|
}
|
||||||
|
|
||||||
if (flags == 0x80) { // SPAN_FIRST
|
if (flags == 0x80) { // SPAN_FIRST
|
||||||
gocaAccumulator.reset();
|
gocaAccumulator.reset();
|
||||||
@@ -855,7 +875,8 @@ public class DataStreamProcessor {
|
|||||||
}
|
}
|
||||||
byte[] fullStream = gocaAccumulator.toByteArray();
|
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||||
gocaAccumulator.reset();
|
gocaAccumulator.reset();
|
||||||
if (currentGocaSubtype == org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
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);
|
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||||
} else {
|
} else {
|
||||||
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
gocaDecoder.decodeStream(fullStream, 0, fullStream.length);
|
||||||
@@ -863,7 +884,8 @@ public class DataStreamProcessor {
|
|||||||
notifyScreenUpdated();
|
notifyScreenUpdated();
|
||||||
} else { // SPAN_ONLY (0xC0)
|
} else { // SPAN_ONLY (0xC0)
|
||||||
gocaAccumulator.reset();
|
gocaAccumulator.reset();
|
||||||
if (sfSubId == org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
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);
|
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
|
||||||
} else {
|
} else {
|
||||||
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
gocaDecoder.decodeStream(data, orderOffset, orderLen);
|
||||||
@@ -879,8 +901,15 @@ public class DataStreamProcessor {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders)
|
||||||
case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
|
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) {
|
if (fieldLen > 3) {
|
||||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||||
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||||
@@ -915,6 +944,8 @@ public class DataStreamProcessor {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case SF_RP_QUERY:
|
case SF_RP_QUERY:
|
||||||
log.info("ReadPartition Query — sending all query replies");
|
log.info("ReadPartition Query — sending all query replies");
|
||||||
|
graphicsPlane.clear();
|
||||||
|
gocaDecoder.resetDefaults();
|
||||||
sendAllQueryReplies();
|
sendAllQueryReplies();
|
||||||
break;
|
break;
|
||||||
case SF_RP_QLIST:
|
case SF_RP_QLIST:
|
||||||
+53
-20
@@ -1,11 +1,11 @@
|
|||||||
package org.lib3270j.datastream;
|
package haus.nightmare.lib3270j.datastream;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import org.lib3270j.graphics.GraphicsMode;
|
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||||
@@ -125,8 +125,8 @@ public class QueryReplyBuilder {
|
|||||||
// Vector Graphics QRs if enabled
|
// Vector Graphics QRs if enabled
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
|
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
|
||||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics()); // 0xB0
|
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0
|
||||||
appendQueryReply(out, QR_GIMAGE, buildGImage()); // 0xB1
|
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1
|
||||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
|
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
|
||||||
appendOemFmt(out); // 0xB3
|
appendOemFmt(out); // 0xB3
|
||||||
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
|
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
|
||||||
@@ -197,21 +197,21 @@ public class QueryReplyBuilder {
|
|||||||
case QR_RPQ_NAMES:
|
case QR_RPQ_NAMES:
|
||||||
case QR_RPQNAMES:
|
case QR_RPQNAMES:
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames());
|
appendQueryReply(out, code, buildRpqNames());
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_GRAPHICS:
|
case QR_GRAPHICS:
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics());
|
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows));
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case QR_GIMAGE:
|
case QR_GIMAGE:
|
||||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||||
appendQueryReply(out, QR_GIMAGE, buildGImage());
|
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows));
|
||||||
} else {
|
} else {
|
||||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||||
}
|
}
|
||||||
@@ -293,14 +293,36 @@ public class QueryReplyBuilder {
|
|||||||
out.write((Yr_3279_2 >> 16) & 0xFF);
|
out.write((Yr_3279_2 >> 16) & 0xFF);
|
||||||
out.write((Yr_3279_2 >> 8) & 0xFF);
|
out.write((Yr_3279_2 >> 8) & 0xFF);
|
||||||
out.write(Yr_3279_2 & 0xFF);
|
out.write(Yr_3279_2 & 0xFF);
|
||||||
out.write(SW_3279_2); // AW
|
int charW = getCharWidth();
|
||||||
out.write(SH_3279_2); // AH
|
int charH = getCharHeight();
|
||||||
|
out.write(charW); // AW
|
||||||
|
out.write(charH); // AH
|
||||||
int buf = maxCols * maxRows;
|
int buf = maxCols * maxRows;
|
||||||
out.write((buf >> 8) & 0xFF); // buffer size high
|
out.write((buf >> 8) & 0xFF); // buffer size high
|
||||||
out.write(buf & 0xFF); // buffer size low
|
out.write(buf & 0xFF); // buffer size low
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCharWidth() {
|
||||||
|
return SW_3279_2; // 9
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCharHeight() {
|
||||||
|
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES:
|
||||||
|
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations:
|
||||||
|
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes
|
||||||
|
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
|
||||||
|
// GDDM then places the top menu bar at Row 1 (gy = 187..200).
|
||||||
|
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
|
||||||
|
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
|
||||||
|
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
|
||||||
|
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
|
||||||
|
//
|
||||||
|
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
|
||||||
|
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
|
||||||
|
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] buildAlphaPartitions(int maxRows) {
|
private byte[] buildAlphaPartitions(int maxRows) {
|
||||||
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
int bufSize = screen.getMaxCols() * screen.getMaxRows();
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
|
||||||
@@ -312,13 +334,16 @@ public class QueryReplyBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildCharsets() {
|
private byte[] buildCharsets() {
|
||||||
|
int charW = getCharWidth();
|
||||||
|
int charH = getCharHeight();
|
||||||
|
|
||||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||||
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||||
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||||
out.write(0x00); // more flags
|
out.write(0x00); // more flags
|
||||||
out.write(SW_3279_2); // SDW (9)
|
out.write(charW); // SDW (9)
|
||||||
out.write(SH_3279_2); // SDH (12)
|
out.write(charH); // SDH (16 for 3179G, 12 for 3279-2)
|
||||||
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
|
||||||
out.write(0x00); // Load PS device type (high)
|
out.write(0x00); // Load PS device type (high)
|
||||||
out.write(0x00); // Load PS device type (low)
|
out.write(0x00); // Load PS device type (low)
|
||||||
@@ -343,8 +368,8 @@ public class QueryReplyBuilder {
|
|||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
|
||||||
out.write(0x82); // flags: GE, CGCSGID present
|
out.write(0x82); // flags: GE, CGCSGID present
|
||||||
out.write(0x00); // more flags
|
out.write(0x00); // more flags
|
||||||
out.write(SW_3279_2); // SDW - default char width (9)
|
out.write(charW); // SDW - default char width (9)
|
||||||
out.write(SH_3279_2); // SDH - default char height (12)
|
out.write(charH); // SDH - default char height (16 for 3179G, 12 for 3279-2)
|
||||||
out.write(0x00); // LoadPS format (0x00)
|
out.write(0x00); // LoadPS format (0x00)
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
@@ -422,13 +447,21 @@ public class QueryReplyBuilder {
|
|||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildGraphics() {
|
private byte[] buildGraphics(int maxCols, int maxRows) {
|
||||||
return new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 };
|
return new byte[]{
|
||||||
|
(byte) 0x80, 0x02,
|
||||||
|
0x00, 0x00,
|
||||||
|
0x00, (byte) 0xFC,
|
||||||
|
0x00
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildGImage() {
|
private byte[] buildGImage(int maxCols, int maxRows) {
|
||||||
return new byte[]{
|
return new byte[]{
|
||||||
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
|
0x00, 0x01,
|
||||||
|
0x00, 0x00,
|
||||||
|
0x00, (byte) 0xFC,
|
||||||
|
0x00,
|
||||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||||
};
|
};
|
||||||
@@ -481,7 +514,7 @@ public class QueryReplyBuilder {
|
|||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
out.write(0x00);
|
out.write(0x00);
|
||||||
out.write(i);
|
out.write(i);
|
||||||
int argb = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||||
int r = (argb >> 16) & 0xFF;
|
int r = (argb >> 16) & 0xFF;
|
||||||
int g = (argb >> 8) & 0xFF;
|
int g = (argb >> 8) & 0xFF;
|
||||||
int b = argb & 0xFF;
|
int b = argb & 0xFF;
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.ft;
|
package haus.nightmare.lib3270j.ft;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for an IND$FILE file transfer session.
|
* 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.
|
* 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.
|
* Decode a CUT-mode base-64 encoded integer from EBCDIC.
|
||||||
* Converts a table6-encoded EBCDIC character to its 6-bit value.
|
* 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);
|
char ascii = translator.ebcdicToUnicode(ebcdicByte & 0xFF);
|
||||||
int idx = TABLE6.indexOf(ascii);
|
int idx = TABLE6.indexOf(ascii);
|
||||||
return idx >= 0 ? idx : 0;
|
return idx >= 0 ? idx : 0;
|
||||||
@@ -199,7 +199,7 @@ public final class FTConstants {
|
|||||||
/**
|
/**
|
||||||
* Encode a 6-bit value into a table6-encoded EBCDIC character.
|
* 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);
|
char ascii = TABLE6.charAt(value & 0x3F);
|
||||||
int ebc = translator.unicodeToEbcdic(ascii);
|
int ebc = translator.unicodeToEbcdic(ascii);
|
||||||
return ebc >= 0 ? ebc : ASC2FT[ascii & 0xFF];
|
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 haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import org.lib3270j.input.InputProcessor;
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import static org.lib3270j.ft.FTConstants.*;
|
import static haus.nightmare.lib3270j.ft.FTConstants.*;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
+75
-23
@@ -1,8 +1,8 @@
|
|||||||
package org.lib3270j.ft;
|
package haus.nightmare.lib3270j.ft;
|
||||||
|
|
||||||
import org.lib3270j.input.InputProcessor;
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import static org.lib3270j.ft.FTConstants.*;
|
import static haus.nightmare.lib3270j.ft.FTConstants.*;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -106,6 +106,7 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
dftSaveBuf = null;
|
dftSaveBuf = null;
|
||||||
dftSaveBufLen = 0;
|
dftSaveBufLen = 0;
|
||||||
|
resetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,9 +174,9 @@ public class FTDft {
|
|||||||
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
|
nameBuf = extractName(data, sfOffset + 3 + 31, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OPEN_MSG.equalsIgnoreCase(nameBuf)) {
|
if (isMessageStream(nameBuf)) {
|
||||||
messageFlag = true;
|
messageFlag = true;
|
||||||
log.info("DFT: Open request for message stream");
|
log.info("DFT: Open request for message stream (" + nameBuf + ")");
|
||||||
} else {
|
} else {
|
||||||
messageFlag = false;
|
messageFlag = false;
|
||||||
listener.onDftRunning();
|
listener.onDftRunning();
|
||||||
@@ -188,15 +189,36 @@ public class FTDft {
|
|||||||
dftOpenAck();
|
dftOpenAck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isMessageStream(String name) {
|
||||||
|
if (name == null) return false;
|
||||||
|
String u = name.toUpperCase();
|
||||||
|
return u.contains(OPEN_MSG) || u.contains("MSG");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDataStream(String name) {
|
||||||
|
if (name == null) return false;
|
||||||
|
String u = name.toUpperCase();
|
||||||
|
return u.contains("FT:DATA") || u.contains("DATA");
|
||||||
|
}
|
||||||
|
|
||||||
private String extractName(byte[] data, int start, int maxLen) {
|
private String extractName(byte[] data, int start, int maxLen) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder asciiSb = new StringBuilder();
|
||||||
|
StringBuilder ebcdicSb = new StringBuilder();
|
||||||
for (int i = 0; i < maxLen && (start + i) < data.length; i++) {
|
for (int i = 0; i < maxLen && (start + i) < data.length; i++) {
|
||||||
int b = data[start + i] & 0xFF;
|
int b = data[start + i] & 0xFF;
|
||||||
if (b == 0) break;
|
if (b == 0) break;
|
||||||
char ch = translator.ebcdicToUnicode(b);
|
asciiSb.append((char) b);
|
||||||
sb.append(ch);
|
ebcdicSb.append(translator.ebcdicToUnicode(b));
|
||||||
}
|
}
|
||||||
return sb.toString().trim();
|
String ascii = asciiSb.toString().trim();
|
||||||
|
String ebcdic = ebcdicSb.toString().trim();
|
||||||
|
if (isMessageStream(ascii) || isDataStream(ascii)) {
|
||||||
|
return ascii;
|
||||||
|
}
|
||||||
|
if (isMessageStream(ebcdic) || isDataStream(ebcdic)) {
|
||||||
|
return ebcdic;
|
||||||
|
}
|
||||||
|
return ascii;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dftOpenAck() {
|
private void dftOpenAck() {
|
||||||
@@ -227,6 +249,7 @@ public class FTDft {
|
|||||||
int pos = offset + 2;
|
int pos = offset + 2;
|
||||||
int end = offset + length;
|
int end = offset + length;
|
||||||
|
|
||||||
|
boolean dataFound = false;
|
||||||
// Look for TR_BEGIN_DATA marker
|
// Look for TR_BEGIN_DATA marker
|
||||||
while (pos < end) {
|
while (pos < end) {
|
||||||
int headerCode = data[pos] & 0xFF;
|
int headerCode = data[pos] & 0xFF;
|
||||||
@@ -234,10 +257,11 @@ public class FTDft {
|
|||||||
if (headerCode == TR_BEGIN_DATA) {
|
if (headerCode == TR_BEGIN_DATA) {
|
||||||
if (pos + 3 > end) break;
|
if (pos + 3 > end) break;
|
||||||
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
|
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
|
||||||
int actualDataLen = dataLen - 3;
|
int actualDataLen = (dataLen > 5) ? Math.min(dataLen - 5, end - (pos + 3)) : (end - (pos + 3));
|
||||||
pos += 3;
|
pos += 3;
|
||||||
|
|
||||||
if (actualDataLen > 0 && pos + actualDataLen <= end) {
|
if (actualDataLen > 0 && pos + actualDataLen <= end) {
|
||||||
|
dataFound = true;
|
||||||
if (messageFlag) {
|
if (messageFlag) {
|
||||||
// Handle message payload from host
|
// Handle message payload from host
|
||||||
dftDataAck();
|
dftDataAck();
|
||||||
@@ -269,25 +293,46 @@ public class FTDft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send acknowledgement for file data
|
// Send acknowledgement only if file data was actually received and processed
|
||||||
|
if (dataFound) {
|
||||||
dftDataAck();
|
dftDataAck();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleHostMessage(byte[] data, int offset, int length) {
|
private void handleHostMessage(byte[] data, int offset, int length) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder asciiSb = new StringBuilder();
|
||||||
|
StringBuilder ebcdicSb = new StringBuilder();
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
int b = data[offset + i] & 0xFF;
|
int b = data[offset + i] & 0xFF;
|
||||||
if (b == 0 || b == '$') break;
|
if (b == 0 || b == '$') break;
|
||||||
char ch = translator.ebcdicToUnicode(b);
|
asciiSb.append((char) b);
|
||||||
sb.append(ch);
|
ebcdicSb.append(translator.ebcdicToUnicode(b));
|
||||||
}
|
}
|
||||||
String msg = sb.toString().trim();
|
String asciiMsg = asciiSb.toString().trim();
|
||||||
log.info("DFT message: " + msg);
|
String ebcdicMsg = ebcdicSb.toString().trim();
|
||||||
|
|
||||||
String msgLower = msg.toLowerCase();
|
// Determine if message is ASCII or EBCDIC
|
||||||
if (msg.startsWith(END_TRANSFER) || msgLower.contains("complete") || msgLower.contains("transferred") || msgLower.contains("success")) {
|
String msg = asciiMsg;
|
||||||
|
if (asciiMsg.toUpperCase().startsWith("TRANS") || asciiMsg.toLowerCase().contains("file") || asciiMsg.toLowerCase().contains("error")) {
|
||||||
|
msg = asciiMsg;
|
||||||
|
} else if (ebcdicMsg.toUpperCase().startsWith("TRANS") || ebcdicMsg.toLowerCase().contains("file") || ebcdicMsg.toLowerCase().contains("error")) {
|
||||||
|
msg = ebcdicMsg;
|
||||||
|
} else {
|
||||||
|
int asciiPrintable = 0;
|
||||||
|
for (char c : asciiMsg.toCharArray()) {
|
||||||
|
if (c >= 32 && c <= 126) asciiPrintable++;
|
||||||
|
}
|
||||||
|
if (asciiPrintable < asciiMsg.length() / 2 && !ebcdicMsg.isEmpty()) {
|
||||||
|
msg = ebcdicMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("DFT host message: " + msg);
|
||||||
|
|
||||||
|
String msgUpper = msg.toUpperCase();
|
||||||
|
if (msgUpper.startsWith(END_TRANSFER) || msgUpper.contains("COMPLETE") || msgUpper.contains("TRANSFERRED") || msgUpper.contains("SUCCESS")) {
|
||||||
listener.onTransferComplete(null);
|
listener.onTransferComplete(null);
|
||||||
} else if (listener.getCurrentState() == FTState.ABORT_SENT || msgLower.contains("error") || msgLower.contains("failed") || msgLower.contains("abort")) {
|
} else if (msgUpper.startsWith("TRANS") || msgUpper.contains("ERROR") || msgUpper.contains("FAILED") || msgUpper.contains("ABORT") || msgUpper.contains("NOT FOUND") || listener.getCurrentState() == FTState.ABORT_SENT) {
|
||||||
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
|
listener.onTransferAborted(msg.isEmpty() ? "Transfer aborted" : msg);
|
||||||
} else {
|
} else {
|
||||||
// Informational message (default success)
|
// Informational message (default success)
|
||||||
@@ -495,7 +540,7 @@ public class FTDft {
|
|||||||
|
|
||||||
input.sendStructuredFieldData(out.toByteArray());
|
input.sendStructuredFieldData(out.toByteArray());
|
||||||
|
|
||||||
if (!messageFlag) {
|
if (listener.getCurrentState() != FTState.NONE) {
|
||||||
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
||||||
listener.onTransferComplete(null);
|
listener.onTransferComplete(null);
|
||||||
}
|
}
|
||||||
@@ -504,13 +549,20 @@ public class FTDft {
|
|||||||
// ========== Data Acknowledgement ==========
|
// ========== Data Acknowledgement ==========
|
||||||
|
|
||||||
private void dftDataAck() {
|
private void dftDataAck() {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
ByteArrayOutputStream out = new ByteArrayOutputStream(12);
|
||||||
out.write(AID_SF);
|
out.write(AID_SF);
|
||||||
out.write(0); out.write(5);
|
out.write(0); out.write(11); // SF length = 11 (0x000B)
|
||||||
out.write(SF_TRANSFER_DATA);
|
out.write(SF_TRANSFER_DATA);
|
||||||
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
|
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
|
||||||
out.write(TR_NORMAL_REPLY & 0xFF);
|
out.write(TR_NORMAL_REPLY & 0xFF);
|
||||||
|
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
|
||||||
|
out.write(TR_RECNUM_HDR & 0xFF);
|
||||||
|
out.write((int) ((recnum >> 24) & 0xFF));
|
||||||
|
out.write((int) ((recnum >> 16) & 0xFF));
|
||||||
|
out.write((int) ((recnum >> 8) & 0xFF));
|
||||||
|
out.write((int) (recnum & 0xFF));
|
||||||
|
|
||||||
|
recnum++;
|
||||||
input.sendStructuredFieldData(out.toByteArray());
|
input.sendStructuredFieldData(out.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
|
import java.awt.Point;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
|
||||||
|
* Matches IBM Host On-Demand (HODTransform.java, PS3179G.java, HODInput.java).
|
||||||
|
*
|
||||||
|
* <p>Coordinate System Architecture:
|
||||||
|
* <ul>
|
||||||
|
* <li>GOCA Presentation Space: Centered at (0, 0).
|
||||||
|
* X ranges [-xMax .. +xMax] where width = cols * defaultCharWidth (defsx = 9).
|
||||||
|
* Y ranges [-yMax .. +yMax] where height = rows * defaultCharHeight (defsy = 16 or 12).
|
||||||
|
* (+Y is UP, -Y is DOWN, +X is RIGHT, -X is LEFT).</li>
|
||||||
|
* <li>Base Presentation Space (px, py): Unsigned top-left origin (0, 0).
|
||||||
|
* px = gx + xMax (ranges 0 .. totalWidth).
|
||||||
|
* py = yMax - gy (ranges 0 .. totalHeight).</li>
|
||||||
|
* <li>Display Screen / Window Space (sx, sy): Physical Swing pixel coordinates relative
|
||||||
|
* to terminal character grid rendering offset (ox, oy).
|
||||||
|
* sx = ox + (int) Math.round(px * transformX).
|
||||||
|
* sy = oy + (int) Math.round(py * transformY).
|
||||||
|
* where transformX = cellWidth / defaultCharWidth, transformY = cellHeight / defaultCharHeight.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Why simple fixed pixel scaling or hardcoded multipliers failed in past iterations:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Failure Mode 1</b>: Hardcoding totalHeight = rows * 12 caused coordinate truncation in 43-row mode (Model 4),
|
||||||
|
* capping yMax at 257 instead of 343 and making the top menu / EXIT button unreachable.</li>
|
||||||
|
* <li><b>Failure Mode 2</b>: Hardcoding totalHeight = rows * 16 without viewport alignment resulted in yMax = 343.
|
||||||
|
* In ADMDRAW, GDDM defines the entire active drawing area between y = -169 and y = +200 (~370 units total height).
|
||||||
|
* Mapping this into a 688-high canvas placed the top menu at Row 9.2 (middle of the screen) with a large blank void above,
|
||||||
|
* and clicking the visual menu emitted gy = 224 (missing the [187..200] menu bounding box).</li>
|
||||||
|
* <li><b>Failure Mode 3</b>: Intermediate raster buffering (e.g. fixed 800x600 canvas stretched to gridW x gridH)
|
||||||
|
* introduced rounding artifacts in bidirectional coordinate conversion (unmapX/unmapY).</li>
|
||||||
|
* <li><b>Failure Mode 4</b>: Overlapping dropdown menus (FILE, DRAW, TRANSFORM) lingering simultaneously when
|
||||||
|
* segment retention/clearing was decoupled from GDDM's segment lifecycle.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class GddmCoordinateTransform {
|
||||||
|
|
||||||
|
public static final int DEFAULT_CHAR_WIDTH = 9; // defsx: 3179G standard character cell width
|
||||||
|
public static final int DEFAULT_CHAR_HEIGHT_16 = 16; // defsy: 3179G standard character cell height
|
||||||
|
public static final int DEFAULT_CHAR_HEIGHT_12 = 12; // defsy: 3279-2 alternate character cell height
|
||||||
|
|
||||||
|
private int defaultCharWidth = DEFAULT_CHAR_WIDTH;
|
||||||
|
private int defaultCharHeight = DEFAULT_CHAR_HEIGHT_16;
|
||||||
|
|
||||||
|
private int screenCols = 80;
|
||||||
|
private int screenRows = 24;
|
||||||
|
|
||||||
|
private double transformX = 1.0;
|
||||||
|
private double transformY = 1.0;
|
||||||
|
|
||||||
|
public GddmCoordinateTransform() {
|
||||||
|
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GddmCoordinateTransform(int screenCols, int screenRows) {
|
||||||
|
this(screenCols, screenRows, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GddmCoordinateTransform(int screenCols, int screenRows, int defCharWidth, int defCharHeight) {
|
||||||
|
this.screenCols = screenCols > 0 ? screenCols : 80;
|
||||||
|
this.screenRows = screenRows > 0 ? screenRows : 24;
|
||||||
|
this.defaultCharWidth = defCharWidth > 0 ? defCharWidth : DEFAULT_CHAR_WIDTH;
|
||||||
|
this.defaultCharHeight = defCharHeight > 0 ? defCharHeight : DEFAULT_CHAR_HEIGHT_16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates screen grid and cell dimensions from UI.
|
||||||
|
*
|
||||||
|
* @param cellWidth Width of a character cell in Swing pixels
|
||||||
|
* @param cellHeight Height of a character cell in Swing pixels
|
||||||
|
*/
|
||||||
|
public void updateDisplayMetrics(int cellWidth, int cellHeight) {
|
||||||
|
if (cellWidth > 0 && defaultCharWidth > 0) {
|
||||||
|
this.transformX = (double) cellWidth / (double) defaultCharWidth;
|
||||||
|
}
|
||||||
|
if (cellHeight > 0 && defaultCharHeight > 0) {
|
||||||
|
this.transformY = (double) cellHeight / (double) defaultCharHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScreenDimensions(int cols, int rows) {
|
||||||
|
if (cols > 0) this.screenCols = cols;
|
||||||
|
if (rows > 0) this.screenRows = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultCharMetrics(int defWidth, int defHeight) {
|
||||||
|
if (defWidth > 0) this.defaultCharWidth = defWidth;
|
||||||
|
if (defHeight > 0) this.defaultCharHeight = defHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalWidth() {
|
||||||
|
return screenCols * defaultCharWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalHeight() {
|
||||||
|
return screenRows * defaultCharHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getXMax() {
|
||||||
|
int totalW = getTotalWidth();
|
||||||
|
return (totalW - 1) / 2 + (totalW - 1) % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getYMax() {
|
||||||
|
int totalH = getTotalHeight();
|
||||||
|
return (totalH - 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTransformX() {
|
||||||
|
return transformX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTransformY() {
|
||||||
|
return transformY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDefaultCharWidth() {
|
||||||
|
return defaultCharWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDefaultCharHeight() {
|
||||||
|
return defaultCharHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GOCA signed coordinate (gx, gy) to base presentation space coordinate (px, py).
|
||||||
|
*/
|
||||||
|
public Point gocaToBase(int gx, int gy) {
|
||||||
|
int px = gx + getXMax();
|
||||||
|
int py = getYMax() - gy;
|
||||||
|
return new Point(px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a base presentation space coordinate (px, py) to GOCA signed coordinate (gx, gy).
|
||||||
|
*/
|
||||||
|
public Point baseToGoca(int px, int py) {
|
||||||
|
int gx = px - getXMax();
|
||||||
|
int gy = getYMax() - py;
|
||||||
|
return new Point(gx, gy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GOCA signed coordinate (gx, gy) directly to Swing screen pixel coordinate (sx, sy).
|
||||||
|
*/
|
||||||
|
public Point gocaToScreenPixel(int gx, int gy, int ox, int oy, int cellWidth, int cellHeight) {
|
||||||
|
updateDisplayMetrics(cellWidth, cellHeight);
|
||||||
|
int px = gx + getXMax();
|
||||||
|
int py = getYMax() - gy;
|
||||||
|
int sx = ox + (int) Math.round(px * transformX);
|
||||||
|
int sy = oy + (int) Math.round(py * transformY);
|
||||||
|
return new Point(sx, sy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a mouse click in Swing screen coordinates (mouseX, mouseY) directly to GOCA signed coordinate (gx, gy).
|
||||||
|
* Matches HODInput.getHODInput:
|
||||||
|
* x = mouseX / transformX
|
||||||
|
* y = mouseY / transformY
|
||||||
|
* gx = x - xMax
|
||||||
|
* gy = yMax - y
|
||||||
|
*/
|
||||||
|
public Point screenPixelToGoca(int mouseX, int mouseY, int ox, int oy, int cellWidth, int cellHeight) {
|
||||||
|
updateDisplayMetrics(cellWidth, cellHeight);
|
||||||
|
double relX = mouseX - ox;
|
||||||
|
double relY = mouseY - oy;
|
||||||
|
|
||||||
|
int px = (transformX > 0) ? (int) Math.round(relX / transformX) : (int) relX;
|
||||||
|
int py = (transformY > 0) ? (int) Math.round(relY / transformY) : (int) relY;
|
||||||
|
|
||||||
|
int gx = px - getXMax();
|
||||||
|
int gy = getYMax() - py;
|
||||||
|
return new Point(gx, gy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps GOCA (gx, gy) to fixed canvas buffer dimensions (canvasWidth, canvasHeight).
|
||||||
|
*/
|
||||||
|
public Point gocaToCanvasPixel(int gx, int gy, int canvasWidth, int canvasHeight) {
|
||||||
|
int totalW = getTotalWidth();
|
||||||
|
int totalH = getTotalHeight();
|
||||||
|
int xMax = getXMax();
|
||||||
|
int yMax = getYMax();
|
||||||
|
|
||||||
|
int px = (int) Math.round((double) (gx + xMax) * canvasWidth / (totalW > 0 ? totalW : 1));
|
||||||
|
int py = (int) Math.round((double) (yMax - gy) * canvasHeight / (totalH > 0 ? totalH : 1));
|
||||||
|
return new Point(px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps fixed canvas buffer pixel (canvasX, canvasY) to GOCA (gx, gy).
|
||||||
|
*/
|
||||||
|
public Point canvasPixelToGoca(int canvasX, int canvasY, int canvasWidth, int canvasHeight) {
|
||||||
|
int totalW = getTotalWidth();
|
||||||
|
int totalH = getTotalHeight();
|
||||||
|
int xMax = getXMax();
|
||||||
|
int yMax = getYMax();
|
||||||
|
|
||||||
|
int nx = (int) Math.round((double) canvasX * totalW / (canvasWidth > 0 ? canvasWidth : 1));
|
||||||
|
int ny = (int) Math.round((double) canvasY * totalH / (canvasHeight > 0 ? canvasHeight : 1));
|
||||||
|
return new Point(nx - xMax, yMax - ny);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants for IBM 3179G / 3270-PC GOCA (Graphics Object Content Architecture)
|
* Constants for IBM 3179G / 3270-PC GOCA (Graphics Object Content Architecture)
|
||||||
@@ -54,12 +54,14 @@ public final class GocaConstants {
|
|||||||
public static final int G_GSVW = 0x27; // Set Viewing Window
|
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_GSPT = 0x28; // Set Pattern Symbol
|
||||||
public static final int G_GSMT = 0x29; // Set Marker Symbol / Type
|
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_GSCH = 0x33; // Set Character Cell
|
||||||
public static final int G_GSCA = 0x34; // Set Character Angle
|
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_GSCR = 0x35; // Set Character Shear
|
||||||
public static final int G_GSMCEL = 0x37; // Set Marker Cell
|
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_GSCS = 0x38; // Set Character Set
|
||||||
public static final int G_GSMP = 0x39; // Set Marker Precision
|
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_GSCD = 0x3A; // Set Character Direction
|
||||||
public static final int G_GSCC = 0x3B; // Set Character Precision
|
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_GSMS_SET = 0x3C; // Set Marker Set
|
||||||
@@ -103,6 +105,12 @@ public final class GocaConstants {
|
|||||||
public static final int LW_NORMAL = 1;
|
public static final int LW_NORMAL = 1;
|
||||||
public static final int LW_THICK = 2;
|
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)
|
// Fill Patterns (GSPT)
|
||||||
public static final int PT_DEFAULT = 0;
|
public static final int PT_DEFAULT = 0;
|
||||||
public static final int PT_D1 = 1;
|
public static final int PT_D1 = 1;
|
||||||
@@ -176,6 +184,9 @@ public final class GocaConstants {
|
|||||||
* Returns Green (0xFF00FF00) if the index is out of range.
|
* Returns Green (0xFF00FF00) if the index is out of range.
|
||||||
*/
|
*/
|
||||||
public static int getGocaColorArgb(int colorIndex) {
|
public static int getGocaColorArgb(int colorIndex) {
|
||||||
|
if (colorIndex == 0xFF) {
|
||||||
|
return GOCA_COLORS[7];
|
||||||
|
}
|
||||||
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
|
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
|
||||||
return GOCA_COLORS[colorIndex];
|
return GOCA_COLORS[colorIndex];
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
|||||||
|
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).
|
||||||
|
// Note on Structured Field Length (bytes 0-1):
|
||||||
|
// IBM HOD sets bytes 0-1 to 0x00 0x34 (52 decimal). In 3270 GOCA architecture,
|
||||||
|
// the Data Unit Object Control payload is 52 bytes. Overwriting this with 0x00 0x38 (56)
|
||||||
|
// causes the host's 3270 inbound structured field decoder to misalign the trailing AID byte
|
||||||
|
// (0x7D) and cursor address by 4 bytes, causing host GDDM to reject the click with an alarm beep.
|
||||||
|
private static final byte[] MASK = new byte[] {
|
||||||
|
0x00, 0x34, 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, 0x04,
|
||||||
|
0x00, 0x04, 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, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag (legacy overload).
|
||||||
|
* Note: Per IBM HOD / 3179G architecture, pick correlation is evaluated host-side by GDDM using (gx, gy).
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
return buildGraphicInput(gocaX, gocaY, row, col, buttonOrAidCode, isMouseAction, isShift, isCtrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the 56-byte Graphic Input Structured Field matching IBM Host On-Demand (HODInput.java).
|
||||||
|
*/
|
||||||
|
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
|
||||||
|
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
||||||
|
byte[] sf = new byte[MASK.length];
|
||||||
|
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
||||||
|
|
||||||
|
// Bytes 0-1: Structured Field Length (0x0034 = 52 decimal per IBM HODInput.java / GOCA architecture).
|
||||||
|
// Preserved from MASK; do not overwrite with MASK.length (56).
|
||||||
|
|
||||||
|
// Bytes 16-19: Device correlation class descriptor (0x23, 0x00, 0x23, 0x00)
|
||||||
|
// Note: Preserved from MASK per IBM Host On-Demand (HODInput.java); do not overwrite with row/col.
|
||||||
|
|
||||||
|
// 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: Fixed mouse trigger class constant (0x00000004 per IBM HODInput.java)
|
||||||
|
sf[28] = 0x00;
|
||||||
|
sf[29] = 0x00;
|
||||||
|
sf[30] = 0x00;
|
||||||
|
sf[31] = 0x04;
|
||||||
|
|
||||||
|
// Byte 32-33: Fixed mouse correlation class constant (0x0004 per IBM HODInput.java)
|
||||||
|
sf[32] = 0x00;
|
||||||
|
sf[33] = 0x04;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Graphics modes supported by j3270 / lib3270j.
|
* Graphics modes supported by j3270 / lib3270j.
|
||||||
@@ -0,0 +1,952 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3179G Presentation Space metrics: 9x16 cell pitch (720x384 for Model 2, 720x688 for Model 4)
|
||||||
|
private int screenCols = 80;
|
||||||
|
private int screenRows = 24;
|
||||||
|
private int canvasWidth = 720;
|
||||||
|
private int canvasHeight = 384;
|
||||||
|
private int[] rgbBuffer;
|
||||||
|
private boolean hasContent = false;
|
||||||
|
private long updateCount = 0;
|
||||||
|
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;
|
||||||
|
this.transform.setScreenDimensions(this.screenCols, this.screenRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getScreenCols() {
|
||||||
|
return screenCols;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getScreenRows() {
|
||||||
|
return screenRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedicated GDDM coordinate transformation scaling layer (matching IBM HODTransform)
|
||||||
|
private final GddmCoordinateTransform transform = new GddmCoordinateTransform();
|
||||||
|
|
||||||
|
public GddmCoordinateTransform getTransform() {
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalWidth() {
|
||||||
|
int cols = screenCols > 0 ? screenCols : 80;
|
||||||
|
return cols * 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total vertical presentation space units.
|
||||||
|
*
|
||||||
|
* <p>Historical / Architecture Note for Future Iterations:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Failed Approach 1 (Hardcoded 12-pitch / rows * 12)</b>:
|
||||||
|
* Used 516 units (yMax = 257) on 43-row screen. Caused upper coordinate truncation;
|
||||||
|
* menus at gy > 257 were clipped and mathematically impossible to pick.</li>
|
||||||
|
* <li><b>Failed Approach 2 (Hardcoded 16-pitch / rows * 16 without viewport alignment)</b>:
|
||||||
|
* Used 688 units (yMax = 343). Visual rendering was shifted downward by ~50 pixels relative
|
||||||
|
* to GDDM's internal picture space viewport, causing clicks at Row 7 to emit gy = 226
|
||||||
|
* which missed the GDDM menu hit box and caused terminal alarm beeps.</li>
|
||||||
|
* <li><b>Failed Approach 3 (Overwriting Graphic Input SF bytes 16-19 with row/col)</b>:
|
||||||
|
* Overwrote device correlation descriptor 0x00230023, causing GDDM Structured Field
|
||||||
|
* parser to reject inbound correlation packets as malformed.</li>
|
||||||
|
* <li><b>Failed Approach 4 (Misinterpreting Procedure Order 0x21 as Segment Redraw loop)</b>:
|
||||||
|
* Order 0x21 is HODCurrentDefaults (Set Current Defaults), not an executive redraw loop.
|
||||||
|
* Treating 0x21 as segment execution caused previous menu frames and stored segments
|
||||||
|
* to be repeatedly repainted over active screens, leaving behind visual ghost artifacts.</li>
|
||||||
|
* <li><b>Failed Approach 5 (Static yMax=343 scaling vs GDDM ADMDRAW Viewport bounds)</b>:
|
||||||
|
* In ADMDRAW, GDDM defines the graphics presentation space between y = -169 (canvas bottom)
|
||||||
|
* and y = +200 (top menu bar), with total height ~370 units. Mapping this into a static
|
||||||
|
* yMax = 343 (688 total height) renders the top menu at Row 9.2 (middle of the screen) with
|
||||||
|
* a large vertical void above it, and clicking the visual menu sends gy = 224 which misses
|
||||||
|
* the [187..200] menu bounding box. Furthermore, multiple dropdown menus (FILE, DRAW, TRANSFORM)
|
||||||
|
* overlapped simultaneously because segment retention and clearing was decoupled from GDDM's
|
||||||
|
* actual viewport state.</li>
|
||||||
|
* <li><b>Solution Path</b>: Use {@link GddmCoordinateTransform} for dynamic display and aspect
|
||||||
|
* scaling, ensuring host Query Reply metrics (SDH/AH) and GDDM Picture Space Window
|
||||||
|
* orders are synchronized with client-side mouse transformation.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public int getTotalHeight() {
|
||||||
|
int rows = screenRows > 0 ? screenRows : 24;
|
||||||
|
return rows * 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 * 16).
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
|
||||||
|
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
|
||||||
|
// Background Mix (GSBMX / bgMix):
|
||||||
|
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
|
||||||
|
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
|
||||||
|
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
|
||||||
|
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
|
||||||
|
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
|
||||||
|
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
|
||||||
|
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
||||||
|
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
|
||||||
|
|
||||||
|
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
||||||
|
// 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-16
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
@@ -87,8 +87,11 @@ public class ProgramSymbolManager {
|
|||||||
}
|
}
|
||||||
ProgramSymbolSet set = lcidMap[lcid];
|
ProgramSymbolSet set = lcidMap[lcid];
|
||||||
if (set == null) {
|
if (set == null) {
|
||||||
|
for (ProgramSymbolSet s : sets) {
|
||||||
|
if (s != null && s.getLcid() == lcid) return s;
|
||||||
|
}
|
||||||
for (ProgramSymbolSet s : stagingSets) {
|
for (ProgramSymbolSet s : stagingSets) {
|
||||||
if (s.getLcid() == lcid) return s;
|
if (s != null && s.getLcid() == lcid) return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return set;
|
return set;
|
||||||
@@ -98,18 +101,7 @@ public class ProgramSymbolManager {
|
|||||||
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
|
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
|
||||||
*/
|
*/
|
||||||
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
|
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
|
||||||
if (lcid <= 0 || lcid >= 256) {
|
ProgramSymbolSet set = getSymbolSet(lcid);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
ProgramSymbolSet set = lcidMap[lcid];
|
|
||||||
if (set == null) {
|
|
||||||
for (ProgramSymbolSet s : stagingSets) {
|
|
||||||
if (s.getLcid() == lcid) {
|
|
||||||
set = s;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (set == null) {
|
if (set == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -153,7 +145,7 @@ public class ProgramSymbolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||||
ProgramSymbolSet set = isTriplePlane ? stagingSets[setIndex] : sets[setIndex];
|
ProgramSymbolSet set = sets[setIndex];
|
||||||
|
|
||||||
int extHeaderLen = 0;
|
int extHeaderLen = 0;
|
||||||
int cellWidth = defaultCellWidth;
|
int cellWidth = defaultCellWidth;
|
||||||
@@ -176,7 +168,7 @@ public class ProgramSymbolManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set.setLcid(lcid);
|
set.setLcid(lcid);
|
||||||
if (!isTriplePlane && lcid > 0 && lcid < 256) {
|
if (lcid > 0 && lcid < 256) {
|
||||||
lcidMap[lcid] = set;
|
lcidMap[lcid] = set;
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
/*
|
/*
|
||||||
* Decompiled with CFR 0.152.
|
* Decompiled with CFR 0.152.
|
||||||
*/
|
*/
|
||||||
package org.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
import org.lib3270j.graphics.GocaConstants;
|
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||||
|
|
||||||
|
|
||||||
public final class VectorSymbolData {
|
public final class VectorSymbolData {
|
||||||
+325
-71
@@ -1,11 +1,11 @@
|
|||||||
package org.lib3270j.input;
|
package haus.nightmare.lib3270j.input;
|
||||||
|
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import org.lib3270j.screen.ExtendedAttribute;
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import org.lib3270j.telnet.TelnetFSM;
|
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||||
import org.lib3270j.protocol.TelnetConstants;
|
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -31,12 +31,21 @@ public class InputProcessor {
|
|||||||
this.fsm = fsm;
|
this.fsm = fsm;
|
||||||
}
|
}
|
||||||
|
|
||||||
private org.lib3270j.graphics.GraphicsPlane graphicsPlane;
|
private haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane;
|
||||||
|
private haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder;
|
||||||
|
|
||||||
public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) {
|
public void setGraphicsPlane(haus.nightmare.lib3270j.graphics.GraphicsPlane gp) {
|
||||||
this.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 {
|
public enum OiaStatus {
|
||||||
NOT_CONNECTED("OFFLINE"),
|
NOT_CONNECTED("OFFLINE"),
|
||||||
X_SYSTEM("X SYSTEM"),
|
X_SYSTEM("X SYSTEM"),
|
||||||
@@ -86,8 +95,10 @@ public class InputProcessor {
|
|||||||
public void typeCharacter(char ch) {
|
public void typeCharacter(char ch) {
|
||||||
if (keyboardLocked) return;
|
if (keyboardLocked) return;
|
||||||
|
|
||||||
int baddr = screen.getCursorAddress();
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
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
|
// Check if cursor is at a field attribute or in a protected field
|
||||||
ExtendedAttribute ea = screen.getCell(baddr);
|
ExtendedAttribute ea = screen.getCell(baddr);
|
||||||
@@ -111,8 +122,10 @@ public class InputProcessor {
|
|||||||
// Insert mode: shift characters right from cursor to end of field
|
// Insert mode: shift characters right from cursor to end of field
|
||||||
// Find end of field
|
// Find end of field
|
||||||
int endAddr = baddr;
|
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);
|
endAddr = screen.incrementAddress(endAddr);
|
||||||
|
count++;
|
||||||
if (endAddr == baddr) break; // wrapped around (unformatted)
|
if (endAddr == baddr) break; // wrapped around (unformatted)
|
||||||
}
|
}
|
||||||
// Check if last position is non-null (field overflow)
|
// Check if last position is non-null (field overflow)
|
||||||
@@ -122,11 +135,13 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
// Shift right from endAddr-1 down to baddr
|
// Shift right from endAddr-1 down to baddr
|
||||||
int dst = endAddr;
|
int dst = endAddr;
|
||||||
while (dst != baddr) {
|
int shiftCount = 0;
|
||||||
|
while (dst != baddr && shiftCount < size) {
|
||||||
int src = screen.decrementAddress(dst);
|
int src = screen.decrementAddress(dst);
|
||||||
screen.getCell(dst).ec = screen.getCell(src).ec;
|
screen.getCell(dst).ec = screen.getCell(src).ec;
|
||||||
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
|
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
|
||||||
dst = src;
|
dst = src;
|
||||||
|
shiftCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,19 +161,26 @@ public class InputProcessor {
|
|||||||
// Advance cursor
|
// Advance cursor
|
||||||
int startAdvance = baddr;
|
int startAdvance = baddr;
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
|
int advCount = 0;
|
||||||
// Skip over field attributes safely
|
// Skip over field attributes safely
|
||||||
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance) {
|
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance && advCount < size) {
|
||||||
baddr = (baddr + 1) % size;
|
baddr = (baddr + 1) % size;
|
||||||
|
advCount++;
|
||||||
}
|
}
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
|
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
|
||||||
*/
|
*/
|
||||||
public void sendAid(int aidCode) {
|
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;
|
lastAid = aidCode;
|
||||||
setKeyboardLocked(true);
|
setKeyboardLocked(true);
|
||||||
@@ -206,20 +228,61 @@ public class InputProcessor {
|
|||||||
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
|
||||||
screen.setCursorAddress(nextRowAddr);
|
screen.setCursorAddress(nextRowAddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
setKeyboardLocked(false);
|
setKeyboardLocked(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
|
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[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
byte[] data = new byte[] { (byte) aidCode, caddr[0], caddr[1] };
|
out.write(caddr[0] & 0xFF);
|
||||||
sendAidResponse(data);
|
out.write(caddr[1] & 0xFF);
|
||||||
|
sendAidResponse(out.toByteArray());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter, PF keys: send AID + cursor address + modified field data
|
if (aidCode == AID_SELECT) {
|
||||||
|
// 3270 Selector Pen / Light Pen Immediate Selection (AID 0x7E):
|
||||||
|
// Per IBM 3270 Data Stream Architecture (GA23-0059) and IBM Host On-Demand (DS3270.sendAid lines 727-1019):
|
||||||
|
// The inbound data stream consists of:
|
||||||
|
// 1. AID byte (0x7E)
|
||||||
|
// 2. Cursor address (2 bytes)
|
||||||
|
// 3. For each field with MDT=1:
|
||||||
|
// - SBA order (0x11)
|
||||||
|
// - Designator character address (faAddr + 1)
|
||||||
|
// CRITICAL: NO FIELD CHARACTER DATA IS TRANSMITTED FOR AID_SELECT!
|
||||||
|
// Sending character data in an AID_SELECT stream violates 3270 protocol and causes the host to reject the selection.
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(AID_SELECT);
|
||||||
|
|
||||||
|
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
|
out.write(caddr[0] & 0xFF);
|
||||||
|
out.write(caddr[1] & 0xFF);
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
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 designatorAddr = (i + 1) % size;
|
||||||
|
out.write(ORDER_SBA);
|
||||||
|
byte[] addr = encodeAddress(designatorAddr, screen.getRows(), screen.getCols());
|
||||||
|
out.write(addr[0] & 0xFF);
|
||||||
|
out.write(addr[1] & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendAidResponse(out.toByteArray());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter, PF keys: send AID + optional PID + cursor address + modified field data
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|
||||||
out.write(aidCode);
|
out.write(aidCode);
|
||||||
|
|
||||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
@@ -228,65 +291,42 @@ public class InputProcessor {
|
|||||||
|
|
||||||
if (screen.isFormatted()) {
|
if (screen.isFormatted()) {
|
||||||
// Send modified fields with SBA
|
// 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();
|
int size = screen.getRows() * screen.getCols();
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
ExtendedAttribute ea = screen.getCell(i);
|
ExtendedAttribute ea = screen.getCell(i);
|
||||||
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
|
||||||
int fieldStart = (i + 1) % size;
|
int fieldStart = (i + 1) % size;
|
||||||
|
|
||||||
// First, collect field data and find last non-null byte
|
// Always send SBA and address of first character in field
|
||||||
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
|
|
||||||
out.write(ORDER_SBA);
|
out.write(ORDER_SBA);
|
||||||
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||||
out.write(addr[0] & 0xFF);
|
out.write(addr[0] & 0xFF);
|
||||||
out.write(addr[1] & 0xFF);
|
out.write(addr[1] & 0xFF);
|
||||||
|
|
||||||
// Only send data if there is any (strip trailing nulls)
|
// Send all non-null characters in field (suppressing 0x00)
|
||||||
if (lastNonNull >= 0) {
|
int pos = fieldStart;
|
||||||
byte[] allData = fieldData.toByteArray();
|
while (!screen.getCell(pos).isFieldAttribute()) {
|
||||||
out.write(allData, 0, lastNonNull + 1);
|
int b = screen.getCell(pos).ec & 0xFF;
|
||||||
|
if (b != 0x00) {
|
||||||
|
out.write(b);
|
||||||
|
}
|
||||||
|
pos = (pos + 1) % size;
|
||||||
|
if (pos == fieldStart) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unformatted screen in 3270 mode (e.g. line-mode console):
|
// Unformatted screen in 3270 mode:
|
||||||
// Send AID + cursor address + only the active input line (row containing cursor)
|
// Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line
|
||||||
int cols = screen.getCols();
|
// or we can just send everything up to the last non-null on the screen.
|
||||||
int curAddr = screen.getCursorAddress();
|
// IBM spec: "all alphanumeric characters... Nulls are suppressed."
|
||||||
int row = curAddr / cols;
|
// Actually, the simplest is to send everything, but suppress nulls.
|
||||||
int rowStart = row * cols;
|
int size = screen.getRows() * screen.getCols();
|
||||||
int rowEnd = rowStart + cols;
|
for (int i = 0; i < size; i++) {
|
||||||
|
|
||||||
// Find last non-null, non-blank character on the current line
|
|
||||||
int lastChar = rowStart - 1;
|
|
||||||
for (int i = rowEnd - 1; i >= rowStart; i--) {
|
|
||||||
int ec = screen.getCell(i).ec & 0xFF;
|
|
||||||
if (ec != 0x00 && ec != 0x40) {
|
|
||||||
lastChar = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastChar >= rowStart) {
|
|
||||||
for (int i = rowStart; i <= lastChar; i++) {
|
|
||||||
int b = screen.getCell(i).ec & 0xFF;
|
int b = screen.getCell(i).ec & 0xFF;
|
||||||
out.write(b != 0 ? b : 0x40);
|
if (b != 0x00) {
|
||||||
|
out.write(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,7 +334,7 @@ public class InputProcessor {
|
|||||||
sendAidResponse(out.toByteArray());
|
sendAidResponse(out.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendAidResponse(byte[] data) {
|
protected void sendAidResponse(byte[] data) {
|
||||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
||||||
fsm.sendSscpLuData(data);
|
fsm.sendSscpLuData(data);
|
||||||
} else if (fsm != null) {
|
} else if (fsm != null) {
|
||||||
@@ -302,13 +342,179 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
|
||||||
|
*/
|
||||||
|
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||||
|
if (fsm != null && fsm.getConnectionState() != 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 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
|
||||||
|
);
|
||||||
|
|
||||||
|
StringBuilder sfHex = new StringBuilder();
|
||||||
|
for (byte b : sf) {
|
||||||
|
sfHex.append(String.format("%02X ", b & 0xFF));
|
||||||
|
}
|
||||||
|
log.info(String.format(
|
||||||
|
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
|
||||||
|
gx, gy, row, col, button, isShift, isCtrl, sfHex.toString().trim()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
||||||
|
out.write(AID_SF);
|
||||||
|
try {
|
||||||
|
out.write(sf);
|
||||||
|
} catch (java.io.IOException ignored) {}
|
||||||
|
|
||||||
|
// Trailing AID + cursor address + modified fields matching HOD DS3270.sendMouseAid
|
||||||
|
out.write(aidCode);
|
||||||
|
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||||
|
out.write(caddr[0] & 0xFF);
|
||||||
|
out.write(caddr[1] & 0xFF);
|
||||||
|
|
||||||
|
if (screen.isFormatted()) {
|
||||||
|
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;
|
||||||
|
out.write(ORDER_SBA);
|
||||||
|
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
|
||||||
|
out.write(addr[0] & 0xFF);
|
||||||
|
out.write(addr[1] & 0xFF);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 ==========
|
// ========== Cursor movement ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate a Text Light Pen selection at the specified screen address.
|
||||||
|
* Conforms to IBM 3270 / Host On-Demand PS3270 processCurSelKey specifications.
|
||||||
|
*/
|
||||||
|
public boolean lightPenSelect(int address) {
|
||||||
|
if (screen == null || !screen.isFormatted()) {
|
||||||
|
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) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||||
|
int fa = faCell.fa & 0xFF;
|
||||||
|
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) ||
|
||||||
|
haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int designatorPos = (faPos + 1) % size;
|
||||||
|
ExtendedAttribute desCell = screen.getCell(designatorPos);
|
||||||
|
int ebcdic = desCell.ec & 0xFF;
|
||||||
|
char ascii = (char) desCell.ucs4;
|
||||||
|
|
||||||
|
// Set cursor to the clicked/selected position matching HOD / 3270 standards
|
||||||
|
screen.setCursorAddress(address);
|
||||||
|
|
||||||
|
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
|
||||||
|
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
|
||||||
|
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
|
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Enter Immediate (&) -> AID_ENTER (0x7D)
|
||||||
|
else if (ebcdic == 0x50 || ascii == '&' || ascii == '\uFF06') {
|
||||||
|
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
|
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Deferred Selection (? -> >)
|
||||||
|
else if (ebcdic == 0x6F || ascii == '?' || ascii == '\uFF1F') {
|
||||||
|
desCell.ec = (byte) 0x6E;
|
||||||
|
desCell.ucs4 = '>';
|
||||||
|
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Deferred Deselection (> -> ?)
|
||||||
|
else if (ebcdic == 0x6E || ascii == '>' || ascii == '\uFF1E') {
|
||||||
|
desCell.ec = (byte) 0x6F;
|
||||||
|
desCell.ucs4 = '?';
|
||||||
|
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Any other character is an invalid designator and cannot be selected
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate the 3270 CURSR SEL (Cursor Select) key at the current cursor position.
|
||||||
|
* Equivalent to processCurSelKey in IBM Host On-Demand PS3270.
|
||||||
|
*/
|
||||||
|
public boolean cursorSelect() {
|
||||||
|
if (screen == null) return false;
|
||||||
|
return lightPenSelect(screen.getCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
public void cursorUp() {
|
public void cursorUp() {
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
addr -= screen.getCols();
|
addr -= screen.getCols();
|
||||||
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
if (addr < 0) addr += screen.getRows() * screen.getCols();
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorDown() {
|
public void cursorDown() {
|
||||||
@@ -316,18 +522,21 @@ public class InputProcessor {
|
|||||||
addr += screen.getCols();
|
addr += screen.getCols();
|
||||||
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorLeft() {
|
public void cursorLeft() {
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
addr = screen.decrementAddress(addr);
|
addr = screen.decrementAddress(addr);
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorRight() {
|
public void cursorRight() {
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
addr = screen.incrementAddress(addr);
|
addr = screen.incrementAddress(addr);
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cursorHome() {
|
public void cursorHome() {
|
||||||
@@ -336,11 +545,13 @@ public class InputProcessor {
|
|||||||
} else {
|
} else {
|
||||||
screen.setCursorAddress(0);
|
screen.setCursorAddress(0);
|
||||||
}
|
}
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tab() {
|
public void tab() {
|
||||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||||
screen.setCursorAddress(addr);
|
screen.setCursorAddress(addr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLastAid() { return lastAid; }
|
public int getLastAid() { return lastAid; }
|
||||||
@@ -348,23 +559,29 @@ public class InputProcessor {
|
|||||||
|
|
||||||
public void setCursorAddress(int baddr) {
|
public void setCursorAddress(int baddr) {
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backTab() {
|
public void backTab() {
|
||||||
// Find previous unprotected field
|
if (!screen.isFormatted()) return;
|
||||||
int addr = screen.getCursorAddress();
|
|
||||||
int size = screen.getRows() * screen.getCols();
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return;
|
||||||
|
int addr = screen.getCursorAddress();
|
||||||
|
addr = ((addr % size) + size) % size;
|
||||||
int start = screen.decrementAddress(addr);
|
int start = screen.decrementAddress(addr);
|
||||||
addr = start;
|
addr = start;
|
||||||
|
int count = 0;
|
||||||
do {
|
do {
|
||||||
addr = screen.decrementAddress(addr);
|
addr = screen.decrementAddress(addr);
|
||||||
if (screen.getCell(addr).isFieldAttribute()) {
|
if (screen.getCell(addr).isFieldAttribute()) {
|
||||||
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
|
||||||
screen.setCursorAddress(screen.incrementAddress(addr));
|
screen.setCursorAddress(screen.incrementAddress(addr));
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (addr != start);
|
count++;
|
||||||
|
} while (addr != start && count < size);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void eraseEof() {
|
public void eraseEof() {
|
||||||
@@ -377,19 +594,24 @@ public class InputProcessor {
|
|||||||
ea.ucs4 = 0;
|
ea.ucs4 = 0;
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int addr = screen.getCursorAddress();
|
int addr = screen.getCursorAddress();
|
||||||
int size = screen.getRows() * screen.getCols();
|
int size = screen.getRows() * screen.getCols();
|
||||||
|
if (size <= 0) return;
|
||||||
|
addr = ((addr % size) + size) % size;
|
||||||
byte faVal = screen.getFieldAttributeAt(addr);
|
byte faVal = screen.getFieldAttributeAt(addr);
|
||||||
if (faIsProtected(faVal & 0xFF)) return;
|
if (faIsProtected(faVal & 0xFF)) return;
|
||||||
|
|
||||||
// Erase from cursor to end of field
|
// 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);
|
ExtendedAttribute ea = screen.getCell(addr);
|
||||||
ea.ec = 0;
|
ea.ec = 0;
|
||||||
ea.ucs4 = 0;
|
ea.ucs4 = 0;
|
||||||
addr = screen.incrementAddress(addr);
|
addr = screen.incrementAddress(addr);
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set MDT
|
// Set MDT
|
||||||
@@ -398,18 +620,26 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteChar() {
|
public void deleteChar() {
|
||||||
if (!screen.isFormatted()) return;
|
if (!screen.isFormatted()) return;
|
||||||
int addr = screen.getCursorAddress();
|
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);
|
byte faVal = screen.getFieldAttributeAt(addr);
|
||||||
if (faIsProtected(faVal & 0xFF)) return;
|
if (faIsProtected(faVal & 0xFF)) return;
|
||||||
|
|
||||||
// Shift characters left within the field
|
// Shift characters left within the field
|
||||||
int shiftAddr = addr;
|
int shiftAddr = addr;
|
||||||
int size = screen.getRows() * screen.getCols();
|
int count = 0;
|
||||||
while (true) {
|
while (count < size) {
|
||||||
int next = screen.incrementAddress(shiftAddr);
|
int next = screen.incrementAddress(shiftAddr);
|
||||||
if (screen.getCell(next).isFieldAttribute()) {
|
if (screen.getCell(next).isFieldAttribute()) {
|
||||||
screen.getCell(shiftAddr).ec = 0;
|
screen.getCell(shiftAddr).ec = 0;
|
||||||
@@ -419,6 +649,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
|
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
|
||||||
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
|
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
|
||||||
shiftAddr = next;
|
shiftAddr = next;
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
int faAddr = screen.findFieldAttribute(addr);
|
int faAddr = screen.findFieldAttribute(addr);
|
||||||
@@ -426,11 +657,23 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void backspace() {
|
public void backspace() {
|
||||||
if (screen.getCursorAddress() == 0) return;
|
int addr = screen.getCursorAddress();
|
||||||
cursorLeft();
|
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();
|
deleteChar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +682,7 @@ public class InputProcessor {
|
|||||||
if (keyboardLocked) return;
|
if (keyboardLocked) return;
|
||||||
screen.eraseAllUnprotected();
|
screen.eraseAllUnprotected();
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
|
/** Move cursor to first unprotected field on next line (Newline key in 3270). */
|
||||||
@@ -455,6 +699,7 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
screen.setCursorAddress(target);
|
screen.setCursorAddress(target);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert Duplicate (DUP) code and advance to next field. */
|
/** Insert Duplicate (DUP) code and advance to next field. */
|
||||||
@@ -472,6 +717,8 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
tab();
|
tab();
|
||||||
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Insert Field Mark (FM) code. */
|
/** Insert Field Mark (FM) code. */
|
||||||
@@ -495,6 +742,7 @@ public class InputProcessor {
|
|||||||
}
|
}
|
||||||
screen.setCursorAddress(baddr);
|
screen.setCursorAddress(baddr);
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attention key (sends Telnet IP). */
|
/** Attention key (sends Telnet IP). */
|
||||||
@@ -571,6 +819,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(i).ucs4 = 0;
|
screen.getCell(i).ucs4 = 0;
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,11 +844,13 @@ public class InputProcessor {
|
|||||||
int fieldLen = 0;
|
int fieldLen = 0;
|
||||||
int pos = fieldStart;
|
int pos = fieldStart;
|
||||||
int size = screen.getRows() * screen.getCols();
|
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).ec = 0;
|
||||||
screen.getCell(pos).ucs4 = 0;
|
screen.getCell(pos).ucs4 = 0;
|
||||||
fieldLen++;
|
fieldLen++;
|
||||||
pos = screen.incrementAddress(pos);
|
pos = screen.incrementAddress(pos);
|
||||||
|
count++;
|
||||||
if (pos == fieldStart) break;
|
if (pos == fieldStart) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +859,7 @@ public class InputProcessor {
|
|||||||
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
|
||||||
}
|
}
|
||||||
screen.markAllChanged();
|
screen.markAllChanged();
|
||||||
|
screen.updateDisplaySnapshot();
|
||||||
return fieldLen;
|
return fieldLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,8 +868,10 @@ public class InputProcessor {
|
|||||||
* Used by DFT mode file transfer.
|
* Used by DFT mode file transfer.
|
||||||
*/
|
*/
|
||||||
public void sendStructuredFieldData(byte[] data) {
|
public void sendStructuredFieldData(byte[] data) {
|
||||||
|
if (fsm != null) {
|
||||||
fsm.send3270Data(data);
|
fsm.send3270Data(data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Get a reference to the TelnetFSM for direct SF operations. */
|
/** Get a reference to the TelnetFSM for direct SF operations. */
|
||||||
public TelnetFSM getTelnetFSM() {
|
public TelnetFSM getTelnetFSM() {
|
||||||
+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.
|
* 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.
|
* Listener for screen buffer changes.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.protocol;
|
package haus.nightmare.lib3270j.protocol;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 3270 Data Stream protocol constants.
|
* 3270 Data Stream protocol constants.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.protocol;
|
package haus.nightmare.lib3270j.protocol;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TN3270E protocol constants per RFC 2355.
|
* 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.
|
* Telnet protocol constants from RFC 854 and extensions.
|
||||||
@@ -76,6 +76,7 @@ public final class TelnetConstants {
|
|||||||
case TELOPT_BINARY: return "BINARY";
|
case TELOPT_BINARY: return "BINARY";
|
||||||
case TELOPT_ECHO: return "ECHO";
|
case TELOPT_ECHO: return "ECHO";
|
||||||
case TELOPT_SGA: return "SGA";
|
case TELOPT_SGA: return "SGA";
|
||||||
|
case TELOPT_TM: return "TIMING-MARK";
|
||||||
case TELOPT_TTYPE: return "TTYPE";
|
case TELOPT_TTYPE: return "TTYPE";
|
||||||
case TELOPT_EOR: return "EOR";
|
case TELOPT_EOR: return "EOR";
|
||||||
case TELOPT_NAWS: return "NAWS";
|
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.
|
* Extended attribute structure for a single screen buffer position.
|
||||||
+61
-14
@@ -1,8 +1,8 @@
|
|||||||
package org.lib3270j.screen;
|
package haus.nightmare.lib3270j.screen;
|
||||||
|
|
||||||
import org.lib3270j.TerminalModel;
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 3270 screen buffer.
|
* The 3270 screen buffer.
|
||||||
@@ -25,6 +25,9 @@ public class ScreenBuffer {
|
|||||||
private boolean formatted; // Screen has at least one field attribute?
|
private boolean formatted; // Screen has at least one field attribute?
|
||||||
private byte replyMode = SF_SRM_FIELD;
|
private byte replyMode = SF_SRM_FIELD;
|
||||||
|
|
||||||
|
private int activePartition = 0; // 0 = implicit partition
|
||||||
|
private boolean explicitPartitionActive = false;
|
||||||
|
|
||||||
// Change tracking
|
// Change tracking
|
||||||
private boolean screenChanged;
|
private boolean screenChanged;
|
||||||
private int firstChanged = -1;
|
private int firstChanged = -1;
|
||||||
@@ -60,6 +63,7 @@ public class ScreenBuffer {
|
|||||||
defaultFA.ic = 1;
|
defaultFA.ic = 1;
|
||||||
|
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void allocateBuffers() {
|
private void allocateBuffers() {
|
||||||
@@ -139,7 +143,7 @@ public class ScreenBuffer {
|
|||||||
public boolean isScreenAlt() { return screenAlt; }
|
public boolean isScreenAlt() { return screenAlt; }
|
||||||
|
|
||||||
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
|
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
|
||||||
public void setAlternateDimensions(int newAltRows, int newAltCols) {
|
public synchronized void setAlternateDimensions(int newAltRows, int newAltCols) {
|
||||||
if (newAltRows == altRows && newAltCols == altCols) return;
|
if (newAltRows == altRows && newAltCols == altCols) return;
|
||||||
this.altRows = newAltRows;
|
this.altRows = newAltRows;
|
||||||
this.altCols = newAltCols;
|
this.altCols = newAltCols;
|
||||||
@@ -149,11 +153,15 @@ public class ScreenBuffer {
|
|||||||
this.maxCols = Math.max(maxCols, newAltCols);
|
this.maxCols = Math.max(maxCols, newAltCols);
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
}
|
}
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Cursor ==========
|
// ========== Cursor ==========
|
||||||
public int getCursorAddress() { return cursorAddress; }
|
public int getCursorAddress() { return cursorAddress; }
|
||||||
public void setCursorAddress(int addr) { this.cursorAddress = addr; }
|
public synchronized void setCursorAddress(int addr) {
|
||||||
|
this.cursorAddress = addr;
|
||||||
|
this.displayCursorAddress = addr;
|
||||||
|
}
|
||||||
public int getCursorRow() { return cursorAddress / cols; }
|
public int getCursorRow() { return cursorAddress / cols; }
|
||||||
public int getCursorCol() { return cursorAddress % cols; }
|
public int getCursorCol() { return cursorAddress % cols; }
|
||||||
|
|
||||||
@@ -163,31 +171,53 @@ public class ScreenBuffer {
|
|||||||
public byte getReplyMode() { return replyMode; }
|
public byte getReplyMode() { return replyMode; }
|
||||||
public void setReplyMode(byte mode) { this.replyMode = mode; }
|
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 ==========
|
// ========== Screen erase ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform an erase, optionally using the alternate screen size.
|
* Perform an erase, optionally using the alternate screen size.
|
||||||
*/
|
*/
|
||||||
public void erase(boolean alt) {
|
public synchronized void erase(boolean alt) {
|
||||||
clear();
|
clear();
|
||||||
int newRows = alt ? altRows : defRows;
|
int newRows = alt ? altRows : defRows;
|
||||||
int newCols = alt ? altCols : defCols;
|
int newCols = alt ? altCols : defCols;
|
||||||
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
if (alt == screenAlt && rows == newRows && cols == newCols) {
|
||||||
|
updateDisplaySnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rows = newRows;
|
rows = newRows;
|
||||||
cols = newCols;
|
cols = newCols;
|
||||||
screenAlt = alt;
|
screenAlt = alt;
|
||||||
|
updateDisplaySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
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. */
|
/** Clear the entire buffer. */
|
||||||
public void clear() {
|
public synchronized void clear() {
|
||||||
for (ExtendedAttribute ea : buffer) {
|
for (ExtendedAttribute ea : buffer) {
|
||||||
ea.clear();
|
ea.clear();
|
||||||
}
|
}
|
||||||
cursorAddress = 0;
|
cursorAddress = 0;
|
||||||
bufferAddress = 0;
|
bufferAddress = 0;
|
||||||
formatted = false;
|
formatted = false;
|
||||||
|
replyMode = SF_SRM_FIELD;
|
||||||
|
activePartition = 0;
|
||||||
|
explicitPartitionActive = false;
|
||||||
screenChanged = true;
|
screenChanged = true;
|
||||||
|
|
||||||
defaultFg = 0x00;
|
defaultFg = 0x00;
|
||||||
@@ -196,12 +226,13 @@ public class ScreenBuffer {
|
|||||||
defaultCs = 0x00;
|
defaultCs = 0x00;
|
||||||
defaultIc = 0x00;
|
defaultIc = 0x00;
|
||||||
replyMode = SF_SRM_FIELD;
|
replyMode = SF_SRM_FIELD;
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erase all unprotected fields.
|
* Erase all unprotected fields.
|
||||||
*/
|
*/
|
||||||
public void eraseAllUnprotected() {
|
public synchronized void eraseAllUnprotected() {
|
||||||
int size = rows * cols;
|
int size = rows * cols;
|
||||||
boolean inUnprotected = false;
|
boolean inUnprotected = false;
|
||||||
|
|
||||||
@@ -229,6 +260,7 @@ public class ScreenBuffer {
|
|||||||
// Move cursor to first unprotected field
|
// Move cursor to first unprotected field
|
||||||
cursorAddress = findNextUnprotected(0);
|
cursorAddress = findNextUnprotected(0);
|
||||||
screenChanged = true;
|
screenChanged = true;
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Field attribute navigation ==========
|
// ========== Field attribute navigation ==========
|
||||||
@@ -240,13 +272,17 @@ public class ScreenBuffer {
|
|||||||
public int findFieldAttribute(int baddr) {
|
public int findFieldAttribute(int baddr) {
|
||||||
if (!formatted) return -1;
|
if (!formatted) return -1;
|
||||||
int size = rows * cols;
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return -1;
|
||||||
|
baddr = ((baddr % size) + size) % size;
|
||||||
int start = baddr;
|
int start = baddr;
|
||||||
|
int count = 0;
|
||||||
do {
|
do {
|
||||||
if (buffer[baddr].isFieldAttribute()) {
|
if (buffer[baddr].isFieldAttribute()) {
|
||||||
return baddr;
|
return baddr;
|
||||||
}
|
}
|
||||||
baddr = (baddr > 0) ? baddr - 1 : size - 1;
|
baddr = (baddr > 0) ? baddr - 1 : size - 1;
|
||||||
} while (baddr != start);
|
count++;
|
||||||
|
} while (baddr != start && count < size);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,8 +300,12 @@ public class ScreenBuffer {
|
|||||||
* Returns 0 if none found.
|
* Returns 0 if none found.
|
||||||
*/
|
*/
|
||||||
public int findNextUnprotected(int baddr) {
|
public int findNextUnprotected(int baddr) {
|
||||||
|
if (!formatted) return 0;
|
||||||
int size = rows * cols;
|
int size = rows * cols;
|
||||||
|
if (size <= 0) return 0;
|
||||||
|
baddr = ((baddr % size) + size) % size;
|
||||||
int start = baddr;
|
int start = baddr;
|
||||||
|
int count = 0;
|
||||||
do {
|
do {
|
||||||
int next = (baddr + 1) % size;
|
int next = (baddr + 1) % size;
|
||||||
if (buffer[baddr].isFieldAttribute()
|
if (buffer[baddr].isFieldAttribute()
|
||||||
@@ -274,7 +314,8 @@ public class ScreenBuffer {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
baddr = next;
|
baddr = next;
|
||||||
} while (baddr != start);
|
count++;
|
||||||
|
} while (baddr != start && count < size);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,12 +338,17 @@ public class ScreenBuffer {
|
|||||||
|
|
||||||
/** Increment buffer address (wrapping). */
|
/** Increment buffer address (wrapping). */
|
||||||
public int incrementAddress(int addr) {
|
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). */
|
/** Decrement buffer address (wrapping). */
|
||||||
public int decrementAddress(int addr) {
|
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. */
|
/** Convert buffer address to row. */
|
||||||
@@ -322,7 +368,7 @@ public class ScreenBuffer {
|
|||||||
|
|
||||||
// ========== Setters for model reconfiguration ==========
|
// ========== Setters for model reconfiguration ==========
|
||||||
|
|
||||||
public void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
|
public synchronized void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
|
||||||
int altRows, int altCols) {
|
int altRows, int altCols) {
|
||||||
this.maxRows = maxRows;
|
this.maxRows = maxRows;
|
||||||
this.maxCols = maxCols;
|
this.maxCols = maxCols;
|
||||||
@@ -333,6 +379,7 @@ public class ScreenBuffer {
|
|||||||
this.rows = defRows;
|
this.rows = defRows;
|
||||||
this.cols = defCols;
|
this.cols = defCols;
|
||||||
allocateBuffers();
|
allocateBuffers();
|
||||||
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void translateToUnicode() {
|
public void translateToUnicode() {
|
||||||
+6
-6
@@ -1,15 +1,15 @@
|
|||||||
package org.lib3270j.telnet;
|
package haus.nightmare.lib3270j.telnet;
|
||||||
|
|
||||||
import org.lib3270j.ConnectionState;
|
import haus.nightmare.lib3270j.ConnectionState;
|
||||||
import org.lib3270j.ConnectionConfig;
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
import org.lib3270j.Telnet3270Client;
|
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.logging.Level;
|
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.
|
* Manages the raw TCP socket connection to a TN3270 host.
|
||||||
@@ -47,7 +47,7 @@ public class TelnetConnection {
|
|||||||
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
|
log.info("Connecting with TLS to " + config.getHost() + ":" + config.getPort() +
|
||||||
" (verifyCert=" + config.isTlsVerifyCert() + ")");
|
" (verifyCert=" + config.isTlsVerifyCert() + ")");
|
||||||
try {
|
try {
|
||||||
javax.net.ssl.SSLContext sslContext = org.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
javax.net.ssl.SSLContext sslContext = haus.nightmare.lib3270j.tls.TlsTrustManager.createSSLContext(config);
|
||||||
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
javax.net.ssl.SSLSocketFactory ssf = sslContext.getSocketFactory();
|
||||||
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) ssf.createSocket();
|
||||||
sslSocket.setKeepAlive(true);
|
sslSocket.setKeepAlive(true);
|
||||||
+104
-12
@@ -1,20 +1,21 @@
|
|||||||
package org.lib3270j.telnet;
|
package haus.nightmare.lib3270j.telnet;
|
||||||
|
|
||||||
import org.lib3270j.*;
|
import haus.nightmare.lib3270j.*;
|
||||||
import org.lib3270j.datastream.DataStreamProcessor;
|
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||||
import org.lib3270j.protocol.*;
|
import haus.nightmare.lib3270j.protocol.*;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import org.lib3270j.listener.*;
|
import haus.nightmare.lib3270j.listener.*;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import static org.lib3270j.protocol.TelnetConstants.*;
|
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
|
||||||
import static org.lib3270j.protocol.TN3270EConstants.*;
|
import static haus.nightmare.lib3270j.protocol.TN3270EConstants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Telnet finite state machine.
|
* Telnet finite state machine.
|
||||||
@@ -59,6 +60,32 @@ public class TelnetFSM {
|
|||||||
private int responseRequired = RSF_NO_RESPONSE;
|
private int responseRequired = RSF_NO_RESPONSE;
|
||||||
private boolean deferredWillTtype;
|
private boolean deferredWillTtype;
|
||||||
private boolean tn3270eDeviceTypeSent;
|
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
|
// Connection references
|
||||||
private TelnetConnection connection;
|
private TelnetConnection connection;
|
||||||
@@ -109,6 +136,7 @@ public class TelnetFSM {
|
|||||||
tn3270eBound = false;
|
tn3270eBound = false;
|
||||||
eXmitSeq = 0;
|
eXmitSeq = 0;
|
||||||
deferredWillTtype = false;
|
deferredWillTtype = false;
|
||||||
|
ttypeIndex = 0;
|
||||||
ibuf.reset();
|
ibuf.reset();
|
||||||
sbbuf.reset();
|
sbbuf.reset();
|
||||||
|
|
||||||
@@ -116,6 +144,9 @@ public class TelnetFSM {
|
|||||||
eFuncs[FUNC_BIND_IMAGE] = true;
|
eFuncs[FUNC_BIND_IMAGE] = true;
|
||||||
eFuncs[FUNC_RESPONSES] = true;
|
eFuncs[FUNC_RESPONSES] = true;
|
||||||
eFuncs[FUNC_SYSREQ] = 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);
|
changeState(ConnectionState.TELNET_PENDING);
|
||||||
}
|
}
|
||||||
@@ -240,6 +271,11 @@ public class TelnetFSM {
|
|||||||
log.fine("RCVD GA");
|
log.fine("RCVD GA");
|
||||||
state = TNS_DATA;
|
state = TNS_DATA;
|
||||||
break;
|
break;
|
||||||
|
case AYT:
|
||||||
|
log.info("RCVD AYT - sending acknowledgment");
|
||||||
|
sendBytes(new byte[] { (byte) IAC, (byte) NOP });
|
||||||
|
state = TNS_DATA;
|
||||||
|
break;
|
||||||
case NOP:
|
case NOP:
|
||||||
log.fine("RCVD NOP");
|
log.fine("RCVD NOP");
|
||||||
state = TNS_DATA;
|
state = TNS_DATA;
|
||||||
@@ -267,6 +303,11 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TELOPT_TM:
|
||||||
|
// RFC 860: Timing Mark - reply with DO TM
|
||||||
|
sendCommand(DO, opt);
|
||||||
|
break;
|
||||||
|
|
||||||
case TELOPT_TN3270E:
|
case TELOPT_TN3270E:
|
||||||
if (!config.isTn3270eEnabled()) {
|
if (!config.isTn3270eEnabled()) {
|
||||||
sendCommand(DONT, opt);
|
sendCommand(DONT, opt);
|
||||||
@@ -310,6 +351,11 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TELOPT_TM:
|
||||||
|
// RFC 860: Timing Mark - reply with WILL TM
|
||||||
|
sendCommand(WILL, opt);
|
||||||
|
break;
|
||||||
|
|
||||||
case TELOPT_TTYPE:
|
case TELOPT_TTYPE:
|
||||||
if (!myOpts[opt]) {
|
if (!myOpts[opt]) {
|
||||||
myOpts[opt] = true;
|
myOpts[opt] = true;
|
||||||
@@ -426,8 +472,12 @@ public class TelnetFSM {
|
|||||||
|
|
||||||
private void handleTTypeSB(byte[] data) {
|
private void handleTTypeSB(byte[] data) {
|
||||||
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
|
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
|
||||||
// Host asks for terminal type
|
// Host asks for terminal type — cycle per RFC 1091
|
||||||
String termType = config.getEffectiveTerminalType();
|
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);
|
log.info("RCVD SB TTYPE SEND - Responding with: " + termType);
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
@@ -538,7 +588,7 @@ public class TelnetFSM {
|
|||||||
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
|
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
|
||||||
// Try fallback model 2 if we were requesting something else
|
// Try fallback model 2 if we were requesting something else
|
||||||
if (config.getModel() != TerminalModel.IBM_3278_2 &&
|
if (config.getModel() != TerminalModel.IBM_3278_2 &&
|
||||||
config.getModel() != TerminalModel.IBM_3279_2) {
|
config.getModel() != TerminalModel.IBM_3279_4) {
|
||||||
log.warning("TN3270E device-type rejected (" +
|
log.warning("TN3270E device-type rejected (" +
|
||||||
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
|
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
|
||||||
config.setModel(TerminalModel.IBM_3278_2);
|
config.setModel(TerminalModel.IBM_3278_2);
|
||||||
@@ -880,6 +930,23 @@ public class TelnetFSM {
|
|||||||
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
tn3270eSubmode = TN3270ESubmode.E_NVT;
|
||||||
break;
|
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:
|
case DT_RESPONSE:
|
||||||
log.fine("Received response, seq=" + seqNumber);
|
log.fine("Received response, seq=" + seqNumber);
|
||||||
break;
|
break;
|
||||||
@@ -903,7 +970,7 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendTN3270EPositiveResponse(int seqNumber) {
|
public void sendTN3270EPositiveResponse(int seqNumber) {
|
||||||
byte[] resp = new byte[EH_SIZE + 1];
|
byte[] resp = new byte[EH_SIZE + 1];
|
||||||
resp[0] = (byte) DT_RESPONSE;
|
resp[0] = (byte) DT_RESPONSE;
|
||||||
resp[1] = 0;
|
resp[1] = 0;
|
||||||
@@ -915,6 +982,31 @@ public class TelnetFSM {
|
|||||||
sendRecord(resp);
|
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 ==========
|
// ========== Check if we should transition to 3270 mode ==========
|
||||||
|
|
||||||
private void checkIn3270() {
|
private void checkIn3270() {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.tls;
|
package haus.nightmare.lib3270j.tls;
|
||||||
|
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package org.lib3270j.tls;
|
package haus.nightmare.lib3270j.tls;
|
||||||
|
|
||||||
import org.lib3270j.ConnectionConfig;
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
|
|
||||||
import javax.net.ssl.*;
|
import javax.net.ssl.*;
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
@@ -1,827 +0,0 @@
|
|||||||
package org.lib3270j.graphics;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interprets GOCA (Graphics Object Content Architecture) drawing orders
|
|
||||||
* and updates the GraphicsPlane.
|
|
||||||
*/
|
|
||||||
public class GocaDecoder {
|
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(GocaDecoder.class.getName());
|
|
||||||
|
|
||||||
private final GraphicsPlane plane;
|
|
||||||
|
|
||||||
// Drawing state
|
|
||||||
private int curX = 0;
|
|
||||||
private int curY = 0;
|
|
||||||
private int curColor = GocaConstants.GOCA_COLORS[0];
|
|
||||||
private int lineType = GocaConstants.LT_SOLID;
|
|
||||||
private int lineWidth = GocaConstants.LW_NORMAL;
|
|
||||||
private int markerType = GocaConstants.MK_PLUS;
|
|
||||||
private int markerSize = 5;
|
|
||||||
private int markerColor = GocaConstants.GOCA_COLORS[0];
|
|
||||||
private int pattern = GocaConstants.PT_SOLID;
|
|
||||||
private int fillColor = GocaConstants.GOCA_COLORS[0];
|
|
||||||
private int charDir = GocaConstants.CD_LR;
|
|
||||||
private double charAngle = 0.0;
|
|
||||||
private int charWidth = 9;
|
|
||||||
private int charHeight = 16;
|
|
||||||
private int charSet = 0;
|
|
||||||
private int arcParamP = 1;
|
|
||||||
private int arcParamQ = 1;
|
|
||||||
private int arcParamR = 0;
|
|
||||||
private int arcParamS = 0;
|
|
||||||
|
|
||||||
private ProgramSymbolManager programSymbolManager;
|
|
||||||
|
|
||||||
// Area accumulation
|
|
||||||
private boolean inArea = false;
|
|
||||||
private boolean areaDrawBoundary = true;
|
|
||||||
private final List<Integer> areaPointsX = new ArrayList<>();
|
|
||||||
private final List<Integer> areaPointsY = new ArrayList<>();
|
|
||||||
|
|
||||||
// Image accumulation
|
|
||||||
private boolean inImage = false;
|
|
||||||
private int imgX = 0;
|
|
||||||
private int imgY = 0;
|
|
||||||
private int imgWidth = 0;
|
|
||||||
private int imgHeight = 0;
|
|
||||||
private final List<Byte> imgBuffer = new ArrayList<>();
|
|
||||||
|
|
||||||
public GocaDecoder(GraphicsPlane plane) {
|
|
||||||
this.plane = plane;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
|
||||||
this.programSymbolManager = psm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GraphicsPlane getGraphicsPlane() {
|
|
||||||
return plane;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCurX() {
|
|
||||||
return curX;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCurY() {
|
|
||||||
return curY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void resetDefaults() {
|
|
||||||
curX = 0;
|
|
||||||
curY = 0;
|
|
||||||
resetAttributes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void resetAttributes() {
|
|
||||||
curColor = getColor(0);
|
|
||||||
lineType = GocaConstants.LT_SOLID;
|
|
||||||
lineWidth = GocaConstants.LW_NORMAL;
|
|
||||||
markerType = GocaConstants.MK_PLUS;
|
|
||||||
markerSize = 5;
|
|
||||||
markerColor = curColor;
|
|
||||||
pattern = GocaConstants.PT_SOLID;
|
|
||||||
fillColor = curColor;
|
|
||||||
charDir = GocaConstants.CD_LR;
|
|
||||||
charAngle = 0.0;
|
|
||||||
charSet = 0;
|
|
||||||
inArea = false;
|
|
||||||
areaPointsX.clear();
|
|
||||||
areaPointsY.clear();
|
|
||||||
inImage = false;
|
|
||||||
imgBuffer.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] partialOrderBuffer = new byte[0];
|
|
||||||
|
|
||||||
private int getOrderLength(byte[] data, int idx, int end) {
|
|
||||||
int order = data[idx] & 0xFF;
|
|
||||||
if (order == GocaConstants.G_NOP1 || order == 0xFF) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (order == GocaConstants.G_GEAR ||
|
|
||||||
order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_ENDPROLOGUE ||
|
|
||||||
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) {
|
|
||||||
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
|
||||||
}
|
|
||||||
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS ||
|
|
||||||
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
|
|
||||||
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW ||
|
|
||||||
order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW ||
|
|
||||||
order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT ||
|
|
||||||
order == GocaConstants.G_GSMT || order == GocaConstants.G_GSCS ||
|
|
||||||
order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCD ||
|
|
||||||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMS_SET ||
|
|
||||||
order == GocaConstants.G_GBAR) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
if (order == GocaConstants.G_GSAP || order == GocaConstants.G_GBIMG || order == 0x91) {
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
if (idx + 1 >= end) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return (data[idx + 1] & 0xFF) + 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes a stream of GOCA drawing orders.
|
|
||||||
*/
|
|
||||||
public synchronized void decodeStream(byte[] data, int offset, int length) {
|
|
||||||
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] inputData;
|
|
||||||
int idx;
|
|
||||||
int end;
|
|
||||||
|
|
||||||
if (partialOrderBuffer.length > 0) {
|
|
||||||
inputData = new byte[partialOrderBuffer.length + length];
|
|
||||||
System.arraycopy(partialOrderBuffer, 0, inputData, 0, partialOrderBuffer.length);
|
|
||||||
System.arraycopy(data, offset, inputData, partialOrderBuffer.length, length);
|
|
||||||
idx = 0;
|
|
||||||
end = inputData.length;
|
|
||||||
partialOrderBuffer = new byte[0];
|
|
||||||
} else {
|
|
||||||
inputData = data;
|
|
||||||
idx = offset;
|
|
||||||
end = offset + length;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (idx < end) {
|
|
||||||
int order = inputData[idx] & 0xFF;
|
|
||||||
int orderLen = getOrderLength(inputData, idx, end);
|
|
||||||
|
|
||||||
if (orderLen == -1 || idx + orderLen > end) {
|
|
||||||
int remaining = end - idx;
|
|
||||||
partialOrderBuffer = new byte[remaining];
|
|
||||||
System.arraycopy(inputData, idx, partialOrderBuffer, 0, remaining);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int payloadLen = (orderLen >= 2) ? (inputData[idx + 1] & 0xFF) : 0;
|
|
||||||
|
|
||||||
switch (order) {
|
|
||||||
case GocaConstants.G_NOP1:
|
|
||||||
case 0xFF: {
|
|
||||||
idx++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GEAR: {
|
|
||||||
endArea();
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GEIMG: {
|
|
||||||
endImage();
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
|
|
||||||
if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) {
|
|
||||||
resetAttributes();
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_ENDPROLOGUE: { // End Prologue (0x3E)
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_COMT: { // Comment (0x01)
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSCP: { // Set Current Position (0x21)
|
|
||||||
if (payloadLen >= 4 && idx + 5 < end) {
|
|
||||||
curX = readCoord(inputData, idx + 2);
|
|
||||||
curY = readCoord(inputData, idx + 4);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSAP: { // Arc Parameters (0x22)
|
|
||||||
if (idx + 9 < end) {
|
|
||||||
arcParamP = readCoord(inputData, idx + 2);
|
|
||||||
arcParamQ = readCoord(inputData, idx + 4);
|
|
||||||
arcParamR = readCoord(inputData, idx + 6);
|
|
||||||
arcParamS = readCoord(inputData, idx + 8);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSVW: { // Set Viewing Window (0x27)
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSCA: { // Set Character Angle (0x34)
|
|
||||||
if (payloadLen >= 4 && idx + 5 < end) {
|
|
||||||
int ax = readCoord(inputData, idx + 2);
|
|
||||||
int ay = readCoord(inputData, idx + 4);
|
|
||||||
if (ax != 0 || ay != 0) {
|
|
||||||
charAngle = Math.toDegrees(Math.atan2(ay, ax));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSCH: { // Set Character Cell (0x33)
|
|
||||||
if (payloadLen >= 4 && idx + 5 < end) {
|
|
||||||
charWidth = readCoord(inputData, idx + 2);
|
|
||||||
charHeight = readCoord(inputData, idx + 4);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSCR: { // Set Character Shear (0x35)
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSCOL: { // Set Color (0x0A)
|
|
||||||
int colIdx = inputData[idx + 1] & 0xFF;
|
|
||||||
curColor = getColor(colIdx);
|
|
||||||
fillColor = curColor;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSECOL: { // Set Extended Color (0x26)
|
|
||||||
if (payloadLen >= 2 && idx + 3 < end) {
|
|
||||||
int colIdx = inputData[idx + 3] & 0xFF;
|
|
||||||
curColor = getColor(colIdx);
|
|
||||||
fillColor = curColor;
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
|
||||||
lineType = inputData[idx + 1] & 0xFF;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSLW: { // Set Line Width (0x19)
|
|
||||||
lineWidth = inputData[idx + 1] & 0xFF;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSMC: { // Set Marker Color (0x07)
|
|
||||||
int colIdx = inputData[idx + 1] & 0xFF;
|
|
||||||
markerColor = getColor(colIdx);
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSMS: { // Set Marker Size (0x1B)
|
|
||||||
markerSize = inputData[idx + 1] & 0xFF;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSMT: { // Set Marker Type (0x29)
|
|
||||||
markerType = inputData[idx + 1] & 0xFF;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSPS: { // Set Pattern Set (0x08)
|
|
||||||
pattern = inputData[idx + 1] & 0xFF;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28)
|
|
||||||
pattern = inputData[idx + 1] & 0xFF;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSCD: { // Set Character Direction (0x3A)
|
|
||||||
charDir = inputData[idx + 1] & 0xFF;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GSCS: { // Set Character Set (0x38)
|
|
||||||
charSet = inputData[idx + 1] & 0xFF;
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 0x04:
|
|
||||||
case GocaConstants.G_GSMX:
|
|
||||||
case GocaConstants.G_GSBMX:
|
|
||||||
case GocaConstants.G_GSFLW:
|
|
||||||
case GocaConstants.G_GSMP:
|
|
||||||
case GocaConstants.G_GSCC:
|
|
||||||
case GocaConstants.G_GSMS_SET:
|
|
||||||
case GocaConstants.G_GPOP: {
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GBAR: { // Begin Area (0x68)
|
|
||||||
int flags = inputData[idx + 1] & 0xFF;
|
|
||||||
beginArea((flags & 0x40) != 0);
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GLINE: { // Line Absolute (0xC1)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processLine(inputData, idx + 2, payloadLen, false);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GCLINE: { // Line Current Position (0x81)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processLine(inputData, idx + 2, payloadLen, true);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GRLINE: { // Relative Line Absolute Start (0xE1)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processRelativeLine(inputData, idx + 2, payloadLen, false);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GCRLIN: { // Relative Line Current Position (0xA1)
|
|
||||||
if (payloadLen >= 2) {
|
|
||||||
processRelativeLine(inputData, idx + 2, payloadLen, true);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GFARC: { // Full Arc Absolute (0xC7)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processArc(inputData, idx + 2, payloadLen, false, true);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GCFARC: { // Full Arc Current Position (0x87)
|
|
||||||
if (payloadLen >= 2) {
|
|
||||||
processArc(inputData, idx + 2, payloadLen, true, true);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GARC: { // Partial Arc Absolute (0xC6)
|
|
||||||
if (payloadLen >= 8) {
|
|
||||||
processArc(inputData, idx + 2, payloadLen, false, false);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GCARC: { // Partial Arc Current Position (0x86)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processArc(inputData, idx + 2, payloadLen, true, false);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GFLT: { // Fillet Absolute (0xC5)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processFillet(inputData, idx + 2, payloadLen, false);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GCFLT: { // Fillet Current Position (0x85)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processFillet(inputData, idx + 2, payloadLen, true);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GMRK: { // Marker Absolute (0xC2)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processMarker(inputData, idx + 2, payloadLen, false);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GCMRK: { // Marker Current Position (0x82)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processMarker(inputData, idx + 2, payloadLen, true);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GCHST: { // Character String Absolute (0xC3)
|
|
||||||
if (payloadLen >= 4) {
|
|
||||||
processText(inputData, idx + 2, payloadLen, false);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GCCHST: { // Character String Current Position (0x83)
|
|
||||||
if (payloadLen >= 0) {
|
|
||||||
processText(inputData, idx + 2, payloadLen, true);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
|
|
||||||
if (idx + 9 < end) {
|
|
||||||
int x = readCoord(inputData, idx + 2);
|
|
||||||
int y = readCoord(inputData, idx + 4);
|
|
||||||
int w = readCoord(inputData, idx + 6);
|
|
||||||
int h = readCoord(inputData, idx + 8);
|
|
||||||
beginImage(x, y, w, h);
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.G_GIMD: { // Image Data (0x92)
|
|
||||||
if (inImage) {
|
|
||||||
for (int k = 0; k < payloadLen; k++) {
|
|
||||||
imgBuffer.add(inputData[idx + 2 + k]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
idx += orderLen;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes a stream of Object Control Procedure Orders (subtype 0x11).
|
|
||||||
*/
|
|
||||||
public synchronized void processProcedureOrders(byte[] data, int offset, int length) {
|
|
||||||
if (data == null || length <= 0 || offset < 0 || offset + length > data.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int idx = offset;
|
|
||||||
int end = offset + length;
|
|
||||||
|
|
||||||
while (idx < end) {
|
|
||||||
int order = data[idx] & 0xFF;
|
|
||||||
|
|
||||||
switch (order) {
|
|
||||||
case GocaConstants.P_NOP1:
|
|
||||||
case GocaConstants.P_ATTCUR:
|
|
||||||
case GocaConstants.P_DETCUR:
|
|
||||||
case GocaConstants.P_STOPDR: {
|
|
||||||
idx++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
|
|
||||||
plane.clear();
|
|
||||||
resetDefaults();
|
|
||||||
idx++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12)
|
|
||||||
int len = 12;
|
|
||||||
if (idx + 1 < end && data[idx + 1] != 0) {
|
|
||||||
len = (data[idx + 1] & 0xFF) + 2;
|
|
||||||
}
|
|
||||||
idx += len;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults
|
|
||||||
if (idx + 1 < end) {
|
|
||||||
int len = data[idx + 1] & 0xFF;
|
|
||||||
if (idx + 2 + len <= end) {
|
|
||||||
decodeStream(data, idx + 2, len);
|
|
||||||
}
|
|
||||||
idx += 2 + len;
|
|
||||||
} else {
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GocaConstants.P_COMT:
|
|
||||||
case GocaConstants.P_SETCUR: {
|
|
||||||
if (idx + 1 < end) {
|
|
||||||
int len = data[idx + 1] & 0xFF;
|
|
||||||
idx += 2 + len;
|
|
||||||
} else {
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
if (idx + 1 < end) {
|
|
||||||
int len = data[idx + 1] & 0xFF;
|
|
||||||
idx += 2 + len;
|
|
||||||
} else {
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void beginArea(boolean drawBoundary) {
|
|
||||||
this.inArea = true;
|
|
||||||
this.areaDrawBoundary = drawBoundary;
|
|
||||||
this.fillColor = this.curColor;
|
|
||||||
this.areaPointsX.clear();
|
|
||||||
this.areaPointsY.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void endArea() {
|
|
||||||
if (!inArea || areaPointsX.size() < 3) {
|
|
||||||
inArea = false;
|
|
||||||
areaPointsX.clear();
|
|
||||||
areaPointsY.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int n = areaPointsX.size();
|
|
||||||
int[] px = new int[n];
|
|
||||||
int[] py = new int[n];
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
px[i] = plane.mapX(areaPointsX.get(i));
|
|
||||||
py[i] = plane.mapY(areaPointsY.get(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
plane.fillArea(px, py, n, fillColor, pattern, areaDrawBoundary, curColor, lineType, lineWidth);
|
|
||||||
inArea = false;
|
|
||||||
areaPointsX.clear();
|
|
||||||
areaPointsY.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAreaPoint(int x, int y) {
|
|
||||||
if (inArea) {
|
|
||||||
areaPointsX.add(x);
|
|
||||||
areaPointsY.add(y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void beginImage(int x, int y, int w, int h) {
|
|
||||||
this.inImage = true;
|
|
||||||
this.imgX = x;
|
|
||||||
this.imgY = y;
|
|
||||||
this.imgWidth = w;
|
|
||||||
this.imgHeight = h;
|
|
||||||
this.imgBuffer.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void endImage() {
|
|
||||||
if (!inImage || imgWidth <= 0 || imgHeight <= 0 || imgBuffer.isEmpty()) {
|
|
||||||
inImage = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byte[] bytes = new byte[imgBuffer.size()];
|
|
||||||
for (int i = 0; i < bytes.length; i++) {
|
|
||||||
bytes[i] = imgBuffer.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
int px = plane.mapX(imgX);
|
|
||||||
int py = plane.mapY(imgY);
|
|
||||||
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor);
|
|
||||||
inImage = false;
|
|
||||||
imgBuffer.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processLine(byte[] data, int off, int len, boolean fromCurPos) {
|
|
||||||
int pos = off;
|
|
||||||
int end = off + len;
|
|
||||||
|
|
||||||
int startX = curX;
|
|
||||||
int startY = curY;
|
|
||||||
|
|
||||||
if (!fromCurPos && pos + 4 <= end) {
|
|
||||||
startX = readCoord(data, pos);
|
|
||||||
startY = readCoord(data, pos + 2);
|
|
||||||
pos += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inArea) {
|
|
||||||
addAreaPoint(startX, startY);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (pos + 4 <= end) {
|
|
||||||
int nextX = readCoord(data, pos);
|
|
||||||
int nextY = readCoord(data, pos + 2);
|
|
||||||
pos += 4;
|
|
||||||
|
|
||||||
if (inArea) {
|
|
||||||
addAreaPoint(nextX, nextY);
|
|
||||||
} else {
|
|
||||||
plane.drawLine(plane.mapX(startX), plane.mapY(startY),
|
|
||||||
plane.mapX(nextX), plane.mapY(nextY),
|
|
||||||
curColor, lineType, lineWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
startX = nextX;
|
|
||||||
startY = nextY;
|
|
||||||
}
|
|
||||||
|
|
||||||
curX = startX;
|
|
||||||
curY = startY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processRelativeLine(byte[] data, int off, int len, boolean fromCurPos) {
|
|
||||||
int pos = off;
|
|
||||||
int end = off + len;
|
|
||||||
|
|
||||||
int startX = curX;
|
|
||||||
int startY = curY;
|
|
||||||
|
|
||||||
if (!fromCurPos && pos + 4 <= end) {
|
|
||||||
startX = readCoord(data, pos);
|
|
||||||
startY = readCoord(data, pos + 2);
|
|
||||||
pos += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inArea) {
|
|
||||||
addAreaPoint(startX, startY);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (pos + 2 <= end) {
|
|
||||||
int dx = (byte) data[pos];
|
|
||||||
int dy = (byte) data[pos + 1];
|
|
||||||
pos += 2;
|
|
||||||
|
|
||||||
int nextX = startX + dx;
|
|
||||||
int nextY = startY + dy;
|
|
||||||
|
|
||||||
if (inArea) {
|
|
||||||
addAreaPoint(nextX, nextY);
|
|
||||||
} else {
|
|
||||||
plane.drawLine(plane.mapX(startX), plane.mapY(startY),
|
|
||||||
plane.mapX(nextX), plane.mapY(nextY),
|
|
||||||
curColor, lineType, lineWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
startX = nextX;
|
|
||||||
startY = nextY;
|
|
||||||
}
|
|
||||||
|
|
||||||
curX = startX;
|
|
||||||
curY = startY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
|
|
||||||
int pos = off;
|
|
||||||
int startX = curX;
|
|
||||||
int startY = curY;
|
|
||||||
|
|
||||||
if (!fromCurPos && pos + 4 <= off + len) {
|
|
||||||
startX = readCoord(data, pos);
|
|
||||||
startY = readCoord(data, pos + 2);
|
|
||||||
pos += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
double multiplier = 1.0;
|
|
||||||
if (pos + 2 <= off + len) {
|
|
||||||
multiplier = (data[pos] & 0xFF) + ((data[pos + 1] & 0xFF) / 255.0);
|
|
||||||
if (multiplier == 0.0) multiplier = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int dxP = Math.abs(arcParamP - arcParamR);
|
|
||||||
int dyQ = Math.abs(arcParamQ - arcParamS);
|
|
||||||
if (dxP == 0) dxP = 10;
|
|
||||||
if (dyQ == 0) dyQ = 10;
|
|
||||||
|
|
||||||
int rxVirtual = (int) (dxP * multiplier);
|
|
||||||
int ryVirtual = (int) (dyQ * multiplier);
|
|
||||||
|
|
||||||
int rx = Math.abs(plane.mapX(rxVirtual) - plane.mapX(0));
|
|
||||||
int ry = Math.abs(plane.mapY(ryVirtual) - plane.mapY(0));
|
|
||||||
if (rx <= 0) rx = 10;
|
|
||||||
if (ry <= 0) ry = 10;
|
|
||||||
|
|
||||||
plane.drawArc(plane.mapX(startX), plane.mapY(startY), rx, ry, 0.0, 360.0,
|
|
||||||
curColor, lineType, lineWidth, isFull);
|
|
||||||
|
|
||||||
curX = startX;
|
|
||||||
curY = startY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processFillet(byte[] data, int off, int len, boolean fromCurPos) {
|
|
||||||
int pos = off;
|
|
||||||
int end = off + len;
|
|
||||||
|
|
||||||
List<Integer> ptsX = new ArrayList<>();
|
|
||||||
List<Integer> ptsY = new ArrayList<>();
|
|
||||||
|
|
||||||
if (fromCurPos) {
|
|
||||||
ptsX.add(plane.mapX(curX));
|
|
||||||
ptsY.add(plane.mapY(curY));
|
|
||||||
}
|
|
||||||
|
|
||||||
while (pos + 4 <= end) {
|
|
||||||
int x = readCoord(data, pos);
|
|
||||||
int y = readCoord(data, pos + 2);
|
|
||||||
ptsX.add(plane.mapX(x));
|
|
||||||
ptsY.add(plane.mapY(y));
|
|
||||||
curX = x;
|
|
||||||
curY = y;
|
|
||||||
pos += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ptsX.size() >= 2) {
|
|
||||||
int n = ptsX.size();
|
|
||||||
int[] px = new int[n];
|
|
||||||
int[] py = new int[n];
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
px[i] = ptsX.get(i);
|
|
||||||
py[i] = ptsY.get(i);
|
|
||||||
}
|
|
||||||
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
|
|
||||||
int pos = off;
|
|
||||||
int end = off + len;
|
|
||||||
|
|
||||||
if (fromCurPos) {
|
|
||||||
plane.drawMarker(plane.mapX(curX), plane.mapY(curY), markerType, markerSize, markerColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (pos + 4 <= end) {
|
|
||||||
int x = readCoord(data, pos);
|
|
||||||
int y = readCoord(data, pos + 2);
|
|
||||||
plane.drawMarker(plane.mapX(x), plane.mapY(y), markerType, markerSize, markerColor);
|
|
||||||
curX = x;
|
|
||||||
curY = y;
|
|
||||||
pos += 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processText(byte[] data, int off, int len, boolean fromCurPos) {
|
|
||||||
int pos = off;
|
|
||||||
int end = off + len;
|
|
||||||
|
|
||||||
int startX = curX;
|
|
||||||
int startY = curY;
|
|
||||||
|
|
||||||
if (!fromCurPos && pos + 4 <= end) {
|
|
||||||
startX = readCoord(data, pos);
|
|
||||||
startY = readCoord(data, pos + 2);
|
|
||||||
pos += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
int textLen = end - pos;
|
|
||||||
if (textLen <= 0) return;
|
|
||||||
|
|
||||||
int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 12;
|
|
||||||
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 20;
|
|
||||||
|
|
||||||
if (charSet != 0 && programSymbolManager != null) {
|
|
||||||
for (int i = 0; i < textLen; i++) {
|
|
||||||
int code = data[pos + i] & 0xFF;
|
|
||||||
int px = plane.mapX(startX);
|
|
||||||
int py = plane.mapY(startY) - ch;
|
|
||||||
ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code);
|
|
||||||
if (slot != null) {
|
|
||||||
int[] rgb = slot.getRgbPixels(curColor, 0);
|
|
||||||
int symW = slot.getWidth();
|
|
||||||
int symH = slot.getHeight();
|
|
||||||
for (int dy = 0; dy < ch; dy++) {
|
|
||||||
int sy = (dy * symH) / ch;
|
|
||||||
for (int dx = 0; dx < cw; dx++) {
|
|
||||||
int sx = (dx * symW) / cw;
|
|
||||||
int pixelArgb = rgb[sy * symW + sx];
|
|
||||||
if ((pixelArgb >>> 24) != 0) {
|
|
||||||
plane.setPixel(px + dx, py + dy, pixelArgb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
|
||||||
plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle);
|
|
||||||
}
|
|
||||||
startX += (charWidth > 0 ? charWidth : 9);
|
|
||||||
}
|
|
||||||
curX = startX;
|
|
||||||
curY = startY;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char[] chars = new char[textLen];
|
|
||||||
for (int i = 0; i < textLen; i++) {
|
|
||||||
chars[i] = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
|
|
||||||
}
|
|
||||||
String text = new String(chars);
|
|
||||||
|
|
||||||
plane.drawVectorText(plane.mapX(startX), plane.mapY(startY) - ch, text,
|
|
||||||
curColor, cw, ch, charDir, charAngle);
|
|
||||||
|
|
||||||
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
|
|
||||||
curY = startY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int readCoord(byte[] data, int off) {
|
|
||||||
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getColor(int colorIndex) {
|
|
||||||
return GocaConstants.getGocaColorArgb(colorIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,524 +0,0 @@
|
|||||||
package org.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 int screenCols = 80;
|
|
||||||
private int screenRows = 24;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean hasContent() {
|
|
||||||
return hasContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized int[] getRgbBuffer() {
|
|
||||||
return rgbBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCanvasWidth() {
|
|
||||||
return canvasWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCanvasHeight() {
|
|
||||||
return canvasHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
|
|
||||||
*/
|
|
||||||
public int mapX(int gocaX) {
|
|
||||||
int nominalWidth = screenCols * 9;
|
|
||||||
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
|
||||||
int nx = gocaX + xMax;
|
|
||||||
return (int) Math.round((double) nx * canvasWidth / nominalWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
|
|
||||||
*/
|
|
||||||
public int mapY(int gocaY) {
|
|
||||||
int nominalHeight = screenRows * 16;
|
|
||||||
int yMax = (nominalHeight - 1) / 2;
|
|
||||||
int ny = yMax - gocaY;
|
|
||||||
return (int) Math.round((double) ny * canvasHeight / nominalHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely plots a pixel at (x, y).
|
|
||||||
*/
|
|
||||||
public synchronized void setPixel(int x, int y, int colorArgb) {
|
|
||||||
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
|
||||||
rgbBuffer[y * canvasWidth + x] = colorArgb;
|
|
||||||
hasContent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws an absolute or relative line using Bresenham's algorithm with line styles and widths.
|
|
||||||
*/
|
|
||||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
|
|
||||||
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
|
||||||
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
|
|
||||||
|
|
||||||
int dx = Math.abs(x2 - x1);
|
|
||||||
int dy = Math.abs(y2 - y1);
|
|
||||||
int sx = x1 < x2 ? 1 : -1;
|
|
||||||
int sy = y1 < y2 ? 1 : -1;
|
|
||||||
int err = dx - dy;
|
|
||||||
|
|
||||||
int curX = x1;
|
|
||||||
int curY = y1;
|
|
||||||
int stepIndex = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (shouldPlotLinePixel(stepIndex, lineType)) {
|
|
||||||
drawPixelWithThickness(curX, curY, color, thickness);
|
|
||||||
}
|
|
||||||
stepIndex++;
|
|
||||||
|
|
||||||
if (curX == x2 && curY == y2) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int e2 = 2 * err;
|
|
||||||
if (e2 > -dy) {
|
|
||||||
err -= dy;
|
|
||||||
curX += sx;
|
|
||||||
}
|
|
||||||
if (e2 < dx) {
|
|
||||||
err += dx;
|
|
||||||
curY += sy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hasContent = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldPlotLinePixel(int step, int lineType) {
|
|
||||||
switch (lineType) {
|
|
||||||
case GocaConstants.LT_DOT:
|
|
||||||
return (step % 4) < 2;
|
|
||||||
case GocaConstants.LT_SHORTDASH:
|
|
||||||
return (step % 6) < 4;
|
|
||||||
case GocaConstants.LT_DASHDOT:
|
|
||||||
int m12 = step % 12;
|
|
||||||
return m12 < 6 || (m12 >= 8 && m12 < 10);
|
|
||||||
case GocaConstants.LT_DOUBLEDOT:
|
|
||||||
int m10 = step % 10;
|
|
||||||
return m10 < 2 || (m10 >= 4 && m10 < 6);
|
|
||||||
case GocaConstants.LT_LONGDASH:
|
|
||||||
return (step % 11) < 8;
|
|
||||||
case GocaConstants.LT_DASHDOUBLEDOT:
|
|
||||||
int m18 = step % 18;
|
|
||||||
return m18 < 8 || (m18 >= 10 && m18 < 12) || (m18 >= 14 && m18 < 16);
|
|
||||||
case GocaConstants.LT_SOLID:
|
|
||||||
case GocaConstants.LT_DEFAULT:
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void drawPixelWithThickness(int x, int y, int color, int thickness) {
|
|
||||||
if (thickness <= 1) {
|
|
||||||
setPixel(x, y, color);
|
|
||||||
} else {
|
|
||||||
for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) {
|
|
||||||
for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) {
|
|
||||||
setPixel(x + dx, y + dy, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a full or partial arc / ellipse.
|
|
||||||
*/
|
|
||||||
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
|
|
||||||
int colorArgb, int lineType, int lineWidth, boolean isFull) {
|
|
||||||
if (rx <= 0) rx = 1;
|
|
||||||
if (ry <= 0) ry = 1;
|
|
||||||
|
|
||||||
int numSteps = Math.max(24, Math.max(rx, ry) * 4);
|
|
||||||
double startRad = Math.toRadians(startAngleDeg);
|
|
||||||
double sweepRad = isFull ? (2.0 * Math.PI) : Math.toRadians(sweepAngleDeg);
|
|
||||||
double stepRad = sweepRad / numSteps;
|
|
||||||
|
|
||||||
int prevX = (int) Math.round(cx + rx * Math.cos(startRad));
|
|
||||||
int prevY = (int) Math.round(cy - ry * Math.sin(startRad));
|
|
||||||
|
|
||||||
for (int i = 1; i <= numSteps; i++) {
|
|
||||||
double angle = startRad + i * stepRad;
|
|
||||||
int nextX = (int) Math.round(cx + rx * Math.cos(angle));
|
|
||||||
int nextY = (int) Math.round(cy - ry * Math.sin(angle));
|
|
||||||
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
|
|
||||||
prevX = nextX;
|
|
||||||
prevY = nextY;
|
|
||||||
}
|
|
||||||
hasContent = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a Fillet (spline / curve approximation across control points).
|
|
||||||
*/
|
|
||||||
public synchronized void drawFillet(int[] px, int[] 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
int prevX = px[0];
|
|
||||||
int prevY = py[0];
|
|
||||||
|
|
||||||
for (int i = 0; i < numPoints - 1; i++) {
|
|
||||||
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
|
|
||||||
double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0;
|
|
||||||
double p1x = px[i];
|
|
||||||
double p1y = py[i];
|
|
||||||
double p2x = (i == numPoints - 2) ? px[numPoints - 1] : (px[i] + px[i + 1]) / 2.0;
|
|
||||||
double p2y = (i == numPoints - 2) ? py[numPoints - 1] : (py[i] + py[i + 1]) / 2.0;
|
|
||||||
|
|
||||||
int steps = 20;
|
|
||||||
for (int s = 1; s <= steps; s++) {
|
|
||||||
double t = (double) s / steps;
|
|
||||||
double oneMinusT = 1.0 - t;
|
|
||||||
double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x;
|
|
||||||
double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y;
|
|
||||||
int nextX = (int) Math.round(bx);
|
|
||||||
int nextY = (int) Math.round(by);
|
|
||||||
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
|
|
||||||
prevX = nextX;
|
|
||||||
prevY = nextY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hasContent = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
if (px == null || py == null || numPoints < 3) return;
|
|
||||||
|
|
||||||
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
|
|
||||||
|
|
||||||
if (pattern != GocaConstants.PT_EMPTY) {
|
|
||||||
// Find polygon vertical bounds
|
|
||||||
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 = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0];
|
|
||||||
|
|
||||||
for (int y = minY; y <= maxY; y++) {
|
|
||||||
nodeX.clear();
|
|
||||||
int j = numPoints - 1;
|
|
||||||
for (int i = 0; i < numPoints; i++) {
|
|
||||||
if ((py[i] < y && py[j] >= y) || (py[j] < y && py[i] >= y)) {
|
|
||||||
int x = px[i] + (int) Math.round((double) (y - py[i]) / (py[j] - py[i]) * (px[j] - px[i]));
|
|
||||||
nodeX.add(x);
|
|
||||||
}
|
|
||||||
j = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) {
|
|
||||||
setPixel(x, y, fill);
|
|
||||||
} else {
|
|
||||||
int b = patRows[y & 7] & 0xFF;
|
|
||||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
|
||||||
setPixel(x, y, fill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawBoundary && boundaryColorArgb != 0) {
|
|
||||||
for (int i = 0; i < numPoints; i++) {
|
|
||||||
int next = (i + 1) % numPoints;
|
|
||||||
drawLine(px[i], py[i], px[next], py[next], boundaryColorArgb, lineType, lineWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hasContent = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle).
|
|
||||||
*/
|
|
||||||
public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) {
|
|
||||||
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
|
||||||
int s = Math.max(3, size > 0 ? size : 5);
|
|
||||||
|
|
||||||
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, y - s, x + s / 2, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
|
||||||
drawLine(x - s / 2, y + s, x + s / 2, 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[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4,
|
|
||||||
color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
|
||||||
break;
|
|
||||||
case GocaConstants.MK_SSQUARE: // solid square
|
|
||||||
fillArea(new int[]{x - s, x + s, x + s, x - s}, new int[]{y - s, y - s, y + s, y + s}, 4,
|
|
||||||
color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
|
||||||
break;
|
|
||||||
case GocaConstants.MK_DOT: // dot
|
|
||||||
for (int dy = -2; dy <= 2; dy++) {
|
|
||||||
for (int dx = -2; dx <= 2; dx++) {
|
|
||||||
if (dx * dx + dy * dy <= 4) {
|
|
||||||
setPixel(x + dx, y + dy, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int[] VSS_OFFSETS = new int[256];
|
|
||||||
static {
|
|
||||||
Arrays.fill(VSS_OFFSETS, -1);
|
|
||||||
int sym = VectorSymbolData.VSS_SYMBOL_START; // 33
|
|
||||||
if (sym < 256) {
|
|
||||||
VSS_OFFSETS[sym] = 0;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < VectorSymbolData.vss_data.length; i++) {
|
|
||||||
if (VectorSymbolData.vss_data[i] == VectorSymbolData.END_DEFAULT) { // 0xFF
|
|
||||||
sym++;
|
|
||||||
if (sym < 256 && i + 1 < VectorSymbolData.vss_data.length) {
|
|
||||||
VSS_OFFSETS[sym] = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws stroked vector text using IBM Vector Symbol Set (VSS).
|
|
||||||
*/
|
|
||||||
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
|
|
||||||
int cellWidth, int cellHeight, int dir, double angle) {
|
|
||||||
if (text == null || text.isEmpty()) return;
|
|
||||||
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
|
|
||||||
|
|
||||||
int curX = x;
|
|
||||||
int curY = y;
|
|
||||||
int cw = cellWidth > 0 ? cellWidth : 12;
|
|
||||||
int ch = cellHeight > 0 ? cellHeight : 20;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void drawVssChar(int x, int y, char c, int color, int cw, int 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) {
|
|
||||||
int prevVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
|
|
||||||
int prevVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
|
|
||||||
int prevPx = x + (int) Math.round(((double) prevVx / VectorSymbolData.VSS_WIDTH) * cw);
|
|
||||||
int prevPy = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - prevVy) / VectorSymbolData.VSS_HEIGHT) * ch);
|
|
||||||
|
|
||||||
for (int p = 1; p < numPoints; p++) {
|
|
||||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
|
||||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
|
||||||
int px = x + (int) Math.round(((double) vx / VectorSymbolData.VSS_WIDTH) * cw);
|
|
||||||
int py = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch);
|
|
||||||
|
|
||||||
drawLine(prevPx, prevPy, px, py, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
|
|
||||||
prevPx = px;
|
|
||||||
prevPy = py;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptr += 2 + byteLen;
|
|
||||||
} else {
|
|
||||||
ptr++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws raw image pixel bitmap.
|
|
||||||
*/
|
|
||||||
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 : 0xFFFFFFFF;
|
|
||||||
|
|
||||||
for (int row = 0; row < height; row++) {
|
|
||||||
for (int col = 0; col < width; col++) {
|
|
||||||
int bitIndex = row * width + col;
|
|
||||||
int byteIdx = bitIndex / 8;
|
|
||||||
if (byteIdx < imageData.length) {
|
|
||||||
boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0;
|
|
||||||
if (bit) {
|
|
||||||
setPixel(x + col, y + row, fgColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hasContent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.charset;
|
package haus.nightmare.lib3270j.charset;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
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.junit.jupiter.api.Test;
|
||||||
import org.lib3270j.TerminalModel;
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import org.lib3270j.screen.ExtendedAttribute;
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
public class DataStreamProcessorTest {
|
public class DataStreamProcessorTest {
|
||||||
|
|
||||||
@@ -66,4 +66,20 @@ public class DataStreamProcessorTest {
|
|||||||
assertEquals('A', screen.getCell(i).ucs4, "Cell " + i + " should be 'A'");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+37
-7
@@ -1,12 +1,12 @@
|
|||||||
package org.lib3270j.datastream;
|
package haus.nightmare.lib3270j.datastream;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.lib3270j.TerminalModel;
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
import org.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import org.lib3270j.graphics.GraphicsMode;
|
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||||
import org.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
public class QueryReplyBuilderTest {
|
public class QueryReplyBuilderTest {
|
||||||
|
|
||||||
@@ -132,11 +132,15 @@ public class QueryReplyBuilderTest {
|
|||||||
assertEquals(0x81, replies[3] & 0xFF);
|
assertEquals(0x81, replies[3] & 0xFF);
|
||||||
assertEquals(QR_DDM, replies[4] & 0xFF);
|
assertEquals(QR_DDM, replies[4] & 0xFF);
|
||||||
|
|
||||||
// Second SF should be Usable Area
|
// Second SF should be Usable Area: 23 bytes (4 bytes header + 19 bytes payload)
|
||||||
int pos2 = 1 + len1;
|
int pos2 = 1 + len1;
|
||||||
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
|
||||||
|
assertEquals(23, len2);
|
||||||
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
|
||||||
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
|
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
|
||||||
|
// Offset 0x15 (21 in SF payload = pos2 + 21): Buffer size high
|
||||||
|
int bufSize = ((replies[pos2 + 21] & 0xFF) << 8) | (replies[pos2 + 22] & 0xFF);
|
||||||
|
assertEquals(80 * 43, bufSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -232,4 +236,30 @@ public class QueryReplyBuilderTest {
|
|||||||
assertEquals(0x07, replies[descOffset + 49] & 0xFF);
|
assertEquals(0x07, replies[descOffset + 49] & 0xFF);
|
||||||
assertEquals(0xC0, replies[descOffset + 49 + 1] & 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 org.junit.jupiter.api.Test;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package haus.nightmare.lib3270j.ft;
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class FTDftTest {
|
||||||
|
|
||||||
|
private EbcdicTranslator translator;
|
||||||
|
private ScreenBuffer screen;
|
||||||
|
private InputProcessor input;
|
||||||
|
private TestListener listener;
|
||||||
|
private FTDft dft;
|
||||||
|
private FTConfig config;
|
||||||
|
|
||||||
|
private static class TestListener implements FTDft.FTDftListener {
|
||||||
|
boolean dftRunningCalled = false;
|
||||||
|
boolean completeCalled = false;
|
||||||
|
boolean abortCalled = false;
|
||||||
|
String completeMsg = null;
|
||||||
|
String abortMsg = null;
|
||||||
|
long bytesTransferred = 0;
|
||||||
|
FTConstants.FTState state = FTConstants.FTState.RUNNING;
|
||||||
|
FTConfig config;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDftRunning() {
|
||||||
|
dftRunningCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransferComplete(String errorMessage) {
|
||||||
|
completeCalled = true;
|
||||||
|
completeMsg = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransferAborted(String errorMessage) {
|
||||||
|
abortCalled = true;
|
||||||
|
abortMsg = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBytesTransferred(long bytes) {
|
||||||
|
bytesTransferred = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FTConstants.FTState getCurrentState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setState(FTConstants.FTState state) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FTConfig getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getLocalFile() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
translator = new EbcdicTranslator();
|
||||||
|
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||||
|
input = new InputProcessor(screen, translator, null);
|
||||||
|
listener = new TestListener();
|
||||||
|
config = new FTConfig();
|
||||||
|
config.setDirection(FTConfig.Direction.RECEIVE);
|
||||||
|
config.setTransferMode(FTConfig.TransferMode.BINARY);
|
||||||
|
listener.config = config;
|
||||||
|
|
||||||
|
dft = new FTDft(input, translator, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenMessageStreamAsciiAndHandleTrans14Error() {
|
||||||
|
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||||
|
dft.initTransfer(null, outStream);
|
||||||
|
|
||||||
|
// 1. Simulate TR_OPEN_REQ with ASCII "FT:MSG" matching j3270.log:
|
||||||
|
// SF length = 0x0023, SF type = 0xD0, Request code = 0x0012, stream name "FT:MSG " at offset 28
|
||||||
|
byte[] openReq = new byte[] {
|
||||||
|
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
|
||||||
|
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
|
||||||
|
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
|
||||||
|
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // "FT:MSG "
|
||||||
|
};
|
||||||
|
|
||||||
|
dft.processStructuredField(openReq, 0, openReq.length);
|
||||||
|
|
||||||
|
// Should NOT trigger onDftRunning because it's a message stream
|
||||||
|
assertFalse(listener.dftRunningCalled);
|
||||||
|
|
||||||
|
// 2. Simulate TR_DATA_INSERT on the message stream with "TRANS14 Error reading file from host: ..."
|
||||||
|
String errorMsg = "TRANS14 Error reading file from host: file format invalid";
|
||||||
|
byte[] msgBytes = errorMsg.getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
int totalPayloadLen = msgBytes.length + 5;
|
||||||
|
|
||||||
|
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
|
||||||
|
sfOut.write(0); sfOut.write(0); // placeholder length
|
||||||
|
sfOut.write(0xD0); // SF_TRANSFER_DATA
|
||||||
|
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
|
||||||
|
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
|
||||||
|
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
|
||||||
|
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
|
||||||
|
sfOut.write(FTConstants.TR_BEGIN_DATA);
|
||||||
|
sfOut.write((totalPayloadLen >> 8) & 0xFF);
|
||||||
|
sfOut.write(totalPayloadLen & 0xFF);
|
||||||
|
sfOut.write(msgBytes, 0, msgBytes.length);
|
||||||
|
|
||||||
|
byte[] insertData = sfOut.toByteArray();
|
||||||
|
int sfLen = insertData.length;
|
||||||
|
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
|
||||||
|
insertData[1] = (byte) (sfLen & 0xFF);
|
||||||
|
|
||||||
|
dft.processStructuredField(insertData, 0, insertData.length);
|
||||||
|
|
||||||
|
// Verify that onTransferAborted was called with the error message
|
||||||
|
assertTrue(listener.abortCalled, "onTransferAborted should be called for TRANS14 error");
|
||||||
|
assertNotNull(listener.abortMsg);
|
||||||
|
assertTrue(listener.abortMsg.contains("TRANS14"));
|
||||||
|
assertFalse(listener.completeCalled, "onTransferComplete should NOT be called for an error");
|
||||||
|
|
||||||
|
// Verify that the error message was NOT written to the destination file stream!
|
||||||
|
assertEquals(0, outStream.size(), "Error message should not be written to destination file");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenDataStreamAndWriteData() {
|
||||||
|
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||||
|
dft.initTransfer(null, outStream);
|
||||||
|
|
||||||
|
// 1. Simulate TR_OPEN_REQ with ASCII "FT:DATA"
|
||||||
|
byte[] openReq = new byte[] {
|
||||||
|
0x00, 0x23, (byte) 0xD0, 0x00, 0x12, 0x01, 0x06, 0x01, 0x01, 0x04,
|
||||||
|
0x03, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01,
|
||||||
|
0x00, 0x50, 0x05, 0x52, 0x03, (byte) 0xF0, 0x03, 0x09,
|
||||||
|
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // "FT:DATA"
|
||||||
|
};
|
||||||
|
|
||||||
|
dft.processStructuredField(openReq, 0, openReq.length);
|
||||||
|
assertTrue(listener.dftRunningCalled, "onDftRunning should be called for file data stream");
|
||||||
|
|
||||||
|
// 2. Simulate TR_DATA_INSERT with binary file payload
|
||||||
|
byte[] fileData = new byte[] { 0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34 }; // "%PDF-1.4"
|
||||||
|
int totalPayloadLen = fileData.length + 5;
|
||||||
|
|
||||||
|
ByteArrayOutputStream sfOut = new ByteArrayOutputStream();
|
||||||
|
sfOut.write(0); sfOut.write(0);
|
||||||
|
sfOut.write(0xD0);
|
||||||
|
sfOut.write((FTConstants.TR_DATA_INSERT >> 8) & 0xFF);
|
||||||
|
sfOut.write(FTConstants.TR_DATA_INSERT & 0xFF);
|
||||||
|
sfOut.write((FTConstants.TR_NOT_COMPRESSED >> 8) & 0xFF);
|
||||||
|
sfOut.write(FTConstants.TR_NOT_COMPRESSED & 0xFF);
|
||||||
|
sfOut.write(FTConstants.TR_BEGIN_DATA);
|
||||||
|
sfOut.write((totalPayloadLen >> 8) & 0xFF);
|
||||||
|
sfOut.write(totalPayloadLen & 0xFF);
|
||||||
|
sfOut.write(fileData, 0, fileData.length);
|
||||||
|
|
||||||
|
byte[] insertData = sfOut.toByteArray();
|
||||||
|
int sfLen = insertData.length;
|
||||||
|
insertData[0] = (byte) ((sfLen >> 8) & 0xFF);
|
||||||
|
insertData[1] = (byte) (sfLen & 0xFF);
|
||||||
|
|
||||||
|
dft.processStructuredField(insertData, 0, insertData.length);
|
||||||
|
|
||||||
|
// Data must be written to file stream
|
||||||
|
assertArrayEquals(fileData, outStream.toByteArray());
|
||||||
|
assertEquals(8, listener.bytesTransferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.awt.Point;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class GddmCoordinateTransformTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testModel4PresentationSpaceMetrics() {
|
||||||
|
GddmCoordinateTransform transform = new GddmCoordinateTransform(80, 43, 9, 16);
|
||||||
|
|
||||||
|
assertEquals(720, transform.getTotalWidth());
|
||||||
|
assertEquals(688, transform.getTotalHeight());
|
||||||
|
assertEquals(360, transform.getXMax());
|
||||||
|
assertEquals(343, transform.getYMax());
|
||||||
|
|
||||||
|
// Screen center (0, 0)
|
||||||
|
Point baseCenter = transform.gocaToBase(0, 0);
|
||||||
|
assertEquals(360, baseCenter.x);
|
||||||
|
assertEquals(343, baseCenter.y);
|
||||||
|
assertEquals(new Point(0, 0), transform.baseToGoca(360, 343));
|
||||||
|
|
||||||
|
// Bidirectional screen pixel transformation
|
||||||
|
// Cell width = 10, Cell height = 20, Offset = (100, 50)
|
||||||
|
int ox = 100, oy = 50, cw = 10, ch = 20;
|
||||||
|
Point screenPt = transform.gocaToScreenPixel(0, 0, ox, oy, cw, ch);
|
||||||
|
Point roundtripGoca = transform.screenPixelToGoca(screenPt.x, screenPt.y, ox, oy, cw, ch);
|
||||||
|
assertEquals(0, roundtripGoca.x);
|
||||||
|
assertEquals(0, roundtripGoca.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testModel2PresentationSpaceMetrics() {
|
||||||
|
GddmCoordinateTransform transform = new GddmCoordinateTransform(80, 24, 9, 16);
|
||||||
|
|
||||||
|
assertEquals(720, transform.getTotalWidth());
|
||||||
|
assertEquals(384, transform.getTotalHeight());
|
||||||
|
assertEquals(360, transform.getXMax());
|
||||||
|
assertEquals(191, transform.getYMax());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,817 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
// Standard IBM 3179G 43-row presentation space (80*9 = 720 wide, 43*16 = 688 high)
|
||||||
|
assertEquals(720, plane.getTotalWidth());
|
||||||
|
assertEquals(688, plane.getTotalHeight());
|
||||||
|
assertEquals(360, plane.getXMax());
|
||||||
|
assertEquals(343, plane.getYMax());
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
|
||||||
|
// Standard IBM 3179G Model 2 (24-row) presentation space (24*16 = 384 high, yMax = 191)
|
||||||
|
plane.setScreenDimensions(80, 24);
|
||||||
|
assertEquals(720, plane.getTotalWidth());
|
||||||
|
assertEquals(384, plane.getTotalHeight());
|
||||||
|
assertEquals(360, plane.getXMax());
|
||||||
|
assertEquals(191, plane.getYMax());
|
||||||
|
}
|
||||||
|
|
||||||
|
@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, Pattern 15/Empty, GSCOL White inside GBAR)
|
||||||
|
out.write(GocaConstants.G_GSCOL); out.write(0x08); // Black (background)
|
||||||
|
out.write(GocaConstants.G_GSPT); out.write(0x0F); // Empty pattern (transparent interior)
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
package org.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.lib3270j.ConnectionConfig;
|
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
public class GraphicsModeTest {
|
public class GraphicsModeTest {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package org.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
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(0x34, sf[1]); // Length = 52 bytes (0x0034) matching IBM HODInput.java
|
||||||
|
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 matching HODInput.java (0x04 trigger constants and 0x23 correlation descriptor)
|
||||||
|
byte[] sfMouse = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||||
|
294, 192, 1, true, false, false
|
||||||
|
);
|
||||||
|
assertEquals(56, sfMouse.length);
|
||||||
|
assertEquals(0x00, sfMouse[0]);
|
||||||
|
assertEquals(0x34, sfMouse[1]); // Length = 52 bytes (0x0034) matching IBM HODInput.java
|
||||||
|
assertEquals(0x23, sfMouse[16]);
|
||||||
|
assertEquals(0x00, sfMouse[17]);
|
||||||
|
assertEquals(0x23, sfMouse[18]);
|
||||||
|
assertEquals(0x00, sfMouse[19]);
|
||||||
|
int mx = (sfMouse[24] << 8) | (sfMouse[25] & 0xFF);
|
||||||
|
int my = (sfMouse[26] << 8) | (sfMouse[27] & 0xFF);
|
||||||
|
assertEquals(294, (short) mx);
|
||||||
|
assertEquals(192, (short) my);
|
||||||
|
assertEquals(0x00, sfMouse[28]);
|
||||||
|
assertEquals(0x00, sfMouse[29]);
|
||||||
|
assertEquals(0x00, sfMouse[30]);
|
||||||
|
assertEquals(0x04, sfMouse[31]); // Mouse trigger class constant (0x04)
|
||||||
|
assertEquals(0x00, sfMouse[32]);
|
||||||
|
assertEquals(0x04, sfMouse[33]); // Mouse correlation class constant (0x04)
|
||||||
|
assertEquals(0x00, sfMouse[34]);
|
||||||
|
assertEquals(0x01, sfMouse[35]); // Button 1
|
||||||
|
|
||||||
|
// Mouse Button 2 (Action) with Shift modifier
|
||||||
|
byte[] sfMouse2 = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||||
|
0, 0, 2, true, true, false
|
||||||
|
);
|
||||||
|
assertEquals(0x23, sfMouse2[16]);
|
||||||
|
assertEquals(0x00, sfMouse2[17]);
|
||||||
|
assertEquals(0x23, sfMouse2[18]);
|
||||||
|
assertEquals(0x00, sfMouse2[19]);
|
||||||
|
assertEquals(0x04, sfMouse2[31]);
|
||||||
|
assertEquals(0x04, sfMouse2[33]);
|
||||||
|
assertEquals((byte) 0x80, sfMouse2[34]); // Shift modifier
|
||||||
|
assertEquals(0x02, sfMouse2[35]); // Button 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLightPenSelectImmediateSpace() {
|
||||||
|
screen.erase(false);
|
||||||
|
// Field attribute at pos 0: unprotected, intensified selectable (0x08)
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_HIGH_SEL));
|
||||||
|
// Designator character at pos 1: space (immediate select)
|
||||||
|
screen.getCell(1).ec = 0x40;
|
||||||
|
screen.getCell(1).ucs4 = ' ';
|
||||||
|
screen.getCell(2).ec = (byte) 0xC1;
|
||||||
|
screen.getCell(2).ucs4 = 'A';
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
public void sendAid(int aidCode) {
|
||||||
|
sentAid.set(aidCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
boolean result = input.lightPenSelect(2);
|
||||||
|
assertTrue(result);
|
||||||
|
assertEquals(AID_SELECT, sentAid.get());
|
||||||
|
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||||
|
assertEquals(2, screen.getCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLightPenSelectEnterImmediateAmpersand() {
|
||||||
|
screen.erase(false);
|
||||||
|
// Field attribute at pos 0: normal detectable (0x04)
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||||
|
// Designator character at pos 1: '&' (enter immediate)
|
||||||
|
screen.getCell(1).ec = 0x50;
|
||||||
|
screen.getCell(1).ucs4 = '&';
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
public void sendAid(int aidCode) {
|
||||||
|
sentAid.set(aidCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
boolean result = input.lightPenSelect(1);
|
||||||
|
assertTrue(result);
|
||||||
|
assertEquals(AID_ENTER, sentAid.get());
|
||||||
|
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLightPenSelectDeferredToggle() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||||
|
// Designator at pos 1: '?' (deferred select)
|
||||||
|
screen.getCell(1).ec = 0x6F;
|
||||||
|
screen.getCell(1).ucs4 = '?';
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
public void sendAid(int aidCode) {
|
||||||
|
sentAid.set(aidCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// First click: ? -> > and MDT=1, no transmission
|
||||||
|
boolean r1 = input.lightPenSelect(1);
|
||||||
|
assertTrue(r1);
|
||||||
|
assertEquals(-1, sentAid.get());
|
||||||
|
assertEquals('>', screen.getCell(1).ucs4);
|
||||||
|
assertEquals((byte) 0x6E, screen.getCell(1).ec);
|
||||||
|
assertTrue((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||||
|
|
||||||
|
// Second click: > -> ? and MDT=0, no transmission
|
||||||
|
boolean r2 = input.lightPenSelect(1);
|
||||||
|
assertTrue(r2);
|
||||||
|
assertEquals(-1, sentAid.get());
|
||||||
|
assertEquals('?', screen.getCell(1).ucs4);
|
||||||
|
assertEquals((byte) 0x6F, screen.getCell(1).ec);
|
||||||
|
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLightPenSelectInvalidDesignatorRejected() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||||
|
// Designator at pos 1: 'A' (invalid designator)
|
||||||
|
screen.getCell(1).ec = (byte) 0xC1;
|
||||||
|
screen.getCell(1).ucs4 = 'A';
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
public void sendAid(int aidCode) {
|
||||||
|
sentAid.set(aidCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
boolean result = input.lightPenSelect(1);
|
||||||
|
assertFalse(result);
|
||||||
|
assertEquals(-1, sentAid.get());
|
||||||
|
assertFalse((screen.getCell(0).fa & FA_MODIFY) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCursorSelectKey() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_INT_NORM_SEL));
|
||||||
|
screen.getCell(1).ec = 0x50;
|
||||||
|
screen.getCell(1).ucs4 = '&';
|
||||||
|
screen.setCursorAddress(5);
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicInteger sentAid = new java.util.concurrent.atomic.AtomicInteger(-1);
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
public void sendAid(int aidCode) {
|
||||||
|
sentAid.set(aidCode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
boolean result = input.cursorSelect();
|
||||||
|
assertTrue(result);
|
||||||
|
assertEquals(AID_ENTER, sentAid.get());
|
||||||
|
assertEquals(5, screen.getCursorAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@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'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSendGraphicMouseAidFraming() {
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||||
|
screen.getCell(1).ec = (byte) 0xC1; // 'A'
|
||||||
|
screen.setCellFA(5, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
screen.setCursorAddress(2);
|
||||||
|
|
||||||
|
haus.nightmare.lib3270j.graphics.GraphicsPlane plane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
|
||||||
|
haus.nightmare.lib3270j.graphics.GocaDecoder goca = new haus.nightmare.lib3270j.graphics.GocaDecoder(plane);
|
||||||
|
goca.setGraphicsCursorActive(true);
|
||||||
|
goca.setGraphicCursorPosition(150, -80);
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
protected void sendAidResponse(byte[] data) {
|
||||||
|
sent.set(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.setGocaDecoder(goca);
|
||||||
|
|
||||||
|
input.sendGraphicMouseAid(AID_ENTER, 1, false, false);
|
||||||
|
|
||||||
|
byte[] result = sent.get();
|
||||||
|
assertNotNull(result);
|
||||||
|
// Total expected length:
|
||||||
|
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes
|
||||||
|
assertEquals(64, result.length);
|
||||||
|
assertEquals((byte) AID_SF, result[0]);
|
||||||
|
// SF length = 52 (0x00 0x34) per IBM HOD / GOCA specification
|
||||||
|
assertEquals(0x00, result[1]);
|
||||||
|
assertEquals(0x34, result[2]);
|
||||||
|
// SF ID = 0x0F0F
|
||||||
|
assertEquals(0x0F, result[3]);
|
||||||
|
assertEquals(0x0F, result[4]);
|
||||||
|
// Coordinates in SF at index 1 + 24 = 25
|
||||||
|
int gx = (result[25] << 8) | (result[26] & 0xFF);
|
||||||
|
int gy = (result[27] << 8) | (result[28] & 0xFF);
|
||||||
|
assertEquals(150, (short) gx);
|
||||||
|
assertEquals(-80, (short) gy);
|
||||||
|
// Mouse constant at index 1 + 31 = 32
|
||||||
|
assertEquals(0x04, result[32]);
|
||||||
|
assertEquals(0x04, result[34]);
|
||||||
|
// Button 1 at index 1 + 35 = 36
|
||||||
|
assertEquals(0x01, result[36]);
|
||||||
|
|
||||||
|
// Trailing AID at index 57
|
||||||
|
assertEquals((byte) AID_ENTER, result[57]);
|
||||||
|
// Trailing SBA at 60
|
||||||
|
assertEquals((byte) ORDER_SBA, result[60]);
|
||||||
|
// Trailing field content 'A' at 63
|
||||||
|
assertEquals((byte) 0xC1, result[63]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAidSelectFramingOmitsFieldData() {
|
||||||
|
// Formatted screen with an unprotected field at 0, modified, containing 'A' and 'B'
|
||||||
|
screen.erase(false);
|
||||||
|
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||||
|
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||||
|
screen.getCell(1).ec = (byte) 0xC1;
|
||||||
|
screen.getCell(1).ucs4 = 'A';
|
||||||
|
screen.getCell(2).ec = (byte) 0xC2;
|
||||||
|
screen.getCell(2).ucs4 = 'B';
|
||||||
|
screen.setCursorAddress(1);
|
||||||
|
|
||||||
|
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
InputProcessor input = new InputProcessor(screen, translator, null) {
|
||||||
|
@Override
|
||||||
|
protected void sendAidResponse(byte[] data) {
|
||||||
|
sent.set(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.sendAid(AID_SELECT);
|
||||||
|
|
||||||
|
byte[] result = sent.get();
|
||||||
|
assertNotNull(result);
|
||||||
|
// For AID_SELECT (0x7E), the stream consists ONLY of:
|
||||||
|
// 1 byte AID (0x7E) + 2 bytes cursor address + 1 byte SBA (0x11) + 2 bytes designator address = 6 bytes total.
|
||||||
|
// Field character contents ('A', 'B') MUST NOT be sent per IBM 3270 DS architecture!
|
||||||
|
assertEquals(6, result.length);
|
||||||
|
assertEquals((byte) AID_SELECT, result[0]);
|
||||||
|
// SBA order at byte 3
|
||||||
|
assertEquals((byte) ORDER_SBA, result[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user