Compare commits
42 Commits
v0.1.0-beta.1
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
db6043cb39
|
|||
|
161364a95f
|
|||
|
a56aa5f059
|
|||
|
7b5bc7ec11
|
|||
|
a9956298e2
|
|||
|
a43969da08
|
|||
|
e18d2f436f
|
|||
|
27976dd31f
|
|||
|
a3c4b95379
|
|||
|
3a576b79b4
|
|||
|
40ebd40fe2
|
|||
|
f310d39fc1
|
|||
|
a44ea00d3a
|
|||
|
037ad1f941
|
|||
|
79258c0c44
|
|||
|
ff5dfda343
|
|||
|
8654b72d8c
|
|||
|
2e9b6d325a
|
|||
|
540a8dfd1d
|
|||
|
ebc0b5ae6f
|
|||
|
bcfd4ba2e0
|
|||
|
2584f4289f
|
|||
|
3526e682a1
|
|||
|
7eebd2107c
|
|||
|
36b93df986
|
|||
|
dbe0cdaa89
|
|||
|
b5a4978b24
|
|||
| d2bdb9bb0a | |||
| e28b7dc30a | |||
| d9df1052a1 | |||
| 0837de8db3 | |||
| 42eef519a6 | |||
|
b57d8f960d
|
|||
|
09600260b5
|
|||
|
00c7b0770c
|
|||
|
1212b2cceb
|
|||
|
bf5e4410e6
|
|||
|
7e658a6c20
|
|||
|
c32521fd4b
|
|||
|
d7d2f8bad5
|
|||
|
f0fc1b6e70
|
|||
| 10a31476c2 |
@@ -1,3 +1,9 @@
|
||||
j3270.log.*
|
||||
*.txt
|
||||
*.py
|
||||
*.pcap
|
||||
build/*
|
||||
*.log
|
||||
.DS_Store
|
||||
*.jar
|
||||
build
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# j3270
|
||||
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||
[](https://adoptium.net)
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
|
||||
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E.
|
||||
|
||||
@@ -24,6 +24,19 @@ Requires **Java 11 or higher** (Java 11, 17, 21+):
|
||||
# Launch interactive GUI
|
||||
java -jar j3270.jar
|
||||
|
||||
# Connect to TLS/SSL mainframe with custom port
|
||||
java -jar j3270.jar -s mainframe.example.com 992
|
||||
# Or using standard x3270 L: prefix
|
||||
java -jar j3270.jar L:mainframe.example.com:992
|
||||
|
||||
# Connect via Plain TN3270 (Non-E)
|
||||
java -jar j3270.jar -P mainframe.example.com 23
|
||||
# Or using standard x3270 P: prefix
|
||||
java -jar j3270.jar P:mainframe.example.com:23
|
||||
|
||||
# Connect with unverified/self-signed certificate verification bypass
|
||||
java -jar j3270.jar --tls --insecure mainframe.example.com 992
|
||||
|
||||
# Launch with custom configuration file
|
||||
java -jar j3270.jar -c config.ini
|
||||
|
||||
@@ -35,7 +48,11 @@ java -jar j3270.jar -c config.ini
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption.
|
||||
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, plain TN3270 fallback, and SSL/TLS encryption.
|
||||
- **SSL/TLS Security**:
|
||||
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports.
|
||||
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection.
|
||||
- Optional unverified/insecure mode for testing and headless scripting.
|
||||
- **IND$FILE File Transfer**: Full support for both **CUT** and **DFT (DDM)** high-speed structured field transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS.
|
||||
- **z/VM & Line-Mode Support**: Proper SSCP-LU and unformatted line handling (`CP TERM CONMODE 3270` supported).
|
||||
- **APL & Graphic Escape**: Comprehensive box-drawing and math symbol character rendering.
|
||||
@@ -59,7 +76,7 @@ The project includes self-contained, portable build scripts:
|
||||
# Build executable JAR in 2 seconds:
|
||||
sh ./build_all.sh
|
||||
|
||||
# Run all 25 automated unit tests in ~130ms:
|
||||
# Run all 57 automated unit tests in ~500ms:
|
||||
sh ./test_all.sh
|
||||
```
|
||||
|
||||
@@ -73,38 +90,13 @@ The resulting standalone JAR is created at `build/j3270.jar`.
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ How to Tag & Publish Releases
|
||||
|
||||
Releases can be published automatically to Gitea via Git tags:
|
||||
|
||||
```bash
|
||||
# 1. Create a version tag
|
||||
git tag -a v0.1.0 -m "Release v0.1.0"
|
||||
|
||||
# 2. Push the tag to Gitea
|
||||
git push origin v0.1.0
|
||||
```
|
||||
|
||||
Pushing any `v*` tag triggers the automated release workflow (`.gitea/workflows/release.yaml`), which runs tests, builds the JAR, and publishes a new Gitea Release with `j3270.jar` attached automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Topics & Discoverability
|
||||
|
||||
Suggested keywords/topics for repository indexing:
|
||||
`tn3270` · `tn3270e` · `ibm-3270` · `mainframe` · `terminal-emulator` · `x3270` · `ind-file` · `file-transfer` · `ebcdic` · `z-vm` · `z-os` · `cms` · `tso` · `cics` · `java` · `desktop-app`
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
|
||||
|
||||
---
|
||||
|
||||
## 📜 Acknowledgements
|
||||
|
||||
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
|
||||
- [Antigravity](https://antigravity.google)
|
||||
- [Antigravity](https://antigravity.google/)
|
||||
- Claude Opus 4.6
|
||||
- Gemini 3.1 Pro
|
||||
- Gemini 3.7 Flash
|
||||
- Gemma4 12B and 26B
|
||||
- GPT-OSS 120B
|
||||
- Qwen3 4B and 32B
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Fixed Bugs
|
||||
- Complete failure when using `cp term conmode 3270` under VM:
|
||||
Fixed in InputProcessor by sending raw line-mode EBCDIC character data in SSCP-LU mode instead of 3270 AID headers and 1920-byte buffer dumps, and correctly processing subsequent 3270 stream transitions upon CONMODE 3270.
|
||||
|
||||
- Unable to start a 2nd transfer after first completed ("A transfer is already in progress"):
|
||||
Fixed in FTDft by signaling completion upon handling `TR_CLOSE_REQ` / host completion messages, and adding state reset in FileTransfer.
|
||||
|
||||
- IND$FILE CMS and TSO:
|
||||
Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
|
||||
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
allprojects {
|
||||
group = 'org.j3270'
|
||||
group = 'haus.nightmare.j3270'
|
||||
version = '0.1.0'
|
||||
|
||||
repositories {
|
||||
@@ -10,8 +10,10 @@ allprojects {
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
test {
|
||||
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
|
||||
echo "Packaging j3270.jar..."
|
||||
printf "Manifest-Version: 1.0\nMain-Class: org.pubvm.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF"
|
||||
printf "Manifest-Version: 1.0\nMain-Class: haus.nightmare.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF"
|
||||
|
||||
"$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
|
||||
-C "$BUILD_DIR/lib3270j" . \
|
||||
@@ -87,3 +87,60 @@ echo "=== Build Complete ==="
|
||||
echo "Executable JAR created: $BUILD_DIR/j3270.jar"
|
||||
ls -lh "$BUILD_DIR/j3270.jar"
|
||||
|
||||
# 5. Compile and run tests if requested
|
||||
if [ "$1" = "test" ] || [ "$1" = "check" ]; then
|
||||
echo "=== Running Tests ==="
|
||||
TEST_BUILD_DIR="$BUILD_DIR/test-classes"
|
||||
mkdir -p "$TEST_BUILD_DIR"
|
||||
JUNIT_CP=$(find "$HOME/.gradle/caches/modules-2/files-2.1/" -name "*.jar" 2>/dev/null | grep -E "junit|opentest|apiguardian" | tr "\n" ":" || true)
|
||||
FULL_CP="$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$JUNIT_CP"
|
||||
|
||||
find "$SCRIPT_DIR/lib3270j/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
|
||||
if [ -d "$SCRIPT_DIR/j3270/src/test/java" ]; then
|
||||
find "$SCRIPT_DIR/j3270/src/test/java" -name "*.java" >> "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -s "$BUILD_DIR/test_sources.txt" ] && [ -n "$JUNIT_CP" ]; then
|
||||
echo "Compiling tests..."
|
||||
"$JAVAC_BIN" -cp "$FULL_CP" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt"
|
||||
rm -f "$BUILD_DIR/test_sources.txt"
|
||||
|
||||
echo "Executing tests with JUnit Platform..."
|
||||
cat << 'EOF' > "$BUILD_DIR/TestRunner.java"
|
||||
import org.junit.platform.launcher.*;
|
||||
import org.junit.platform.launcher.core.*;
|
||||
import org.junit.platform.launcher.listeners.*;
|
||||
import org.junit.platform.engine.discovery.*;
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.*;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
public class TestRunner {
|
||||
public static void main(String[] args) {
|
||||
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
|
||||
.selectors(
|
||||
selectPackage("haus.nightmare")
|
||||
)
|
||||
.build();
|
||||
|
||||
Launcher launcher = LauncherFactory.create();
|
||||
SummaryGeneratingListener listener = new SummaryGeneratingListener();
|
||||
launcher.registerTestExecutionListeners(listener);
|
||||
launcher.execute(request);
|
||||
|
||||
TestExecutionSummary summary = listener.getSummary();
|
||||
summary.printTo(new PrintWriter(System.out));
|
||||
summary.printFailuresTo(new PrintWriter(System.err));
|
||||
|
||||
if (summary.getTotalFailureCount() > 0) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
"$JAVAC_BIN" -cp "$FULL_CP:$TEST_BUILD_DIR" -d "$BUILD_DIR" "$BUILD_DIR/TestRunner.java"
|
||||
JAVA_BIN_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/java}"
|
||||
JAVA_BIN_EXEC="${JAVA_BIN_EXEC:-java}"
|
||||
"$JAVA_BIN_EXEC" -cp "$FULL_CP:$TEST_BUILD_DIR:$BUILD_DIR" TestRunner
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec "$(dirname "$0")/build_all.sh"
|
||||
exec "$(dirname "$0")/build_all.sh" "$@"
|
||||
|
||||
+2
-2
@@ -7,12 +7,12 @@ dependencies {
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'org.pubvm.j3270.J3270App'
|
||||
mainClass = 'haus.nightmare.j3270.J3270App'
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Main-Class': 'org.pubvm.j3270.J3270App',
|
||||
attributes 'Main-Class': 'haus.nightmare.j3270.J3270App',
|
||||
'Implementation-Title': 'j3270',
|
||||
'Implementation-Version': archiveVersion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,951 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.lib3270j.*;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.j3270.ui.*;
|
||||
import haus.nightmare.j3270.ft.FileTransfer;
|
||||
import haus.nightmare.j3270.ft.FileTransferDialog;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.event.*;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.*;
|
||||
|
||||
/**
|
||||
* j3270 — Java TN3270 Terminal Emulator.
|
||||
* Main application class. Creates the main window with terminal display,
|
||||
* status bar, menu bar, and manages the Telnet3270Client lifecycle.
|
||||
*/
|
||||
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
|
||||
|
||||
private static final Logger log = Logger.getLogger(J3270App.class.getName());
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
private StatusBar statusBar;
|
||||
private Timer refreshTimer;
|
||||
|
||||
// Last connection details for reconnect
|
||||
private String lastHost = "";
|
||||
private int lastPort = 23;
|
||||
|
||||
private FileTransfer fileTransfer;
|
||||
private final java.util.concurrent.atomic.AtomicBoolean screenUpdatePending = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
// Dialog instances
|
||||
private FindDialog findDialog;
|
||||
private ScriptDialog scriptDialog;
|
||||
private FieldInspectorDialog fieldInspectorDialog;
|
||||
private PrinterSessionDialog printerSessionDialog;
|
||||
|
||||
public J3270App() {
|
||||
super("j3270 — Java TN3270 Terminal Emulator");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setBackground(Color.BLACK);
|
||||
|
||||
buildUI();
|
||||
buildMenuBar();
|
||||
|
||||
ThemeManager.addThemeChangeListener(this::onThemeChanged);
|
||||
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
setMinimumSize(new Dimension(640, 400));
|
||||
|
||||
// Status refresh timer — also ensures focus stays on terminal
|
||||
refreshTimer = new Timer(100, e -> {
|
||||
if (client != null) {
|
||||
statusBar.updateStatus();
|
||||
if (isActive() && !terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
refreshTimer.start();
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
disconnect();
|
||||
if (refreshTimer != null)
|
||||
refreshTimer.stop();
|
||||
if (printerSessionDialog != null) {
|
||||
printerSessionDialog.dispose();
|
||||
}
|
||||
terminalPanel.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
terminalPanel = new TerminalPanel();
|
||||
statusBar = new StatusBar();
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().setBackground(Color.BLACK);
|
||||
getContentPane().add(terminalPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(statusBar, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private void onThemeChanged(UITheme theme) {
|
||||
buildMenuBar();
|
||||
statusBar.applyTheme(theme);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
|
||||
private void buildMenuBar() {
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
ThemeManager.styleMenuBar(menuBar);
|
||||
|
||||
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
|
||||
// 1. File menu
|
||||
JMenu fileMenu = createMenu("File");
|
||||
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
||||
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel)));
|
||||
fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog));
|
||||
fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
||||
disconnect();
|
||||
System.exit(0);
|
||||
}));
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
// 2. Edit menu
|
||||
JMenu editMenu = createMenu("Edit");
|
||||
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
|
||||
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
|
||||
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
|
||||
editMenu.addSeparator();
|
||||
editMenu.add(createMenuItem("Select All", KeyEvent.VK_A, () -> terminalPanel.selectAll()));
|
||||
editMenu.add(createMenuItem("Clear Selection", -1, () -> terminalPanel.clearSelection()));
|
||||
editMenu.addSeparator();
|
||||
editMenu.add(createMenuItem("Find on Screen...", KeyEvent.VK_F, this::showFindDialog));
|
||||
editMenu.add(createMenuItem("Find Next", KeyEvent.VK_G, this::findNext));
|
||||
editMenu.add(createMenuItem("Find Previous", KeyEvent.VK_G, this::findPrevious, true));
|
||||
menuBar.add(editMenu);
|
||||
|
||||
// 3. View menu
|
||||
JMenu viewMenu = createMenu("View");
|
||||
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2), true));
|
||||
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2), true));
|
||||
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
|
||||
terminalPanel.setFontSize(16);
|
||||
terminalPanel.guardedPack();
|
||||
}, true));
|
||||
viewMenu.addSeparator();
|
||||
|
||||
// UI Theme Submenu
|
||||
JMenu themeMenu = createMenu("UI Theme");
|
||||
ButtonGroup themeGroup = new ButtonGroup();
|
||||
|
||||
JRadioButtonMenuItem darkThemeItem = new JRadioButtonMenuItem("Dark Mode", ThemeManager.isDark());
|
||||
ThemeManager.styleMenuItem(darkThemeItem);
|
||||
darkThemeItem.addActionListener(e -> {
|
||||
haus.nightmare.j3270.config.Settings.setJavaUiTheme(UITheme.DARK);
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
});
|
||||
themeGroup.add(darkThemeItem);
|
||||
themeMenu.add(darkThemeItem);
|
||||
|
||||
JRadioButtonMenuItem lightThemeItem = new JRadioButtonMenuItem("Light Mode", ThemeManager.isLight());
|
||||
ThemeManager.styleMenuItem(lightThemeItem);
|
||||
lightThemeItem.addActionListener(e -> {
|
||||
haus.nightmare.j3270.config.Settings.setJavaUiTheme(UITheme.LIGHT);
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
});
|
||||
themeGroup.add(lightThemeItem);
|
||||
themeMenu.add(lightThemeItem);
|
||||
|
||||
viewMenu.add(themeMenu);
|
||||
viewMenu.addSeparator();
|
||||
|
||||
// CodePage Submenu
|
||||
JMenu cpMenu = createMenu("Code Page");
|
||||
String[] codePages = {
|
||||
"037 - US / Canada / Brazil",
|
||||
"1047 - IBM Open Systems / z/OS Unix",
|
||||
"500 - International Latin-1",
|
||||
"273 - Germany / Austria",
|
||||
"277 - Denmark / Norway",
|
||||
"278 - Sweden / Finland",
|
||||
"280 - Italy",
|
||||
"284 - Spain / Latin America",
|
||||
"285 - United Kingdom",
|
||||
"297 - France",
|
||||
"870 - Eastern Europe / Latin-2",
|
||||
"871 - Iceland",
|
||||
"875 - Greece (Greek)",
|
||||
"1026 - Turkey (Turkish)",
|
||||
"1140 - US / Canada (Euro \u20AC)",
|
||||
"1141 - Germany / Austria (Euro \u20AC)",
|
||||
"1148 - International (Euro \u20AC)",
|
||||
"930 - Japanese Katakana Mixed DBCS",
|
||||
"939 - Japanese Latin Mixed DBCS",
|
||||
"935 - Simplified Chinese Mixed DBCS",
|
||||
"937 - Traditional Chinese Mixed DBCS",
|
||||
"933 - Korean Mixed DBCS"
|
||||
};
|
||||
for (String cpItem : codePages) {
|
||||
String cpId = cpItem.split(" -")[0].trim();
|
||||
JMenuItem cpMi = new JMenuItem(cpItem);
|
||||
ThemeManager.styleMenuItem(cpMi);
|
||||
cpMi.addActionListener(e -> changeCodePage(cpId));
|
||||
cpMenu.add(cpMi);
|
||||
}
|
||||
viewMenu.add(cpMenu);
|
||||
|
||||
// Graphics Mode Submenu
|
||||
JMenu gfxMenu = createMenu("Graphics Mode");
|
||||
for (GraphicsMode gm : GraphicsMode.values()) {
|
||||
JMenuItem gmMi = new JMenuItem(gm.name());
|
||||
ThemeManager.styleMenuItem(gmMi);
|
||||
gmMi.addActionListener(e -> changeGraphicsMode(gm));
|
||||
gfxMenu.add(gmMi);
|
||||
}
|
||||
viewMenu.add(gfxMenu);
|
||||
|
||||
viewMenu.addSeparator();
|
||||
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
// 4. Actions menu
|
||||
JMenu actionsMenu = createMenu("Actions");
|
||||
actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendEnter();
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}, true));
|
||||
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
|
||||
if (client != null)
|
||||
client.sendClear();
|
||||
terminalPanel.repaint();
|
||||
}, true));
|
||||
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
|
||||
if (client != null)
|
||||
client.reset();
|
||||
terminalPanel.repaint();
|
||||
}, true));
|
||||
actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.eraseInput();
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}, true));
|
||||
actionsMenu.add(createMenuItem("Attention", KeyEvent.VK_A, () -> {
|
||||
if (client != null && client.isConnected()) client.attn();
|
||||
}, true));
|
||||
actionsMenu.add(createMenuItem("System Request", KeyEvent.VK_S, () -> {
|
||||
if (client != null && client.isConnected()) {
|
||||
client.sysReq();
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}, true));
|
||||
actionsMenu.add(createMenuItem("Cursor Select", KeyEvent.VK_Q, () -> {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.cursorSelect();
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}, true));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
|
||||
terminalPanel.toggleLightPen();
|
||||
}, true));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
|
||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
// 5. Help menu
|
||||
JMenu helpMenu = createMenu("Help");
|
||||
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
|
||||
helpMenu.add(createMenuItem("Mnemonic Keystroke Reference", -1, this::showMnemonicHelp));
|
||||
helpMenu.addSeparator();
|
||||
helpMenu.add(createMenuItem("About", -1, this::showAbout));
|
||||
menuBar.add(helpMenu);
|
||||
|
||||
setJMenuBar(menuBar);
|
||||
}
|
||||
|
||||
private JMenu createMenu(String name) {
|
||||
JMenu menu = new JMenu(name);
|
||||
return ThemeManager.styleMenu(menu);
|
||||
}
|
||||
|
||||
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
|
||||
JMenuItem item = new JMenuItem(name);
|
||||
ThemeManager.styleMenuItem(item);
|
||||
if (mnemonic > 0) {
|
||||
int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
|
||||
}
|
||||
item.addActionListener(e -> action.run());
|
||||
return item;
|
||||
}
|
||||
|
||||
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action) {
|
||||
return createMenuItem(name, mnemonic, action, false);
|
||||
}
|
||||
|
||||
// ========== Dialog Openers ==========
|
||||
|
||||
private void showFindDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before using Find.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (findDialog == null) {
|
||||
findDialog = new FindDialog(this, client, terminalPanel);
|
||||
}
|
||||
findDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void findNext() {
|
||||
if (client == null) return;
|
||||
String last = FindDialog.getLastSearchText();
|
||||
if (last == null || last.isEmpty()) {
|
||||
showFindDialog();
|
||||
} else {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int cols = sb.getDisplayCols();
|
||||
int curRow = curAddr / cols;
|
||||
int curCol = (curAddr % cols + 1) % cols;
|
||||
if (curCol == 0) curRow = (curRow + 1) % sb.getDisplayRows();
|
||||
|
||||
int found = client.getPS().searchString(last, curRow, curCol, ECLConstants.SEARCH_FORWARD, !FindDialog.getLastMatchCase());
|
||||
if (found >= 0) {
|
||||
sb.setCursorAddress(found);
|
||||
terminalPanel.setSearchHighlight(found, last.length());
|
||||
terminalPanel.repaint();
|
||||
} else {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void findPrevious() {
|
||||
if (client == null) return;
|
||||
String last = FindDialog.getLastSearchText();
|
||||
if (last == null || last.isEmpty()) {
|
||||
showFindDialog();
|
||||
} else {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int cols = sb.getDisplayCols();
|
||||
int curRow = curAddr / cols;
|
||||
int curCol = (curAddr % cols - 1 + cols) % cols;
|
||||
if (curCol == cols - 1) curRow = (curRow - 1 + sb.getDisplayRows()) % sb.getDisplayRows();
|
||||
|
||||
int found = client.getPS().searchString(last, curRow, curCol, ECLConstants.SEARCH_BACKWARD, !FindDialog.getLastMatchCase());
|
||||
if (found >= 0) {
|
||||
sb.setCursorAddress(found);
|
||||
terminalPanel.setSearchHighlight(found, last.length());
|
||||
terminalPanel.repaint();
|
||||
} else {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showScriptDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before running scripts.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (scriptDialog == null) {
|
||||
scriptDialog = new ScriptDialog(this, client, terminalPanel);
|
||||
}
|
||||
scriptDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showFieldInspectorDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (fieldInspectorDialog == null) {
|
||||
fieldInspectorDialog = new FieldInspectorDialog(this, client, terminalPanel);
|
||||
} else {
|
||||
fieldInspectorDialog.refreshFields();
|
||||
}
|
||||
fieldInspectorDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showPrinterSessionDialog() {
|
||||
if (printerSessionDialog == null) {
|
||||
String host = (client != null) ? client.getConfig().getHost() : lastHost;
|
||||
int port = (client != null) ? client.getConfig().getPort() : lastPort;
|
||||
boolean tls = (client != null) ? client.getConfig().isUseTls() : false;
|
||||
printerSessionDialog = new PrinterSessionDialog(this, host, port, tls);
|
||||
}
|
||||
printerSessionDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showPasteLineWrapDialog() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor);
|
||||
if (text == null || text.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Clipboard is empty.", "Clipboard", JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
int cols = client.getScreenBuffer().getDisplayCols();
|
||||
String endColStr = JOptionPane.showInputDialog(this, "Enter right margin column (1-" + cols + ") for wrapping [0 = no right margin]:", "80");
|
||||
if (endColStr != null) {
|
||||
int endCol = 0;
|
||||
try {
|
||||
endCol = Integer.parseInt(endColStr.trim());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
terminalPanel.pasteLineWrap(text, endCol, true);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Could not read clipboard: " + ex.getMessage(), "Paste Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void changeCodePage(String cpId) {
|
||||
if (client != null) {
|
||||
client.setCodePage(cpId);
|
||||
client.getScreenBuffer().translateToUnicode();
|
||||
terminalPanel.refreshScreen();
|
||||
statusBar.updateStatus();
|
||||
}
|
||||
haus.nightmare.j3270.config.Settings.setCodePage(cpId);
|
||||
}
|
||||
|
||||
private void changeGraphicsMode(GraphicsMode mode) {
|
||||
if (client != null && client.getDataStreamProcessor() != null) {
|
||||
client.getDataStreamProcessor().getQueryReplyBuilder().setGraphicsMode(mode);
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
haus.nightmare.j3270.config.Settings.setGraphicsMode(mode);
|
||||
}
|
||||
|
||||
private void printScreen() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before printing screen.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setJobName("j3270 Screen Capture");
|
||||
job.setPrintable(terminalPanel);
|
||||
if (job.printDialog()) {
|
||||
try {
|
||||
job.print();
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Print error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showFileTransferDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
||||
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileTransfer == null) {
|
||||
fileTransfer = new FileTransfer(client, new FileTransfer.FileTransferCallback() {
|
||||
private ProgressMonitor monitor;
|
||||
|
||||
@Override
|
||||
public void onTransferStarted() {
|
||||
monitor = new ProgressMonitor(J3270App.this, "Starting IND$FILE transfer...",
|
||||
"Waiting for host...", 0, 100);
|
||||
monitor.setMillisToDecideToPopup(0);
|
||||
monitor.setMillisToPopup(0);
|
||||
|
||||
Timer cancelCheckTimer = new Timer(500, e -> {
|
||||
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
((Timer)e.getSource()).stop();
|
||||
}
|
||||
});
|
||||
cancelCheckTimer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferRunning() {
|
||||
if (monitor != null) {
|
||||
monitor.setNote("Transferring data...");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBytesTransferred(long bytes) {
|
||||
if (monitor != null) {
|
||||
monitor.setNote(bytes + " bytes transferred");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferComplete(String message) {
|
||||
if (monitor != null) monitor.close();
|
||||
monitor = null;
|
||||
JOptionPane.showMessageDialog(J3270App.this, message, "Transfer Complete", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferAborted(String error) {
|
||||
if (monitor != null) monitor.close();
|
||||
monitor = null;
|
||||
JOptionPane.showMessageDialog(J3270App.this, "Transfer Failed: " + error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FileTransferDialog dialog = new FileTransferDialog(this, fileTransfer);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
// ========== Connection management ==========
|
||||
|
||||
private void showConnectDialog() {
|
||||
ConnectDialog dialog = new ConnectDialog(this);
|
||||
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||
int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||
dialog.setInitialHost(initialHost);
|
||||
dialog.setInitialPort(initialPort);
|
||||
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
||||
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
||||
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed()) {
|
||||
ConnectionConfig config = dialog.getConnectionConfig();
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
connect(config);
|
||||
}
|
||||
}
|
||||
|
||||
private void showSettingsDialog() {
|
||||
haus.nightmare.j3270.ui.SettingsDialog dialog = new haus.nightmare.j3270.ui.SettingsDialog(this);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
public TerminalPanel getTerminalPanel() {
|
||||
return terminalPanel;
|
||||
}
|
||||
|
||||
void connect(ConnectionConfig config) {
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
||||
|
||||
disconnect();
|
||||
|
||||
if (config.getCertificateVerifier() == null) {
|
||||
config.setCertificateVerifier((chain, authType, exception) ->
|
||||
haus.nightmare.j3270.ui.UntrustedCertificateDialog.showPrompt(this, config.getHost(), config.getPort(), chain, exception)
|
||||
);
|
||||
}
|
||||
|
||||
client = new Telnet3270Client(config);
|
||||
client.addConnectionListener(this);
|
||||
client.addScreenUpdateListener(this);
|
||||
|
||||
terminalPanel.setClient(client);
|
||||
statusBar.setClient(client, terminalPanel);
|
||||
|
||||
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
||||
|
||||
terminalPanel.guardedPack();
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.connect();
|
||||
} catch (IOException e) {
|
||||
log.log(Level.WARNING, "Connection failed", e);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Connection failed: " + e.getMessage(),
|
||||
"Connection Error", JOptionPane.ERROR_MESSAGE);
|
||||
});
|
||||
}
|
||||
}, "Connect-Thread").start();
|
||||
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
if (client != null) {
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
client.disconnect();
|
||||
client = null;
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null, terminalPanel);
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustFontSize(int delta) {
|
||||
int current = terminalPanel.getFontSize();
|
||||
int newSize = Math.max(8, Math.min(72, current + delta));
|
||||
terminalPanel.setFontSize(newSize);
|
||||
terminalPanel.guardedPack();
|
||||
}
|
||||
|
||||
// ========== ConnectionListener ==========
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
log.info("Connection state: " + oldState + " -> " + newState);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusBar.updateStatus();
|
||||
terminalPanel.repaint();
|
||||
|
||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||
terminalPanel.guardedPack();
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionError(String message) {
|
||||
log.warning("Connection error: " + message);
|
||||
SwingUtilities.invokeLater(statusBar::updateStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTN3270ENegotiated(String deviceType, String deviceName) {
|
||||
log.info("TN3270E negotiated: type=" + deviceType + " name=" + deviceName);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (deviceName != null) {
|
||||
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
|
||||
}
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
// ========== ScreenUpdateListener ==========
|
||||
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
if (screenUpdatePending.compareAndSet(false, true)) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
screenUpdatePending.set(false);
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.onScreenUpdated();
|
||||
}
|
||||
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoundAlarm() {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
terminalPanel.guardedPack();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Help dialogs ==========
|
||||
|
||||
private void showKeyMappings() {
|
||||
String text = "Key Mappings:\n\n" +
|
||||
"Enter — Send Enter (AID)\n" +
|
||||
"F1-F12 — PF1-PF12\n" +
|
||||
"Shift+F1-F12 — PF13-PF24\n" +
|
||||
"Alt+1/2/3 — PA1/PA2/PA3\n" +
|
||||
"Tab — Tab to next field\n" +
|
||||
"Shift+Tab — Back-tab\n" +
|
||||
"Arrow keys — Cursor movement\n" +
|
||||
"Home — Cursor to home\n" +
|
||||
"End — Erase to end of field\n" +
|
||||
"Delete — Delete character\n" +
|
||||
"Backspace — Backspace\n" +
|
||||
"Insert — Toggle insert mode\n" +
|
||||
"Escape — Reset\n" +
|
||||
"PageUp/Down — PF7/PF8\n" +
|
||||
"Alt+C / Alt+K — Clear\n" +
|
||||
"Alt+E — Erase Input\n" +
|
||||
"Alt+A — Attention\n" +
|
||||
"Alt+S — System Request\n" +
|
||||
"Alt+Q — Cursor Select\n" +
|
||||
"Alt+L — Toggle Light Pen\n" +
|
||||
"Alt+T — File Transfer\n" +
|
||||
"Alt+=/-/0 — Font size +/-/reset\n" +
|
||||
"Cmd/Ctrl+F — Find on Screen\n" +
|
||||
"Cmd/Ctrl+G / F3— Find Next\n" +
|
||||
"Cmd/Ctrl+D — Disconnect\n" +
|
||||
"Cmd/Ctrl+Q — Quit";
|
||||
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
ThemeManager.styleTextArea(area);
|
||||
JScrollPane sp = new JScrollPane(area);
|
||||
ThemeManager.styleScrollPane(sp);
|
||||
sp.setPreferredSize(new Dimension(380, 430));
|
||||
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
private void showMnemonicHelp() {
|
||||
String text = "IBM Host On-Demand ECL Mnemonic Tokens:\n\n" +
|
||||
"[enter] — Send Enter key\n" +
|
||||
"[tab] — Tab to next field\n" +
|
||||
"[backtab] — Back-tab to previous field\n" +
|
||||
"[clear] — Clear screen\n" +
|
||||
"[reset] — Reset keyboard lock\n" +
|
||||
"[eraseeof] — Erase to End-of-Field\n" +
|
||||
"[eraseinput] — Erase all unprotected fields\n" +
|
||||
"[newline] — Advance to next line\n" +
|
||||
"[dup] — Insert Duplicate order (0x1C)\n" +
|
||||
"[fm] — Insert Field Mark order (0x1E)\n" +
|
||||
"[attn] — Send Attention signal\n" +
|
||||
"[sysreq] — System Request\n" +
|
||||
"[cursel] — Cursor Select\n" +
|
||||
"[pf1] - [pf24] — PF1 through PF24 keys\n" +
|
||||
"[pa1] - [pa3] — PA1 through PA3 keys\n" +
|
||||
"[up]/[down] — Cursor up / down\n" +
|
||||
"[left]/[right] — Cursor left / right\n" +
|
||||
"[home] — Cursor to home\n\n" +
|
||||
"Example Script:\n" +
|
||||
"LOGON APPLID(TSO)[enter]USER[tab]PASSWORD[enter][pf3]";
|
||||
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
ThemeManager.styleTextArea(area);
|
||||
JScrollPane sp = new JScrollPane(area);
|
||||
ThemeManager.styleScrollPane(sp);
|
||||
sp.setPreferredSize(new Dimension(440, 430));
|
||||
JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
private void showAbout() {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"j3270 — Java TN3270 Terminal Emulator\n\n" +
|
||||
"A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n" +
|
||||
"lib3270j v0.2.0\n\n" +
|
||||
"Features:\n" +
|
||||
"• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" +
|
||||
"• Models 2-5 & Dynamic Geometry\n" +
|
||||
"• IBM 3287/3286 Printer Session Emulation (SCS & LU3)\n" +
|
||||
"• IND$FILE CUT & DFT File Transfer + Host Catalog Query\n" +
|
||||
"• GDDM/GOCA Vector Graphics & Programmed Symbols\n" +
|
||||
"• 22+ Pluggable EBCDIC Codepages & DBCS Support\n" +
|
||||
"• ECL Presentation Space Search, Inspector & Scripting",
|
||||
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
// ========== Main ==========
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean debug = false;
|
||||
boolean cliTls = false;
|
||||
boolean cliNoVerifyCert = false;
|
||||
Boolean cliTn3270e = null;
|
||||
GraphicsMode cliGraphicsMode = null;
|
||||
String configFile = null;
|
||||
|
||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
String arg = args[i];
|
||||
if ("-d".equals(arg) || "--debug".equals(arg)) {
|
||||
debug = true;
|
||||
} else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
|
||||
cliTls = true;
|
||||
} else if ("--no-verify-cert".equals(arg) || "--insecure".equals(arg) || "-k".equals(arg)) {
|
||||
cliNoVerifyCert = true;
|
||||
} else if ("--no-tn3270e".equals(arg) || "--plain-tn3270".equals(arg) || "--plain".equals(arg) || "-P".equals(arg) || "-p".equals(arg) || "--non-e".equals(arg)) {
|
||||
cliTn3270e = false;
|
||||
} else if ("--tn3270e".equals(arg)) {
|
||||
cliTn3270e = true;
|
||||
} else if (arg.startsWith("--graphics=")) {
|
||||
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
|
||||
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
|
||||
} else if ("--no-graphics".equals(arg)) {
|
||||
cliGraphicsMode = GraphicsMode.NONE;
|
||||
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
|
||||
configFile = args[++i];
|
||||
} else if (arg.startsWith("-")) {
|
||||
System.err.println("Unknown option: " + arg);
|
||||
} else {
|
||||
remainingArgs.add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
Level logLevel = debug ? Level.FINE : Level.INFO;
|
||||
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
}
|
||||
|
||||
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
|
||||
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(Level.ALL);
|
||||
consoleHandler.setFormatter(new SimpleFormatter());
|
||||
consoleHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(consoleHandler);
|
||||
|
||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||
|
||||
try {
|
||||
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||
@Override
|
||||
public synchronized void publish(java.util.logging.LogRecord record) {
|
||||
super.publish(record);
|
||||
flush();
|
||||
}
|
||||
};
|
||||
fileHandler.setLevel(Level.ALL);
|
||||
fileHandler.setFormatter(new SimpleFormatter());
|
||||
fileHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(fileHandler);
|
||||
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
||||
} catch (Exception e) {
|
||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (configFile != null) {
|
||||
try {
|
||||
haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to load config file: " + configFile);
|
||||
System.err.println(e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception e) {
|
||||
log.fine("Could not set system look and feel");
|
||||
}
|
||||
ThemeManager.applyUIManagerDefaults(haus.nightmare.j3270.config.Settings.getJavaUiTheme());
|
||||
ThemeManager.setTheme(haus.nightmare.j3270.config.Settings.getJavaUiTheme());
|
||||
|
||||
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
||||
System.setProperty("apple.awt.application.name", "j3270");
|
||||
|
||||
final boolean finalTls = cliTls;
|
||||
final boolean finalNoVerify = cliNoVerifyCert;
|
||||
final Boolean finalTn3270e = cliTn3270e;
|
||||
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
J3270App app = new J3270App();
|
||||
app.setVisible(true);
|
||||
|
||||
if (!remainingArgs.isEmpty()) {
|
||||
String hostArg = remainingArgs.get(0);
|
||||
int port = finalTls ? 992 : 23;
|
||||
if (remainingArgs.size() >= 2) {
|
||||
try {
|
||||
port = Integer.parseInt(remainingArgs.get(1));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
if (remainingArgs.size() >= 3) {
|
||||
try {
|
||||
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
||||
model = TerminalModel.forModel(modelNum, true);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
|
||||
if (finalTls) {
|
||||
config.setUseTls(true);
|
||||
}
|
||||
if (finalNoVerify) {
|
||||
config.setTlsVerifyCert(false);
|
||||
}
|
||||
if (finalTn3270e != null) {
|
||||
config.setTn3270eEnabled(finalTn3270e);
|
||||
}
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
app.connect(config);
|
||||
} else {
|
||||
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
|
||||
.getStartupBehavior();
|
||||
switch (behavior) {
|
||||
case SHOW_CONNECT:
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
break;
|
||||
case AUTO_CONNECT:
|
||||
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
|
||||
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert();
|
||||
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e();
|
||||
if (host != null && !host.isEmpty()) {
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||
config.setTlsVerifyCert(verify);
|
||||
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
app.connect(config);
|
||||
} else {
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
}
|
||||
break;
|
||||
case DO_NOTHING:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+106
-4
@@ -1,5 +1,6 @@
|
||||
package org.pubvm.j3270.config;
|
||||
package haus.nightmare.j3270.config;
|
||||
|
||||
import haus.nightmare.j3270.ui.UITheme;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.awt.Color;
|
||||
import java.io.*;
|
||||
@@ -17,6 +18,15 @@ public class Settings {
|
||||
AUTO_CONNECT
|
||||
}
|
||||
|
||||
public static UITheme getJavaUiTheme() {
|
||||
String themeStr = prefs.get("javaUiTheme", UITheme.DARK.name());
|
||||
return UITheme.fromString(themeStr);
|
||||
}
|
||||
|
||||
public static void setJavaUiTheme(UITheme theme) {
|
||||
prefs.put("javaUiTheme", (theme != null ? theme : UITheme.DARK).name());
|
||||
}
|
||||
|
||||
public static String getFontFamily() {
|
||||
return prefs.get("fontFamily", "Monospaced");
|
||||
}
|
||||
@@ -62,6 +72,47 @@ public class Settings {
|
||||
prefs.putInt("autoConnectPort", port);
|
||||
}
|
||||
|
||||
public static boolean getAutoConnectTls() {
|
||||
return prefs.getBoolean("autoConnectTls", false);
|
||||
}
|
||||
|
||||
public static void setAutoConnectTls(boolean tls) {
|
||||
prefs.putBoolean("autoConnectTls", tls);
|
||||
}
|
||||
|
||||
public static boolean getAutoConnectVerifyCert() {
|
||||
return prefs.getBoolean("autoConnectVerifyCert", true);
|
||||
}
|
||||
|
||||
public static void setAutoConnectVerifyCert(boolean verify) {
|
||||
prefs.putBoolean("autoConnectVerifyCert", verify);
|
||||
}
|
||||
|
||||
public static boolean getAutoConnectTn3270e() {
|
||||
return prefs.getBoolean("autoConnectTn3270e", true);
|
||||
}
|
||||
|
||||
public static void setAutoConnectTn3270e(boolean tn3270e) {
|
||||
prefs.putBoolean("autoConnectTn3270e", tn3270e);
|
||||
}
|
||||
|
||||
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
|
||||
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
|
||||
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||
}
|
||||
|
||||
public static void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||
prefs.put("graphicsMode", (mode != null ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH).name());
|
||||
}
|
||||
|
||||
public static String getCodePage() {
|
||||
return prefs.get("codePage", "037");
|
||||
}
|
||||
|
||||
public static void setCodePage(String codePage) {
|
||||
prefs.put("codePage", (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037");
|
||||
}
|
||||
|
||||
public static Color getColorOverride(int index, Color defaultColor) {
|
||||
String hex = prefs.get("color_" + index, null);
|
||||
try {
|
||||
@@ -188,21 +239,54 @@ public class Settings {
|
||||
switch (key) {
|
||||
case "fontFamily": setFontFamily(value); break;
|
||||
case "fontSize": setFontSize(Integer.parseInt(value)); break;
|
||||
case "javaUiTheme":
|
||||
case "theme":
|
||||
case "uiTheme":
|
||||
setJavaUiTheme(UITheme.fromString(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown appearance key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "behavior":
|
||||
case "connection":
|
||||
switch (key) {
|
||||
case "startupBehavior":
|
||||
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "autoConnectHost": setAutoConnectHost(value); break;
|
||||
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
|
||||
case "autoConnectHost":
|
||||
case "host":
|
||||
setAutoConnectHost(value);
|
||||
break;
|
||||
case "autoConnectPort":
|
||||
case "port":
|
||||
setAutoConnectPort(Integer.parseInt(value));
|
||||
break;
|
||||
case "autoConnectTls":
|
||||
case "tls":
|
||||
case "ssl":
|
||||
case "useTls":
|
||||
setAutoConnectTls(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "autoConnectVerifyCert":
|
||||
case "verifyCert":
|
||||
case "tlsVerifyCert":
|
||||
setAutoConnectVerifyCert(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "autoConnectTn3270e":
|
||||
case "tn3270e":
|
||||
case "enableTn3270e":
|
||||
setAutoConnectTn3270e(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "codePage":
|
||||
case "codepage":
|
||||
case "charset":
|
||||
setCodePage(value);
|
||||
break;
|
||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||
default:
|
||||
log.warning("Unknown behavior key: " + key);
|
||||
log.warning("Unknown behavior/connection key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -222,6 +306,14 @@ public class Settings {
|
||||
setKeyBinding(key, value);
|
||||
break;
|
||||
|
||||
case "graphics":
|
||||
if ("graphicsMode".equalsIgnoreCase(key) || "mode".equalsIgnoreCase(key)) {
|
||||
setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(value));
|
||||
} else {
|
||||
log.warning("Unknown graphics key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Allow bare keys outside any section — treat as raw prefs
|
||||
log.fine("Setting raw preference: " + key + " = " + value);
|
||||
@@ -245,6 +337,7 @@ public class Settings {
|
||||
|
||||
// [appearance]
|
||||
w.println("[appearance]");
|
||||
w.println("javaUiTheme = " + getJavaUiTheme().name());
|
||||
w.println("fontFamily = " + getFontFamily());
|
||||
w.println("fontSize = " + getFontSize());
|
||||
w.println();
|
||||
@@ -256,10 +349,19 @@ public class Settings {
|
||||
if (acHost != null && !acHost.isEmpty()) {
|
||||
w.println("autoConnectHost = " + acHost);
|
||||
w.println("autoConnectPort = " + getAutoConnectPort());
|
||||
w.println("autoConnectTls = " + getAutoConnectTls());
|
||||
w.println("autoConnectVerifyCert = " + getAutoConnectVerifyCert());
|
||||
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
|
||||
}
|
||||
w.println("codePage = " + getCodePage());
|
||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||
w.println();
|
||||
|
||||
// [graphics]
|
||||
w.println("[graphics]");
|
||||
w.println("graphicsMode = " + getGraphicsMode().name());
|
||||
w.println();
|
||||
|
||||
// [colors]
|
||||
w.println("[colors]");
|
||||
// Host colors 0-15
|
||||
+25
-6
@@ -1,10 +1,10 @@
|
||||
package org.pubvm.j3270.ft;
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import org.lib3270j.ft.FTConfig;
|
||||
import org.lib3270j.ft.FTConstants.FTState;
|
||||
import org.lib3270j.ft.FTCut;
|
||||
import org.lib3270j.ft.FTDft;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConstants.FTState;
|
||||
import haus.nightmare.lib3270j.ft.FTCut;
|
||||
import haus.nightmare.lib3270j.ft.FTDft;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -53,6 +53,10 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public Telnet3270Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new file transfer with the given configuration.
|
||||
* @return null if started successfully, error message otherwise.
|
||||
@@ -121,6 +125,21 @@ public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
||||
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
|
||||
log.info("User cancelled transfer");
|
||||
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) {
|
||||
log.info("Forcing cancel from state " + state);
|
||||
completeTransfer("Transfer cancelled.");
|
||||
+49
-20
@@ -1,6 +1,9 @@
|
||||
package org.pubvm.j3270.ft;
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import org.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.j3270.ui.ThemeManager;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConstants;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
@@ -18,6 +21,8 @@ public class FileTransferDialog extends JDialog {
|
||||
private JTextField localFileField;
|
||||
private JButton browseLocalButton;
|
||||
private JTextField hostFileField;
|
||||
private JButton browseHostButton;
|
||||
private JComboBox<Integer> mtuCombo;
|
||||
|
||||
// Options
|
||||
private JRadioButton asciiRadio;
|
||||
@@ -40,7 +45,7 @@ public class FileTransferDialog extends JDialog {
|
||||
private JButton cancelButton;
|
||||
|
||||
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
|
||||
super(owner, "File Transfer (IND$FILE)", false); // non-modal to see progress
|
||||
super(owner, "File Transfer (IND$FILE)", false);
|
||||
this.owner = owner;
|
||||
this.coordinator = coordinator;
|
||||
|
||||
@@ -76,7 +81,6 @@ public class FileTransferDialog extends JDialog {
|
||||
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
|
||||
dirPanel.add(sendRadio);
|
||||
|
||||
// Disable append/overwrite when sending
|
||||
sendRadio.addActionListener(e -> updateOptionStates());
|
||||
receiveRadio.addActionListener(e -> updateOptionStates());
|
||||
|
||||
@@ -85,6 +89,7 @@ public class FileTransferDialog extends JDialog {
|
||||
// Local File
|
||||
localFileField = new JTextField(20);
|
||||
browseLocalButton = new JButton("Browse...");
|
||||
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
|
||||
browseLocalButton.addActionListener(e -> browseLocalFile());
|
||||
JPanel localPanel = new JPanel(new BorderLayout(5, 0));
|
||||
localPanel.setOpaque(false);
|
||||
@@ -94,7 +99,14 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
// Host File
|
||||
hostFileField = new JTextField(20);
|
||||
formPanel.add(createRow("Host File:", hostFileField));
|
||||
browseHostButton = new JButton("Browse Host...");
|
||||
ThemeManager.styleButton(browseHostButton, ThemeManager.ButtonVariant.DEFAULT);
|
||||
browseHostButton.addActionListener(e -> browseHostDirectory());
|
||||
JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
|
||||
hostPanel.setOpaque(false);
|
||||
hostPanel.add(hostFileField, BorderLayout.CENTER);
|
||||
hostPanel.add(browseHostButton, BorderLayout.EAST);
|
||||
formPanel.add(createRow("Host File:", hostPanel));
|
||||
|
||||
// Mode
|
||||
asciiRadio = new JRadioButton("ASCII");
|
||||
@@ -114,6 +126,11 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
formPanel.add(createRow("Transfer Mode:", modePanel));
|
||||
|
||||
// MTU Buffer Size
|
||||
mtuCombo = new JComboBox<>(new Integer[]{2048, 4096, 8192, 16384, 32768});
|
||||
mtuCombo.setSelectedItem(FTConstants.DFT_BUF);
|
||||
formPanel.add(createRow("DFT Buffer Size:", mtuCombo));
|
||||
|
||||
// Options (checkboxes)
|
||||
crCheck = new JCheckBox("Add/Remove CR");
|
||||
crCheck.setSelected(true);
|
||||
@@ -140,10 +157,7 @@ public class FileTransferDialog extends JDialog {
|
||||
// Host-specific options panel (TSO dataset allocation)
|
||||
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
|
||||
hostOptsPanel.setOpaque(false);
|
||||
hostOptsPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
"TSO Allocation Options (Send Only)"));
|
||||
((javax.swing.border.TitledBorder)hostOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
|
||||
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
|
||||
|
||||
hostOptsPanel.add(new JLabel("RECFM:"));
|
||||
recfmField = new JTextField(5);
|
||||
@@ -167,10 +181,7 @@ public class FileTransferDialog extends JDialog {
|
||||
// CMS options
|
||||
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
|
||||
vmOptsPanel.setOpaque(false);
|
||||
vmOptsPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
"CMS Options"));
|
||||
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
|
||||
vmOptsPanel.setBorder(ThemeManager.createTitledBorder("CMS Options"));
|
||||
|
||||
optionsField = new JTextField(20);
|
||||
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
|
||||
@@ -186,9 +197,11 @@ public class FileTransferDialog extends JDialog {
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
transferButton = new JButton("Start Transfer");
|
||||
ThemeManager.styleButton(transferButton, ThemeManager.ButtonVariant.PRIMARY);
|
||||
transferButton.addActionListener(e -> startTransfer());
|
||||
|
||||
cancelButton = new JButton("Close");
|
||||
ThemeManager.styleButton(cancelButton, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelButton.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(cancelButton);
|
||||
@@ -198,10 +211,7 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
setContentPane(mainPanel);
|
||||
|
||||
// Theme styling for components
|
||||
applyTheme(mainPanel);
|
||||
|
||||
// Initialize state
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
updateOptionStates();
|
||||
}
|
||||
|
||||
@@ -236,14 +246,29 @@ public class FileTransferDialog extends JDialog {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
|
||||
|
||||
// Auto-fill host file if empty
|
||||
if (hostFileField.getText().trim().isEmpty()) {
|
||||
hostFileField.setText(chooser.getSelectedFile().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void browseHostDirectory() {
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
String initialQuery = hostFileField.getText().trim();
|
||||
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
||||
hostFileField.setText(dialog.getSelectedHostFile());
|
||||
if (localFileField.getText().trim().isEmpty()) {
|
||||
String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", "");
|
||||
int lastDot = cleanName.lastIndexOf('.');
|
||||
if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1);
|
||||
localFileField.setText(cleanName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startTransfer() {
|
||||
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
|
||||
@@ -262,6 +287,11 @@ public class FileTransferDialog extends JDialog {
|
||||
config.setAppend(appendCheck.isSelected());
|
||||
config.setOverwrite(overwriteCheck.isSelected());
|
||||
|
||||
Integer mtu = (Integer) mtuCombo.getSelectedItem();
|
||||
if (mtu != null) {
|
||||
config.setDftBufferSize(mtu);
|
||||
}
|
||||
|
||||
config.setRecfm(recfmField.getText().trim());
|
||||
config.setLrecl(lreclField.getText().trim());
|
||||
config.setBlksize(blksizeField.getText().trim());
|
||||
@@ -272,7 +302,6 @@ public class FileTransferDialog extends JDialog {
|
||||
if (error != null) {
|
||||
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
||||
} else {
|
||||
// Success, close dialog (progress will be shown separately)
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Connection dialog for entering host, port, model, and LU name.
|
||||
*/
|
||||
public class ConnectDialog extends JDialog {
|
||||
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JComboBox<TerminalModel> modelCombo;
|
||||
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
||||
private JComboBox<String> codePageCombo;
|
||||
private JTextField luField;
|
||||
private JCheckBox tlsCheckBox;
|
||||
private JCheckBox verifyCertCheckBox;
|
||||
private JCheckBox tn3270eCheckBox;
|
||||
private boolean confirmed;
|
||||
private ConnectionConfig result;
|
||||
|
||||
public ConnectDialog(Frame parent) {
|
||||
super(parent, "Connect to Host", true);
|
||||
buildUI();
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
setResizable(false);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new GridBagLayout());
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(5, 6, 5, 6);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
|
||||
// Host
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel hostLabel = new JLabel("Host:");
|
||||
hostLabel.setFont(labelFont);
|
||||
mainPanel.add(hostLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
hostField = createField(20);
|
||||
mainPanel.add(hostField, gbc);
|
||||
|
||||
// Port
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.weightx = 0;
|
||||
JLabel portLabel = new JLabel("Port:");
|
||||
portLabel.setFont(labelFont);
|
||||
mainPanel.add(portLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
portField = createField(6);
|
||||
portField.setText("23");
|
||||
mainPanel.add(portField, gbc);
|
||||
|
||||
// Model
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.weightx = 0;
|
||||
JLabel modelLabel = new JLabel("Model:");
|
||||
modelLabel.setFont(labelFont);
|
||||
mainPanel.add(modelLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
modelCombo = new JComboBox<>(TerminalModel.values());
|
||||
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
|
||||
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
ThemeManager.styleComboBox(modelCombo);
|
||||
mainPanel.add(modelCombo, gbc);
|
||||
|
||||
// LU Name
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 3;
|
||||
gbc.weightx = 0;
|
||||
JLabel luLabel = new JLabel("LU Name:");
|
||||
luLabel.setFont(labelFont);
|
||||
mainPanel.add(luLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
luField = createField(12);
|
||||
mainPanel.add(luField, gbc);
|
||||
|
||||
// Graphics Mode
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.weightx = 0;
|
||||
JLabel graphicsLabel = new JLabel("Graphics:");
|
||||
graphicsLabel.setFont(labelFont);
|
||||
mainPanel.add(graphicsLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
ThemeManager.styleComboBox(graphicsCombo);
|
||||
mainPanel.add(graphicsCombo, gbc);
|
||||
|
||||
// Code Page
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 5;
|
||||
gbc.weightx = 0;
|
||||
JLabel cpLabel = new JLabel("Code Page:");
|
||||
cpLabel.setFont(labelFont);
|
||||
mainPanel.add(cpLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
String[] commonCodePages = {
|
||||
"037 - US / Canada / Brazil",
|
||||
"1047 - IBM Open Systems / z/OS Unix",
|
||||
"500 - International Latin-1",
|
||||
"273 - Germany / Austria",
|
||||
"277 - Denmark / Norway",
|
||||
"278 - Sweden / Finland",
|
||||
"280 - Italy",
|
||||
"284 - Spain / Latin America",
|
||||
"285 - United Kingdom",
|
||||
"297 - France",
|
||||
"870 - Eastern Europe / Latin-2",
|
||||
"871 - Iceland",
|
||||
"875 - Greece (Greek)",
|
||||
"1026 - Turkey (Turkish Latin-5)",
|
||||
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
|
||||
"905 - Turkey (Turkish Latin-3)",
|
||||
"420 - Arabic Bilingual",
|
||||
"424 - Hebrew (with Lowercase)",
|
||||
"803 - Hebrew Old / Standard",
|
||||
"838 - Thai",
|
||||
"1160 - Thai (Euro \u20AC)",
|
||||
"1025 - Cyrillic Multilingual",
|
||||
"1123 - Cyrillic Ukraine",
|
||||
"1154 - Cyrillic Multilingual (Euro \u20AC)",
|
||||
"880 - Cyrillic Russian",
|
||||
"1140 - US / Canada (Euro \u20AC)",
|
||||
"1141 - Germany / Austria (Euro \u20AC)",
|
||||
"1148 - International (Euro \u20AC)",
|
||||
"930 - Japanese Katakana Mixed DBCS",
|
||||
"939 - Japanese Latin Mixed DBCS",
|
||||
"935 - Simplified Chinese Mixed DBCS",
|
||||
"1388 - Simplified Chinese Extended Mixed DBCS",
|
||||
"937 - Traditional Chinese Mixed DBCS",
|
||||
"1371 - Traditional Chinese Extended Mixed DBCS",
|
||||
"933 - Korean Mixed DBCS"
|
||||
};
|
||||
codePageCombo = new JComboBox<>(commonCodePages);
|
||||
codePageCombo.setEditable(true);
|
||||
String currentCp = haus.nightmare.j3270.config.Settings.getCodePage();
|
||||
for (String item : commonCodePages) {
|
||||
if (item.startsWith(currentCp + " ") || item.equals(currentCp)) {
|
||||
codePageCombo.setSelectedItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
ThemeManager.styleComboBox(codePageCombo);
|
||||
mainPanel.add(codePageCombo, gbc);
|
||||
|
||||
// TLS / SSL Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 6;
|
||||
gbc.weightx = 1.0;
|
||||
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
|
||||
ThemeManager.styleCheckBox(tlsCheckBox);
|
||||
tlsCheckBox.setFont(labelFont);
|
||||
tlsCheckBox.addActionListener(e -> {
|
||||
boolean isTls = tlsCheckBox.isSelected();
|
||||
verifyCertCheckBox.setEnabled(isTls);
|
||||
String currentPort = portField.getText().trim();
|
||||
if (isTls && "23".equals(currentPort)) {
|
||||
portField.setText("992");
|
||||
} else if (!isTls && "992".equals(currentPort)) {
|
||||
portField.setText("23");
|
||||
}
|
||||
});
|
||||
mainPanel.add(tlsCheckBox, gbc);
|
||||
|
||||
// Verify Certificate Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 7;
|
||||
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
|
||||
ThemeManager.styleCheckBox(verifyCertCheckBox);
|
||||
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
verifyCertCheckBox.setSelected(true);
|
||||
verifyCertCheckBox.setEnabled(false);
|
||||
mainPanel.add(verifyCertCheckBox, gbc);
|
||||
|
||||
// TN3270E Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 8;
|
||||
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
|
||||
ThemeManager.styleCheckBox(tn3270eCheckBox);
|
||||
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
||||
mainPanel.add(tn3270eCheckBox, gbc);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||
|
||||
JButton connectBtn = new JButton("Connect");
|
||||
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
connectBtn.setFont(labelFont);
|
||||
connectBtn.addActionListener(e -> onConnect());
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel");
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.setFont(labelFont);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
confirmed = false;
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(connectBtn);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 9;
|
||||
gbc.gridwidth = 2;
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// Enter key triggers connect
|
||||
getRootPane().setDefaultButton(connectBtn);
|
||||
}
|
||||
|
||||
private JTextField createField(int cols) {
|
||||
JTextField field = new JTextField(cols);
|
||||
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
|
||||
ThemeManager.styleTextField(field);
|
||||
return field;
|
||||
}
|
||||
|
||||
private void onConnect() {
|
||||
String host = hostField.getText().trim();
|
||||
if (host.isEmpty()) {
|
||||
hostField.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
int port;
|
||||
try {
|
||||
port = Integer.parseInt(portField.getText().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
portField.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem());
|
||||
String lu = luField.getText().trim();
|
||||
if (!lu.isEmpty()) {
|
||||
result.setLuName(lu);
|
||||
}
|
||||
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
|
||||
|
||||
Object cpSelection = codePageCombo.getSelectedItem();
|
||||
if (cpSelection != null) {
|
||||
String cpStr = cpSelection.toString().trim();
|
||||
int dash = cpStr.indexOf(" -");
|
||||
if (dash > 0) {
|
||||
cpStr = cpStr.substring(0, dash).trim();
|
||||
}
|
||||
result.setCodePage(cpStr);
|
||||
haus.nightmare.j3270.config.Settings.setCodePage(cpStr);
|
||||
}
|
||||
|
||||
result.setUseTls(tlsCheckBox.isSelected());
|
||||
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
||||
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
public ConnectionConfig getConnectionConfig() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Set initial values. */
|
||||
public void setInitialHost(String host) {
|
||||
hostField.setText(host);
|
||||
}
|
||||
|
||||
public void setInitialPort(int port) {
|
||||
portField.setText(String.valueOf(port));
|
||||
}
|
||||
|
||||
public void setInitialTls(boolean tls) {
|
||||
tlsCheckBox.setSelected(tls);
|
||||
verifyCertCheckBox.setEnabled(tls);
|
||||
}
|
||||
|
||||
public void setInitialVerifyCert(boolean verify) {
|
||||
verifyCertCheckBox.setSelected(verify);
|
||||
}
|
||||
|
||||
public void setInitialTn3270e(boolean tn3270e) {
|
||||
tn3270eCheckBox.setSelected(tn3270e);
|
||||
}
|
||||
|
||||
public void setInitialCodePage(String cp) {
|
||||
if (cp != null && codePageCombo != null) {
|
||||
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
|
||||
String item = codePageCombo.getItemAt(i);
|
||||
if (item.startsWith(cp + " ") || item.equals(cp)) {
|
||||
codePageCombo.setSelectedIndex(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
codePageCombo.setSelectedItem(cp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Live Field Inspector dialog displaying all fields in the 3270 Presentation Space (ECLFieldList).
|
||||
*/
|
||||
public class FieldInspectorDialog extends JDialog implements ScreenUpdateListener {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private DefaultTableModel tableModel;
|
||||
private JTable table;
|
||||
private JLabel countLabel;
|
||||
|
||||
public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "3270 Presentation Space Field Inspector", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
|
||||
buildUI();
|
||||
setSize(850, 420);
|
||||
setLocationRelativeTo(parent);
|
||||
|
||||
if (client != null) {
|
||||
client.addScreenUpdateListener(this);
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
if (client != null) {
|
||||
// Screen update listener removed on close
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
|
||||
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// Top info bar
|
||||
JPanel topPanel = new JPanel(new BorderLayout());
|
||||
topPanel.setOpaque(false);
|
||||
countLabel = new JLabel("0 fields detected on screen");
|
||||
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||
topPanel.add(countLabel, BorderLayout.WEST);
|
||||
|
||||
JButton refreshBtn = new JButton("Refresh");
|
||||
ThemeManager.styleButton(refreshBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
refreshBtn.addActionListener(e -> refreshFields());
|
||||
topPanel.add(refreshBtn, BorderLayout.EAST);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Table
|
||||
String[] columns = {
|
||||
"#", "Pos", "Start", "End", "Len", "Prot", "MDT", "Num", "Hi-Int", "Hidden", "Pen-Sel", "Content Text"
|
||||
};
|
||||
|
||||
tableModel = new DefaultTableModel(columns, 0) {
|
||||
@Override
|
||||
public boolean isCellEditable(int row, int column) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
table = new JTable(tableModel);
|
||||
ThemeManager.styleTable(table);
|
||||
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(20);
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
|
||||
|
||||
// Column widths
|
||||
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
|
||||
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
|
||||
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
|
||||
}
|
||||
|
||||
// Center align boolean columns
|
||||
DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
|
||||
centerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
for (int i = 5; i <= 10; i++) {
|
||||
table.getColumnModel().getColumn(i).setCellRenderer(centerRenderer);
|
||||
}
|
||||
|
||||
table.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting() && table.getSelectedRow() >= 0) {
|
||||
selectFieldOnTerminal(table.getSelectedRow());
|
||||
}
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom panel
|
||||
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
JButton jumpBtn = new JButton("Jump to Selected Field");
|
||||
ThemeManager.styleButton(jumpBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
jumpBtn.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row >= 0) {
|
||||
selectFieldOnTerminal(row);
|
||||
}
|
||||
});
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
bottomPanel.add(jumpBtn);
|
||||
bottomPanel.add(closeBtn);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
public synchronized void refreshFields() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
tableModel.setRowCount(0);
|
||||
countLabel.setText("Not connected or screen unformatted");
|
||||
return;
|
||||
}
|
||||
|
||||
ECLFieldList fieldList = client.getFieldList();
|
||||
List<ECLField> fields = fieldList.getFields();
|
||||
|
||||
int selectedRow = table.getSelectedRow();
|
||||
tableModel.setRowCount(0);
|
||||
|
||||
int cols = client.getScreenBuffer().getDisplayCols();
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
ECLField f = fields.get(i);
|
||||
int startRow = f.getStartRow() + 1;
|
||||
int startCol = f.getStartCol() + 1;
|
||||
int endRow = f.getEndRow() + 1;
|
||||
int endCol = f.getEndCol() + 1;
|
||||
|
||||
String text = f.getText();
|
||||
if (f.isHidden()) {
|
||||
text = "••••••••";
|
||||
}
|
||||
|
||||
tableModel.addRow(new Object[]{
|
||||
(i + 1),
|
||||
f.getStart(),
|
||||
String.format("%02d/%02d", startRow, startCol),
|
||||
String.format("%02d/%02d", endRow, endCol),
|
||||
f.getLength(),
|
||||
f.isProtected() ? "YES" : "-",
|
||||
f.isModified() ? "YES" : "-",
|
||||
f.isNumeric() ? "YES" : "-",
|
||||
f.isHighIntensity() ? "YES" : "-",
|
||||
f.isHidden() ? "YES" : "-",
|
||||
f.isPenSelectable() ? "YES" : "-",
|
||||
text
|
||||
});
|
||||
}
|
||||
|
||||
countLabel.setText(fields.size() + " fields detected on screen (" + client.getScreenBuffer().getDisplayRows() + "x" + cols + ")");
|
||||
if (selectedRow >= 0 && selectedRow < tableModel.getRowCount()) {
|
||||
table.setRowSelectionInterval(selectedRow, selectedRow);
|
||||
}
|
||||
}
|
||||
|
||||
private void selectFieldOnTerminal(int rowIndex) {
|
||||
if (client == null || rowIndex < 0) return;
|
||||
List<ECLField> fields = client.getFieldList().getFields();
|
||||
if (rowIndex < fields.size()) {
|
||||
ECLField f = fields.get(rowIndex);
|
||||
client.getScreenBuffer().setCursorAddress(f.getDataStart());
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.setSelectionRange(f.getDataStart(), f.getEnd());
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== ScreenUpdateListener ==========
|
||||
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
SwingUtilities.invokeLater(this::refreshFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
SwingUtilities.invokeLater(this::refreshFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoundAlarm() {}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
/**
|
||||
* Find dialog for searching text in the 3270 Presentation Space using ECLPS.
|
||||
*/
|
||||
public class FindDialog extends JDialog {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private JTextField searchField;
|
||||
private JCheckBox matchCaseCheck;
|
||||
private JRadioButton forwardRadio;
|
||||
private JRadioButton backwardRadio;
|
||||
private JLabel statusLabel;
|
||||
|
||||
private static String lastSearchText = "";
|
||||
private static boolean lastMatchCase = false;
|
||||
private static boolean lastForward = true;
|
||||
|
||||
public FindDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "Find on Screen", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
|
||||
buildUI();
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
setResizable(false);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
// Form
|
||||
JPanel formPanel = new JPanel(new GridBagLayout());
|
||||
formPanel.setOpaque(false);
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(4, 4, 4, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
|
||||
// Search text
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel findLabel = new JLabel("Find what:");
|
||||
findLabel.setFont(labelFont);
|
||||
formPanel.add(findLabel, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
searchField = new JTextField(20);
|
||||
searchField.setText(lastSearchText);
|
||||
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
ThemeManager.styleTextField(searchField);
|
||||
formPanel.add(searchField, gbc);
|
||||
|
||||
// Options
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 1;
|
||||
matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
|
||||
ThemeManager.styleCheckBox(matchCaseCheck);
|
||||
matchCaseCheck.setFont(labelFont);
|
||||
formPanel.add(matchCaseCheck, gbc);
|
||||
|
||||
// Direction
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 2;
|
||||
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||
dirPanel.setOpaque(false);
|
||||
JLabel dirLabel = new JLabel("Direction: ");
|
||||
dirLabel.setFont(labelFont);
|
||||
forwardRadio = new JRadioButton("Down", lastForward);
|
||||
backwardRadio = new JRadioButton("Up", !lastForward);
|
||||
ThemeManager.styleRadioButton(forwardRadio);
|
||||
ThemeManager.styleRadioButton(backwardRadio);
|
||||
|
||||
ButtonGroup bg = new ButtonGroup();
|
||||
bg.add(forwardRadio);
|
||||
bg.add(backwardRadio);
|
||||
dirPanel.add(dirLabel);
|
||||
dirPanel.add(forwardRadio);
|
||||
dirPanel.add(Box.createHorizontalStrut(10));
|
||||
dirPanel.add(backwardRadio);
|
||||
formPanel.add(dirPanel, gbc);
|
||||
|
||||
// Status
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 3;
|
||||
gbc.gridwidth = 2;
|
||||
statusLabel = new JLabel(" ");
|
||||
statusLabel.setForeground(ThemeManager.getOiaFgAlert());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
formPanel.add(statusLabel, gbc);
|
||||
|
||||
mainPanel.add(formPanel, BorderLayout.CENTER);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton findNextBtn = new JButton("Find Next");
|
||||
ThemeManager.styleButton(findNextBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
findNextBtn.setFont(labelFont);
|
||||
findNextBtn.addActionListener(e -> findNext());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(findNextBtn);
|
||||
buttonPanel.add(closeBtn);
|
||||
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
getRootPane().setDefaultButton(findNextBtn);
|
||||
|
||||
searchField.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
|
||||
findNext();
|
||||
} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void findNext() {
|
||||
find(forwardRadio.isSelected());
|
||||
}
|
||||
|
||||
public void findPrevious() {
|
||||
find(!forwardRadio.isSelected());
|
||||
}
|
||||
|
||||
private void find(boolean forward) {
|
||||
String target = searchField.getText();
|
||||
if (target == null || target.isEmpty()) {
|
||||
statusLabel.setText("Please enter search text.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
statusLabel.setText("Terminal is not connected.");
|
||||
return;
|
||||
}
|
||||
|
||||
lastSearchText = target;
|
||||
lastMatchCase = matchCaseCheck.isSelected();
|
||||
lastForward = forwardRadio.isSelected();
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int cols = sb.getDisplayCols();
|
||||
int curRow = curAddr / cols;
|
||||
int curCol = curAddr % cols;
|
||||
|
||||
// Offset search start position past current character so repeat search moves to next occurrence
|
||||
int startRow = curRow;
|
||||
int startCol = curCol;
|
||||
if (forward) {
|
||||
startCol = (curCol + 1) % cols;
|
||||
if (startCol == 0) startRow = (curRow + 1) % sb.getDisplayRows();
|
||||
} else {
|
||||
startCol = (curCol - 1 + cols) % cols;
|
||||
if (startCol == cols - 1) startRow = (curRow - 1 + sb.getDisplayRows()) % sb.getDisplayRows();
|
||||
}
|
||||
|
||||
int dir = forward ? ECLConstants.SEARCH_FORWARD : ECLConstants.SEARCH_BACKWARD;
|
||||
int foundPos = client.getPS().searchString(target, startRow, startCol, dir, !matchCaseCheck.isSelected());
|
||||
|
||||
if (foundPos >= 0) {
|
||||
statusLabel.setText("Found at position " + (foundPos + 1) + " (" + (foundPos / cols + 1) + "/" + (foundPos % cols + 1) + ")");
|
||||
statusLabel.setForeground(new Color(100, 255, 100));
|
||||
sb.setCursorAddress(foundPos);
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.setSearchHighlight(foundPos, target.length());
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
} else {
|
||||
statusLabel.setText("Text not found: \"" + target + "\"");
|
||||
statusLabel.setForeground(new Color(255, 120, 120));
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.clearSearchHighlight();
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLastSearchText() {
|
||||
return lastSearchText;
|
||||
}
|
||||
|
||||
public static boolean getLastMatchCase() {
|
||||
return lastMatchCase;
|
||||
}
|
||||
|
||||
public static boolean getLastForward() {
|
||||
return lastForward;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
|
||||
*/
|
||||
public class HostDirectoryDialog extends JDialog {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final FTConfig.HostType initialHostType;
|
||||
|
||||
private JComboBox<FTConfig.HostType> hostTypeCombo;
|
||||
private JTextField queryField;
|
||||
private JTable table;
|
||||
private DefaultTableModel tableModel;
|
||||
private JLabel statusLabel;
|
||||
|
||||
private String selectedHostFile = null;
|
||||
private boolean confirmed = false;
|
||||
|
||||
private final List<HostDirectoryEntry> currentEntries = new ArrayList<>();
|
||||
|
||||
public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) {
|
||||
super(parent, "Host Catalog & Directory Browser", true);
|
||||
this.client = client;
|
||||
this.initialHostType = hostType;
|
||||
|
||||
buildUI(initialQuery);
|
||||
setSize(700, 420);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void buildUI(String initialQuery) {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
|
||||
|
||||
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
|
||||
// Top Query bar
|
||||
JPanel topPanel = new JPanel(new GridBagLayout());
|
||||
topPanel.setOpaque(false);
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(3, 4, 3, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
JLabel envLabel = new JLabel("System:");
|
||||
envLabel.setFont(font);
|
||||
topPanel.add(envLabel, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
hostTypeCombo = new JComboBox<>(new FTConfig.HostType[]{FTConfig.HostType.TSO, FTConfig.HostType.CMS});
|
||||
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
|
||||
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
|
||||
ThemeManager.styleComboBox(hostTypeCombo);
|
||||
topPanel.add(hostTypeCombo, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
JLabel queryLabel = new JLabel("Query Pattern / Text:");
|
||||
queryLabel.setFont(font);
|
||||
topPanel.add(queryLabel, gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.weightx = 1.0;
|
||||
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
|
||||
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
ThemeManager.styleTextField(queryField);
|
||||
topPanel.add(queryField, gbc);
|
||||
|
||||
gbc.gridx = 4;
|
||||
gbc.weightx = 0;
|
||||
JButton parseBtn = new JButton("Query / Parse");
|
||||
ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
parseBtn.setFont(font);
|
||||
parseBtn.addActionListener(e -> runQuery());
|
||||
topPanel.add(parseBtn, gbc);
|
||||
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Table
|
||||
tableModel = new DefaultTableModel();
|
||||
table = new JTable(tableModel);
|
||||
ThemeManager.styleTable(table);
|
||||
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(20);
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
|
||||
|
||||
table.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2 && table.getSelectedRow() >= 0) {
|
||||
onConfirmSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
btnPanel.setOpaque(false);
|
||||
|
||||
JButton pasteScreenBtn = new JButton("Parse Current Screen");
|
||||
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pasteScreenBtn.setFont(font);
|
||||
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
|
||||
|
||||
JButton selectBtn = new JButton("Select Dataset");
|
||||
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
selectBtn.setFont(font);
|
||||
selectBtn.addActionListener(e -> onConfirmSelection());
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel");
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.setFont(font);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
confirmed = false;
|
||||
dispose();
|
||||
});
|
||||
|
||||
btnPanel.add(pasteScreenBtn);
|
||||
btnPanel.add(Box.createHorizontalStrut(10));
|
||||
btnPanel.add(selectBtn);
|
||||
btnPanel.add(cancelBtn);
|
||||
|
||||
bottomPanel.add(btnPanel, BorderLayout.EAST);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// Initial setup
|
||||
setupTableColumns();
|
||||
if (initialQuery != null && !initialQuery.isEmpty()) {
|
||||
runQuery();
|
||||
} else {
|
||||
parseCurrentScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupTableColumns() {
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
tableModel.setRowCount(0);
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
tableModel.setColumnIdentifiers(new Object[]{
|
||||
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
|
||||
});
|
||||
} else {
|
||||
tableModel.setColumnIdentifiers(new Object[]{
|
||||
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void runQuery() {
|
||||
setupTableColumns();
|
||||
currentEntries.clear();
|
||||
String query = queryField.getText().trim();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
|
||||
if (client != null && client.getXfer() != null) {
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(query);
|
||||
currentEntries.addAll(cmsEntries);
|
||||
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
||||
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(query);
|
||||
currentEntries.addAll(tsoEntries);
|
||||
for (TSODirectoryEntry entry : tsoEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
||||
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusLabel.setText(currentEntries.size() + " entries found.");
|
||||
}
|
||||
|
||||
private void parseCurrentScreen() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
statusLabel.setText("No active session to parse.");
|
||||
return;
|
||||
}
|
||||
|
||||
setupTableColumns();
|
||||
currentEntries.clear();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
String screenText = client.getPS().getString(0, client.getPS().getSize());
|
||||
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(screenText);
|
||||
currentEntries.addAll(cmsEntries);
|
||||
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
||||
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText);
|
||||
currentEntries.addAll(tsoEntries);
|
||||
for (TSODirectoryEntry entry : tsoEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
||||
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space.");
|
||||
}
|
||||
|
||||
private void onConfirmSelection() {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0 || row >= currentEntries.size()) {
|
||||
JOptionPane.showMessageDialog(this, "Please select an entry from the table.",
|
||||
"No Selection", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
HostDirectoryEntry entry = currentEntries.get(row);
|
||||
selectedHostFile = entry.getName();
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
public String getSelectedHostFile() {
|
||||
return selectedHostFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.printer.*;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import java.awt.*;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* IBM 3287 / 3286 TN3270E Printer Session Manager and Spool Viewer.
|
||||
* Integrates directly with lib3270j.printer.Telnet3270EP and PD3270.
|
||||
*/
|
||||
public class PrinterSessionDialog extends JDialog implements PrintSessionListener {
|
||||
|
||||
private final Frame parent;
|
||||
private PrinterConfig config;
|
||||
private Telnet3270EP printerSession;
|
||||
|
||||
// Configuration Fields
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JTextField printerLuField;
|
||||
private JTextField displayLuField;
|
||||
private JComboBox<String> codePageCombo;
|
||||
private JCheckBox tlsCheck;
|
||||
private JCheckBox verifyCertCheck;
|
||||
private JComboBox<PrinterConfig.DestinationType> destinationCombo;
|
||||
private JTextField targetField;
|
||||
|
||||
// Status Fields
|
||||
private JLabel statusLabel;
|
||||
private JLabel sessionTypeLabel;
|
||||
private JLabel pagesLabel;
|
||||
private JLabel bytesLabel;
|
||||
private JButton connectBtn;
|
||||
private JButton disconnectBtn;
|
||||
|
||||
// Spool Display
|
||||
private JTextArea spoolArea;
|
||||
|
||||
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
|
||||
super(parent, "IBM 3287 Printer Session Manager", false);
|
||||
this.parent = parent;
|
||||
|
||||
config = new PrinterConfig(defaultHost, defaultPort);
|
||||
config.setUseTls(defaultTls);
|
||||
config.setDestinationType(PrinterConfig.DestinationType.MEMORY);
|
||||
|
||||
buildUI();
|
||||
setSize(780, 560);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
// Top: Configuration Panel
|
||||
JPanel topPanel = new JPanel(new GridBagLayout());
|
||||
topPanel.setOpaque(false);
|
||||
topPanel.setBorder(ThemeManager.createTitledBorder("Printer Session Configuration"));
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(3, 4, 3, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||
|
||||
// Host / Port
|
||||
gbc.gridx = 0; gbc.gridy = 0;
|
||||
JLabel hLbl = new JLabel("Host:");
|
||||
hLbl.setFont(labelFont);
|
||||
topPanel.add(hLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
hostField = createField(config.getHost() != null ? config.getHost() : "localhost", 14);
|
||||
topPanel.add(hostField, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.weightx = 0;
|
||||
JLabel pLbl = new JLabel("Port:");
|
||||
pLbl.setFont(labelFont);
|
||||
topPanel.add(pLbl, gbc);
|
||||
|
||||
gbc.gridx = 3; gbc.weightx = 0.5;
|
||||
portField = createField(String.valueOf(config.getPort()), 5);
|
||||
topPanel.add(portField, gbc);
|
||||
|
||||
// Printer LU / Display LU
|
||||
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
|
||||
JLabel pluLbl = new JLabel("Printer LU:");
|
||||
pluLbl.setFont(labelFont);
|
||||
topPanel.add(pluLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
printerLuField = createField("", 10);
|
||||
topPanel.add(printerLuField, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.weightx = 0;
|
||||
JLabel assocLbl = new JLabel("Assoc LU:");
|
||||
assocLbl.setFont(labelFont);
|
||||
topPanel.add(assocLbl, gbc);
|
||||
|
||||
gbc.gridx = 3; gbc.weightx = 0.5;
|
||||
displayLuField = createField("", 10);
|
||||
topPanel.add(displayLuField, gbc);
|
||||
|
||||
// CodePage & TLS
|
||||
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
|
||||
JLabel cpLbl = new JLabel("CodePage:");
|
||||
cpLbl.setFont(labelFont);
|
||||
topPanel.add(cpLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"});
|
||||
ThemeManager.styleComboBox(codePageCombo);
|
||||
topPanel.add(codePageCombo, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.gridwidth = 2;
|
||||
JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
|
||||
tlsPanel.setOpaque(false);
|
||||
tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls());
|
||||
ThemeManager.styleCheckBox(tlsCheck);
|
||||
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
|
||||
ThemeManager.styleCheckBox(verifyCertCheck);
|
||||
tlsPanel.add(tlsCheck);
|
||||
tlsPanel.add(verifyCertCheck);
|
||||
topPanel.add(tlsPanel, gbc);
|
||||
|
||||
// Destination Type & Target
|
||||
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
|
||||
JLabel destLbl = new JLabel("Destination:");
|
||||
destLbl.setFont(labelFont);
|
||||
topPanel.add(destLbl, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
|
||||
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
|
||||
ThemeManager.styleComboBox(destinationCombo);
|
||||
topPanel.add(destinationCombo, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
JLabel tgtLbl = new JLabel("Target Path:");
|
||||
tgtLbl.setFont(labelFont);
|
||||
topPanel.add(tgtLbl, gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
targetField = createField("printer_output.txt", 12);
|
||||
topPanel.add(targetField, gbc);
|
||||
|
||||
// Connect / Disconnect Buttons
|
||||
gbc.gridx = 0; gbc.gridy = 4; gbc.gridwidth = 4;
|
||||
JPanel connBtnPan = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
connBtnPan.setOpaque(false);
|
||||
|
||||
connectBtn = new JButton("Start Printer Session");
|
||||
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
connectBtn.addActionListener(e -> startPrinterSession());
|
||||
|
||||
disconnectBtn = new JButton("Stop Session");
|
||||
ThemeManager.styleButton(disconnectBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
disconnectBtn.setEnabled(false);
|
||||
disconnectBtn.addActionListener(e -> stopPrinterSession());
|
||||
|
||||
connBtnPan.add(connectBtn);
|
||||
connBtnPan.add(disconnectBtn);
|
||||
topPanel.add(connBtnPan, gbc);
|
||||
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Center: Spool Area & Status
|
||||
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
|
||||
centerPanel.setOpaque(false);
|
||||
centerPanel.setBorder(ThemeManager.createTitledBorder("Printer Spool & Status"));
|
||||
|
||||
// Status Header
|
||||
JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0));
|
||||
statusHeader.setOpaque(false);
|
||||
statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4));
|
||||
|
||||
statusLabel = new JLabel("Status: Disconnected");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(labelFont);
|
||||
|
||||
sessionTypeLabel = new JLabel("Session: -");
|
||||
sessionTypeLabel.setFont(labelFont);
|
||||
|
||||
pagesLabel = new JLabel("Pages: 0");
|
||||
pagesLabel.setFont(labelFont);
|
||||
|
||||
bytesLabel = new JLabel("Bytes: 0");
|
||||
bytesLabel.setFont(labelFont);
|
||||
|
||||
statusHeader.add(statusLabel);
|
||||
statusHeader.add(sessionTypeLabel);
|
||||
statusHeader.add(pagesLabel);
|
||||
statusHeader.add(bytesLabel);
|
||||
centerPanel.add(statusHeader, BorderLayout.NORTH);
|
||||
|
||||
// Spool text area
|
||||
spoolArea = new JTextArea();
|
||||
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
spoolArea.setEditable(false);
|
||||
ThemeManager.styleTextArea(spoolArea);
|
||||
|
||||
JScrollPane spoolScroll = new JScrollPane(spoolArea);
|
||||
ThemeManager.styleScrollPane(spoolScroll);
|
||||
centerPanel.add(spoolScroll, BorderLayout.CENTER);
|
||||
|
||||
mainPanel.add(centerPanel, BorderLayout.CENTER);
|
||||
|
||||
// Bottom: Spool controls
|
||||
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
JButton clearSpoolBtn = new JButton("Clear Spool");
|
||||
ThemeManager.styleButton(clearSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
clearSpoolBtn.addActionListener(e -> {
|
||||
spoolArea.setText("");
|
||||
if (printerSession != null && printerSession.getPD() != null) {
|
||||
printerSession.getPD().resetCapture();
|
||||
}
|
||||
pagesLabel.setText("Pages: 0");
|
||||
bytesLabel.setText("Bytes: 0");
|
||||
});
|
||||
|
||||
JButton saveSpoolBtn = new JButton("Save Spool As...");
|
||||
ThemeManager.styleButton(saveSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveSpoolBtn.addActionListener(e -> saveSpool());
|
||||
|
||||
JButton printSpoolBtn = new JButton("Print Spool...");
|
||||
ThemeManager.styleButton(printSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
printSpoolBtn.addActionListener(e -> printSpool());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
bottomPanel.add(clearSpoolBtn);
|
||||
bottomPanel.add(saveSpoolBtn);
|
||||
bottomPanel.add(printSpoolBtn);
|
||||
bottomPanel.add(Box.createHorizontalStrut(10));
|
||||
bottomPanel.add(closeBtn);
|
||||
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private JTextField createField(String text, int cols) {
|
||||
JTextField tf = new JTextField(text, cols);
|
||||
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
ThemeManager.styleTextField(tf);
|
||||
return tf;
|
||||
}
|
||||
|
||||
private void startPrinterSession() {
|
||||
String host = hostField.getText().trim();
|
||||
if (host.isEmpty()) {
|
||||
hostField.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
int port = 23;
|
||||
try {
|
||||
port = Integer.parseInt(portField.getText().trim());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
|
||||
config.setHost(host);
|
||||
config.setPort(port);
|
||||
config.setUseTls(tlsCheck.isSelected());
|
||||
config.setTlsVerifyCert(verifyCertCheck.isSelected());
|
||||
config.setCodePage((String) codePageCombo.getSelectedItem());
|
||||
|
||||
String plu = printerLuField.getText().trim();
|
||||
config.setPrinterLuName(plu.isEmpty() ? null : plu);
|
||||
|
||||
String dlu = displayLuField.getText().trim();
|
||||
config.setAssociatedDisplayLuName(dlu.isEmpty() ? null : dlu);
|
||||
|
||||
config.setDestinationType((PrinterConfig.DestinationType) destinationCombo.getSelectedItem());
|
||||
config.setDestinationTarget(targetField.getText().trim());
|
||||
|
||||
if (printerSession != null) {
|
||||
printerSession.close();
|
||||
}
|
||||
|
||||
printerSession = new Telnet3270EP(config);
|
||||
printerSession.addPrintListener(this);
|
||||
|
||||
connectBtn.setEnabled(false);
|
||||
disconnectBtn.setEnabled(true);
|
||||
statusLabel.setText("Status: Connecting...");
|
||||
statusLabel.setForeground(new Color(255, 200, 80));
|
||||
|
||||
new Thread(() -> {
|
||||
boolean success = printerSession.open();
|
||||
if (!success) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
connectBtn.setEnabled(true);
|
||||
disconnectBtn.setEnabled(false);
|
||||
statusLabel.setText("Status: Connection Failed");
|
||||
statusLabel.setForeground(new Color(255, 80, 80));
|
||||
});
|
||||
}
|
||||
}, "PrinterSession-Thread").start();
|
||||
}
|
||||
|
||||
private void stopPrinterSession() {
|
||||
if (printerSession != null) {
|
||||
printerSession.close();
|
||||
printerSession = null;
|
||||
}
|
||||
connectBtn.setEnabled(true);
|
||||
disconnectBtn.setEnabled(false);
|
||||
statusLabel.setText("Status: Disconnected");
|
||||
statusLabel.setForeground(new Color(180, 180, 180));
|
||||
sessionTypeLabel.setText("Session: -");
|
||||
}
|
||||
|
||||
private void saveSpool() {
|
||||
JFileChooser fc = new JFileChooser();
|
||||
fc.setDialogTitle("Save Printer Spool Output");
|
||||
fc.setSelectedFile(new File("spool_output.txt"));
|
||||
if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
try (FileOutputStream fos = new FileOutputStream(fc.getSelectedFile())) {
|
||||
fos.write(spoolArea.getText().getBytes(StandardCharsets.UTF_8));
|
||||
JOptionPane.showMessageDialog(this, "Spool saved successfully to " + fc.getSelectedFile().getName(),
|
||||
"Saved", JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (IOException e) {
|
||||
JOptionPane.showMessageDialog(this, "Save error: " + e.getMessage(),
|
||||
"Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void printSpool() {
|
||||
String text = spoolArea.getText();
|
||||
if (text.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Spool buffer is empty.", "Nothing to Print", JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setJobName("j3270 3287 Print Spool");
|
||||
if (job.printDialog()) {
|
||||
try {
|
||||
spoolArea.print();
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Print error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== PrintSessionListener ==========
|
||||
|
||||
@Override
|
||||
public void onPrintJobStarted(PrintSessionEvent event) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("Status: Printing...");
|
||||
statusLabel.setForeground(new Color(50, 205, 50));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrintJobData(PrintSessionEvent event) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (printerSession != null && printerSession.getPD() != null) {
|
||||
spoolArea.setText(printerSession.getPD().getCapturedText());
|
||||
spoolArea.setCaretPosition(spoolArea.getDocument().getLength());
|
||||
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
|
||||
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrintJobPageComplete(PrintSessionEvent event) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (printerSession != null && printerSession.getPD() != null) {
|
||||
spoolArea.setText(printerSession.getPD().getCapturedText());
|
||||
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
|
||||
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrintJobComplete(PrintSessionEvent event) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (printerSession != null && printerSession.getPD() != null) {
|
||||
spoolArea.setText(printerSession.getPD().getCapturedText());
|
||||
pagesLabel.setText("Pages: " + printerSession.getPD().getPageCount());
|
||||
bytesLabel.setText("Bytes: " + printerSession.getPD().getByteCount());
|
||||
}
|
||||
statusLabel.setText("Status: In Session (Idle)");
|
||||
statusLabel.setForeground(new Color(50, 205, 50));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrinterStatusChanged(PrintSessionEvent event) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
int code = event.getStatusCode();
|
||||
if (code == PrinterConstants.STATUS_CONNECTED || code == PrinterConstants.STATUS_PRINTER_READY || code == PrinterConstants.STATUS_PRINTING || code == PrinterConstants.STATUS_JOB_COMPLETE || code == PrinterConstants.STATUS_PAGE_COMPLETE) {
|
||||
String lu = printerSession != null ? printerSession.getAssignedLuName() : null;
|
||||
statusLabel.setText("Status: Connected" + (lu != null ? " (" + lu + ")" : ""));
|
||||
statusLabel.setForeground(new Color(50, 205, 50));
|
||||
connectBtn.setEnabled(false);
|
||||
disconnectBtn.setEnabled(true);
|
||||
|
||||
if (printerSession != null) {
|
||||
short luType = printerSession.getActiveLuType();
|
||||
if (luType == PrinterConstants.LU_TYPE_1_SCS) {
|
||||
sessionTypeLabel.setText("Session: LU1 (SCS)");
|
||||
} else if (luType == PrinterConstants.LU_TYPE_3_DS) {
|
||||
sessionTypeLabel.setText("Session: LU3 (3270 DS)");
|
||||
}
|
||||
}
|
||||
} else if (code == PrinterConstants.STATUS_DISCONNECTED) {
|
||||
statusLabel.setText("Status: Disconnected");
|
||||
statusLabel.setForeground(new Color(180, 180, 180));
|
||||
connectBtn.setEnabled(true);
|
||||
disconnectBtn.setEnabled(false);
|
||||
sessionTypeLabel.setText("Session: -");
|
||||
} else {
|
||||
statusLabel.setText("Status: " + event.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrinterError(PrintSessionEvent event) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("Status: Error (" + event.getMessage() + ")");
|
||||
statusLabel.setForeground(new Color(255, 80, 80));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopPrinterSession();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Utility for exporting the 3270 Presentation Space to Plain Text, HTML, or PNG image.
|
||||
*/
|
||||
public class ScreenExporter {
|
||||
|
||||
public static void showExportDialog(Frame parent, Telnet3270Client client, TerminalPanel panel) {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(parent, "Connect to a host before exporting the screen.",
|
||||
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
JFileChooser fc = new JFileChooser();
|
||||
fc.setDialogTitle("Save Screen As...");
|
||||
fc.addChoosableFileFilter(new FileNameExtensionFilter("PNG Image (*.png)", "png"));
|
||||
fc.addChoosableFileFilter(new FileNameExtensionFilter("HTML Document (*.html)", "html", "htm"));
|
||||
FileNameExtensionFilter txtFilter = new FileNameExtensionFilter("Plain Text (*.txt)", "txt");
|
||||
fc.addChoosableFileFilter(txtFilter);
|
||||
fc.setFileFilter(txtFilter);
|
||||
fc.setSelectedFile(new File("screen_capture.txt"));
|
||||
|
||||
int result = fc.showSaveDialog(parent);
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
File file = fc.getSelectedFile();
|
||||
String name = file.getName().toLowerCase();
|
||||
|
||||
try {
|
||||
if (name.endsWith(".png") || (fc.getFileFilter() != null && fc.getFileFilter().getDescription().contains("PNG"))) {
|
||||
if (!name.endsWith(".png")) file = new File(file.getAbsolutePath() + ".png");
|
||||
exportToPng(panel, file);
|
||||
} else if (name.endsWith(".html") || name.endsWith(".htm") || (fc.getFileFilter() != null && fc.getFileFilter().getDescription().contains("HTML"))) {
|
||||
if (!name.endsWith(".html") && !name.endsWith(".htm")) file = new File(file.getAbsolutePath() + ".html");
|
||||
exportToHtml(client, file);
|
||||
} else {
|
||||
if (!name.endsWith(".txt")) file = new File(file.getAbsolutePath() + ".txt");
|
||||
exportToText(client, file);
|
||||
}
|
||||
|
||||
JOptionPane.showMessageDialog(parent, "Screen saved successfully to:\n" + file.getAbsolutePath(),
|
||||
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(parent, "Failed to export screen: " + ex.getMessage(),
|
||||
"Export Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void exportToText(Telnet3270Client client, File file) throws IOException {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
|
||||
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||
for (int r = 0; r < rows; r++) {
|
||||
StringBuilder line = new StringBuilder();
|
||||
for (int c = 0; c < cols; c++) {
|
||||
ExtendedAttribute ea = sb.getCell(r * cols + c);
|
||||
char ch = ea.ucs4;
|
||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||
line.append(' ');
|
||||
} else {
|
||||
line.append(ch);
|
||||
}
|
||||
}
|
||||
// Strip trailing spaces for clean text output
|
||||
int lastNonSpace = line.length() - 1;
|
||||
while (lastNonSpace >= 0 && line.charAt(lastNonSpace) == ' ') {
|
||||
lastNonSpace--;
|
||||
}
|
||||
writer.write(line.substring(0, lastNonSpace + 1));
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void exportToHtml(Telnet3270Client client, File file) throws IOException {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
|
||||
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||
writer.write("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>3270 Screen Capture</title>\n");
|
||||
writer.write("<style>\n");
|
||||
writer.write("body { background-color: #0a0a0a; color: #32cd32; font-family: 'Courier New', Courier, monospace; font-size: 14px; margin: 20px; }\n");
|
||||
writer.write(".screen { background-color: #000; padding: 15px; border-radius: 4px; display: inline-block; box-shadow: 0 0 10px rgba(0,0,0,0.8); line-height: 1.2; }\n");
|
||||
writer.write(".c-blue { color: #5078ff; }\n");
|
||||
writer.write(".c-red { color: #ff3232; }\n");
|
||||
writer.write(".c-pink { color: #ff82b4; }\n");
|
||||
writer.write(".c-green { color: #32cd32; }\n");
|
||||
writer.write(".c-turq { color: #40e0d0; }\n");
|
||||
writer.write(".c-yellow { color: #ffff50; }\n");
|
||||
writer.write(".c-white { color: #ffffff; }\n");
|
||||
writer.write(".c-black { color: #000000; }\n");
|
||||
writer.write(".c-orange { color: #ffa500; }\n");
|
||||
writer.write(".c-purple { color: #b482ff; }\n");
|
||||
writer.write(".c-palegreen { color: #90ee90; }\n");
|
||||
writer.write(".c-paleturq { color: #afeeee; }\n");
|
||||
writer.write(".c-grey { color: #aaaaaa; }\n");
|
||||
writer.write(".bold { font-weight: bold; }\n");
|
||||
writer.write(".underline { text-decoration: underline; }\n");
|
||||
writer.write("</style>\n</head>\n<body>\n<div class=\"screen\"><pre>");
|
||||
|
||||
byte currentFA = 0;
|
||||
ExtendedAttribute currentFieldEa = null;
|
||||
|
||||
for (int r = 0; r < rows; r++) {
|
||||
String currentStyle = null;
|
||||
StringBuilder currentRun = new StringBuilder();
|
||||
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
|
||||
if (ea.isFieldAttribute()) {
|
||||
currentFA = ea.fa;
|
||||
currentFieldEa = ea;
|
||||
if (currentRun.length() > 0) {
|
||||
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||
currentRun.setLength(0);
|
||||
currentStyle = null;
|
||||
}
|
||||
writer.write(' ');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (faIsZero(currentFA & 0xFF)) {
|
||||
if (currentRun.length() > 0) {
|
||||
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||
currentRun.setLength(0);
|
||||
currentStyle = null;
|
||||
}
|
||||
writer.write(' ');
|
||||
continue;
|
||||
}
|
||||
|
||||
char ch = ea.ucs4;
|
||||
if (ch == 0 || ch == 0xFF) {
|
||||
if (currentRun.length() > 0) {
|
||||
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||
currentRun.setLength(0);
|
||||
currentStyle = null;
|
||||
}
|
||||
writer.write(' ');
|
||||
continue;
|
||||
}
|
||||
|
||||
String colorClass = getHtmlColorClass(ea, currentFieldEa, currentFA);
|
||||
boolean bold = faIsHigh(currentFA & 0xFF) || (ea.gr & GR_INTENSIFY) != 0;
|
||||
boolean underline = (ea.gr & GR_UNDERLINE) != 0;
|
||||
|
||||
StringBuilder css = new StringBuilder();
|
||||
if (!colorClass.isEmpty()) css.append(colorClass).append(" ");
|
||||
if (bold) css.append("bold ");
|
||||
if (underline) css.append("underline ");
|
||||
String style = css.toString().trim();
|
||||
|
||||
if (currentStyle == null || !currentStyle.equals(style)) {
|
||||
if (currentRun.length() > 0) {
|
||||
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||
currentRun.setLength(0);
|
||||
}
|
||||
currentStyle = style;
|
||||
}
|
||||
currentRun.append(escapeHtml(ch));
|
||||
}
|
||||
|
||||
if (currentRun.length() > 0) {
|
||||
writeHtmlSpan(writer, currentStyle, currentRun.toString());
|
||||
}
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
writer.write("</pre></div>\n</body>\n</html>");
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeHtmlSpan(BufferedWriter writer, String style, String content) throws IOException {
|
||||
if (style != null && !style.isEmpty()) {
|
||||
writer.write("<span class=\"" + style + "\">" + content + "</span>");
|
||||
} else {
|
||||
writer.write(content);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getHtmlColorClass(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
||||
int fg = ea.fg != 0 ? (ea.fg & 0xFF) : (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
|
||||
if (fg >= 0xf0 && fg <= 0xff) {
|
||||
switch (fg - 0xf0) {
|
||||
case 1: return "c-blue";
|
||||
case 2: return "c-red";
|
||||
case 3: return "c-pink";
|
||||
case 4: return "c-green";
|
||||
case 5: return "c-turq";
|
||||
case 6: return "c-yellow";
|
||||
case 7: return "c-white";
|
||||
case 8: return "c-black";
|
||||
case 9: return "c-blue";
|
||||
case 10: return "c-orange";
|
||||
case 11: return "c-purple";
|
||||
case 12: return "c-palegreen";
|
||||
case 13: return "c-paleturq";
|
||||
case 14: return "c-grey";
|
||||
case 15: return "c-white";
|
||||
}
|
||||
}
|
||||
if (faIsProtected(currentFA & 0xFF)) {
|
||||
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-blue";
|
||||
}
|
||||
return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green";
|
||||
}
|
||||
|
||||
private static String escapeHtml(char ch) {
|
||||
switch (ch) {
|
||||
case '<': return "<";
|
||||
case '>': return ">";
|
||||
case '&': return "&";
|
||||
case '"': return """;
|
||||
default: return String.valueOf(ch);
|
||||
}
|
||||
}
|
||||
|
||||
public static void exportToPng(TerminalPanel panel, File file) throws IOException {
|
||||
int w = panel.getWidth();
|
||||
int h = panel.getHeight();
|
||||
if (w <= 0 || h <= 0) {
|
||||
Dimension pref = panel.getPreferredSize();
|
||||
w = Math.max(pref.width, 800);
|
||||
h = Math.max(pref.height, 500);
|
||||
}
|
||||
|
||||
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2 = image.createGraphics();
|
||||
panel.paint(g2);
|
||||
g2.dispose();
|
||||
|
||||
ImageIO.write(image, "PNG", file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Dialog for composing and executing Host On-Demand ECL mnemonic keystroke scripts.
|
||||
*/
|
||||
public class ScriptDialog extends JDialog {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private JTextArea scriptArea;
|
||||
private JLabel statusLabel;
|
||||
|
||||
public ScriptDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "Run Keystrokes / ECL Script", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
|
||||
buildUI();
|
||||
setSize(600, 480);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
|
||||
// Header
|
||||
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
|
||||
topPanel.setOpaque(false);
|
||||
JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>"
|
||||
+ "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>");
|
||||
descLabel.setFont(labelFont);
|
||||
topPanel.add(descLabel, BorderLayout.CENTER);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Script area
|
||||
scriptArea = new JTextArea();
|
||||
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
scriptArea.setLineWrap(true);
|
||||
scriptArea.setWrapStyleWord(false);
|
||||
ThemeManager.styleTextArea(scriptArea);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(scriptArea);
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
|
||||
// Mnemonic helper buttons
|
||||
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
|
||||
tokenPanel.setOpaque(false);
|
||||
tokenPanel.setBorder(ThemeManager.createTitledBorder("Insert Keystroke Token"));
|
||||
|
||||
String[] tokens = {
|
||||
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
|
||||
"[eraseeof]", "[eraseinput]", "[newline]", "[dup]", "[fm]",
|
||||
"[attn]", "[sysreq]", "[cursel]",
|
||||
"[pf1]", "[pf2]", "[pf3]", "[pf4]", "[pf5]", "[pf6]",
|
||||
"[pf7]", "[pf8]", "[pf9]", "[pf10]", "[pf11]", "[pf12]",
|
||||
"[pf13]", "[pf14]", "[pf15]", "[pf16]", "[pf17]", "[pf18]",
|
||||
"[pf19]", "[pf20]", "[pf21]", "[pf22]", "[pf23]", "[pf24]",
|
||||
"[pa1]", "[pa2]", "[pa3]", "[up]", "[down]", "[left]", "[right]", "[home]"
|
||||
};
|
||||
|
||||
for (String token : tokens) {
|
||||
JButton btn = new JButton(token);
|
||||
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
|
||||
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btn.setFocusable(false);
|
||||
btn.setMargin(new Insets(2, 4, 2, 4));
|
||||
btn.addActionListener(e -> insertToken(token));
|
||||
tokenPanel.add(btn);
|
||||
}
|
||||
|
||||
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
|
||||
centerPanel.setOpaque(false);
|
||||
centerPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
centerPanel.add(tokenPanel, BorderLayout.SOUTH);
|
||||
mainPanel.add(centerPanel, BorderLayout.CENTER);
|
||||
|
||||
// Bottom
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Ready");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton loadBtn = new JButton("Load Script...");
|
||||
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
loadBtn.setFont(labelFont);
|
||||
loadBtn.addActionListener(e -> loadScript());
|
||||
|
||||
JButton saveBtn = new JButton("Save Script...");
|
||||
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveBtn.setFont(labelFont);
|
||||
saveBtn.addActionListener(e -> saveScript());
|
||||
|
||||
JButton runBtn = new JButton("Execute");
|
||||
ThemeManager.styleButton(runBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
runBtn.setFont(labelFont);
|
||||
runBtn.addActionListener(e -> executeScript());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(loadBtn);
|
||||
buttonPanel.add(saveBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(10));
|
||||
buttonPanel.add(runBtn);
|
||||
buttonPanel.add(closeBtn);
|
||||
|
||||
bottomPanel.add(buttonPanel, BorderLayout.EAST);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private void insertToken(String token) {
|
||||
scriptArea.replaceSelection(token);
|
||||
scriptArea.requestFocusInWindow();
|
||||
}
|
||||
|
||||
private void executeScript() {
|
||||
String script = scriptArea.getText();
|
||||
if (script == null || script.trim().isEmpty()) {
|
||||
statusLabel.setText("Script is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
statusLabel.setText("Not connected to host.");
|
||||
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before executing scripts.",
|
||||
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
statusLabel.setText("Executing keystrokes...");
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// Split multi-line scripts or process sequentially
|
||||
String cleaned = script.replace("\r\n", "\n").replace("\r", "\n");
|
||||
String[] lines = cleaned.split("\n");
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i].trim();
|
||||
if (!line.isEmpty() && !line.startsWith("#") && !line.startsWith("//")) {
|
||||
client.sendKeys(line);
|
||||
Thread.sleep(100);
|
||||
}
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("Script execution completed successfully.");
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("Error executing script: " + ex.getMessage());
|
||||
JOptionPane.showMessageDialog(this, "Execution error: " + ex.getMessage(),
|
||||
"Script Error", JOptionPane.ERROR_MESSAGE);
|
||||
});
|
||||
}
|
||||
}, "ScriptRunner").start();
|
||||
}
|
||||
|
||||
private void loadScript() {
|
||||
JFileChooser fc = new JFileChooser();
|
||||
fc.setDialogTitle("Open Script File");
|
||||
if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
try {
|
||||
byte[] bytes = java.nio.file.Files.readAllBytes(fc.getSelectedFile().toPath());
|
||||
scriptArea.setText(new String(bytes, StandardCharsets.UTF_8));
|
||||
statusLabel.setText("Loaded script: " + fc.getSelectedFile().getName());
|
||||
} catch (IOException e) {
|
||||
JOptionPane.showMessageDialog(this, "Could not open file: " + e.getMessage(),
|
||||
"Open Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveScript() {
|
||||
JFileChooser fc = new JFileChooser();
|
||||
fc.setDialogTitle("Save Script File");
|
||||
if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
try {
|
||||
java.nio.file.Files.write(fc.getSelectedFile().toPath(),
|
||||
scriptArea.getText().getBytes(StandardCharsets.UTF_8));
|
||||
statusLabel.setText("Saved script: " + fc.getSelectedFile().getName());
|
||||
} catch (IOException e) {
|
||||
JOptionPane.showMessageDialog(this, "Could not save file: " + e.getMessage(),
|
||||
"Save Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FlowLayout subclass that fully supports word wrapping inside JScrollPanes / dialogs.
|
||||
*/
|
||||
private static class WrapLayout extends FlowLayout {
|
||||
public WrapLayout(int align, int hgap, int vgap) {
|
||||
super(align, hgap, vgap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension preferredLayoutSize(Container target) {
|
||||
return layoutSize(target, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension minimumLayoutSize(Container target) {
|
||||
Dimension minimum = layoutSize(target, false);
|
||||
minimum.width -= (getHgap() + 1);
|
||||
return minimum;
|
||||
}
|
||||
|
||||
private Dimension layoutSize(Container target, boolean preferred) {
|
||||
synchronized (target.getTreeLock()) {
|
||||
int targetWidth = target.getWidth();
|
||||
if (targetWidth == 0) targetWidth = Integer.MAX_VALUE;
|
||||
|
||||
int hgap = getHgap();
|
||||
int vgap = getVgap();
|
||||
Insets insets = target.getInsets();
|
||||
int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
|
||||
int maxWidth = targetWidth - horizontalInsetsAndGap;
|
||||
|
||||
Dimension dim = new Dimension(0, 0);
|
||||
int rowWidth = 0;
|
||||
int rowHeight = 0;
|
||||
|
||||
int nmembers = target.getComponentCount();
|
||||
for (int i = 0; i < nmembers; i++) {
|
||||
Component m = target.getComponent(i);
|
||||
if (m.isVisible()) {
|
||||
Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
|
||||
if (rowWidth + d.width > maxWidth) {
|
||||
addRow(dim, rowWidth, rowHeight);
|
||||
rowWidth = 0;
|
||||
rowHeight = 0;
|
||||
}
|
||||
if (rowWidth != 0) rowWidth += hgap;
|
||||
rowWidth += d.width;
|
||||
rowHeight = Math.max(rowHeight, d.height);
|
||||
}
|
||||
}
|
||||
addRow(dim, rowWidth, rowHeight);
|
||||
|
||||
dim.width += horizontalInsetsAndGap;
|
||||
dim.height += insets.top + insets.bottom + vgap * 2;
|
||||
return dim;
|
||||
}
|
||||
}
|
||||
|
||||
private void addRow(Dimension dim, int rowWidth, int rowHeight) {
|
||||
dim.width = Math.max(dim.width, rowWidth);
|
||||
if (dim.height > 0) dim.height += getVgap();
|
||||
dim.height += rowHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
-155
@@ -1,9 +1,9 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import org.pubvm.j3270.J3270App;
|
||||
import org.pubvm.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
@@ -19,18 +19,11 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
private final J3270App parentApp;
|
||||
|
||||
// Dark theme colors
|
||||
private static final Color DARK_BG = new Color(43, 43, 43);
|
||||
private static final Color DARK_BG_LIGHTER = new Color(55, 55, 55);
|
||||
private static final Color DARK_FG = new Color(224, 224, 224);
|
||||
private static final Color DARK_BORDER = new Color(70, 70, 70);
|
||||
private static final Color DARK_SELECTION = new Color(75, 110, 175);
|
||||
private static final Color DARK_BUTTON_BG = new Color(60, 63, 65);
|
||||
private static final Color DARK_FIELD_BG = new Color(50, 50, 50);
|
||||
|
||||
// Appearance tab
|
||||
private JComboBox<UITheme> uiThemeBox;
|
||||
private JComboBox<String> fontBox;
|
||||
private JSpinner fontSizeSpinner;
|
||||
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsModeBox;
|
||||
|
||||
// Behavior tab
|
||||
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
||||
@@ -50,22 +43,30 @@ public class SettingsDialog extends JDialog {
|
||||
this.parentApp = parent;
|
||||
|
||||
initComponents();
|
||||
setSize(550, 450);
|
||||
setSize(560, 480);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
JTabbedPane tabbedPane = new JTabbedPane();
|
||||
ThemeManager.styleTabbedPane(tabbedPane);
|
||||
|
||||
tabbedPane.addTab("Appearance", createAppearancePanel());
|
||||
tabbedPane.addTab("Behavior", createBehaviorPanel());
|
||||
tabbedPane.addTab("Advanced", createAdvancedPanel());
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
|
||||
JButton btnExport = new JButton("Export Config...");
|
||||
JButton btnOk = new JButton("OK");
|
||||
ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
JButton btnApply = new JButton("Apply");
|
||||
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
|
||||
|
||||
JButton btnCancel = new JButton("Cancel");
|
||||
ThemeManager.styleButton(btnCancel, ThemeManager.ButtonVariant.CANCEL);
|
||||
|
||||
JButton btnOk = new JButton("OK");
|
||||
ThemeManager.styleButton(btnOk, ThemeManager.ButtonVariant.PRIMARY);
|
||||
|
||||
btnExport.addActionListener((ActionEvent e) -> {
|
||||
exportConfig();
|
||||
@@ -95,132 +96,11 @@ public class SettingsDialog extends JDialog {
|
||||
getContentPane().add(tabbedPane, BorderLayout.CENTER);
|
||||
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
// Apply dark theme to all components for cross-platform readability
|
||||
applyDarkTheme(getContentPane());
|
||||
applyDarkTheme(tabbedPane);
|
||||
applyDarkTheme(buttonPanel);
|
||||
getContentPane().setBackground(DARK_BG);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
// ========== Dark Theme Utility ==========
|
||||
|
||||
/**
|
||||
* Recursively apply dark theme to a component and all its children.
|
||||
* Ensures the Settings dialog is readable on Windows, Linux, and macOS.
|
||||
*/
|
||||
private void applyDarkTheme(Component comp) {
|
||||
if (comp instanceof JTabbedPane) {
|
||||
JTabbedPane tp = (JTabbedPane) comp;
|
||||
tp.setBackground(DARK_BG);
|
||||
tp.setForeground(DARK_FG);
|
||||
for (int i = 0; i < tp.getTabCount(); i++) {
|
||||
applyDarkTheme(tp.getComponentAt(i));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTable) {
|
||||
JTable table = (JTable) comp;
|
||||
table.setBackground(DARK_FIELD_BG);
|
||||
table.setForeground(DARK_FG);
|
||||
table.setSelectionBackground(DARK_SELECTION);
|
||||
table.setSelectionForeground(Color.WHITE);
|
||||
table.setGridColor(DARK_BORDER);
|
||||
JTableHeader header = table.getTableHeader();
|
||||
if (header != null) {
|
||||
header.setBackground(DARK_BG_LIGHTER);
|
||||
header.setForeground(DARK_FG);
|
||||
DefaultTableCellRenderer headerRenderer = new DefaultTableCellRenderer();
|
||||
headerRenderer.setBackground(DARK_BG_LIGHTER);
|
||||
headerRenderer.setForeground(DARK_FG);
|
||||
header.setDefaultRenderer(headerRenderer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JScrollPane) {
|
||||
JScrollPane sp = (JScrollPane) comp;
|
||||
sp.setBackground(DARK_BG);
|
||||
sp.getViewport().setBackground(DARK_FIELD_BG);
|
||||
applyDarkTheme(sp.getViewport().getView());
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JButton) {
|
||||
JButton btn = (JButton) comp;
|
||||
btn.setBackground(DARK_BUTTON_BG);
|
||||
btn.setForeground(DARK_FG);
|
||||
btn.setFocusPainted(false);
|
||||
btn.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(DARK_BORDER, 1),
|
||||
BorderFactory.createEmptyBorder(3, 10, 3, 10)));
|
||||
btn.setOpaque(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JComboBox) {
|
||||
JComboBox<?> cb = (JComboBox<?>) comp;
|
||||
cb.setBackground(DARK_FIELD_BG);
|
||||
cb.setForeground(DARK_FG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JSpinner) {
|
||||
JSpinner sp = (JSpinner) comp;
|
||||
sp.setBackground(DARK_FIELD_BG);
|
||||
sp.setForeground(DARK_FG);
|
||||
JComponent editor = sp.getEditor();
|
||||
if (editor instanceof JSpinner.DefaultEditor) {
|
||||
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
|
||||
tf.setBackground(DARK_FIELD_BG);
|
||||
tf.setForeground(DARK_FG);
|
||||
tf.setCaretColor(DARK_FG);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTextField) {
|
||||
JTextField tf = (JTextField) comp;
|
||||
tf.setBackground(DARK_FIELD_BG);
|
||||
tf.setForeground(DARK_FG);
|
||||
tf.setCaretColor(DARK_FG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JCheckBox) {
|
||||
JCheckBox cb = (JCheckBox) comp;
|
||||
cb.setBackground(DARK_BG);
|
||||
cb.setForeground(DARK_FG);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JLabel) {
|
||||
comp.setForeground(DARK_FG);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic panel / container
|
||||
// Skip color swatch panels — their background IS the color
|
||||
if (comp instanceof JPanel && "colorSwatch".equals(comp.getName())) {
|
||||
return;
|
||||
}
|
||||
comp.setBackground(DARK_BG);
|
||||
comp.setForeground(DARK_FG);
|
||||
|
||||
if (comp instanceof JPanel) {
|
||||
JPanel panel = (JPanel) comp;
|
||||
// Style titled borders
|
||||
if (panel.getBorder() instanceof TitledBorder) {
|
||||
TitledBorder tb = (TitledBorder) panel.getBorder();
|
||||
tb.setTitleColor(DARK_FG);
|
||||
}
|
||||
}
|
||||
|
||||
if (comp instanceof Container) {
|
||||
for (Component child : ((Container) comp).getComponents()) {
|
||||
applyDarkTheme(child);
|
||||
}
|
||||
}
|
||||
ThemeManager.applyTheme(comp);
|
||||
}
|
||||
|
||||
// ========== Tab Panels ==========
|
||||
@@ -231,9 +111,22 @@ public class SettingsDialog extends JDialog {
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
|
||||
// Font family
|
||||
// UI Theme
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel themeLabel = new JLabel("Java UI Theme:");
|
||||
panel.add(themeLabel, gbc);
|
||||
|
||||
uiThemeBox = new JComboBox<>(UITheme.values());
|
||||
uiThemeBox.setSelectedItem(Settings.getJavaUiTheme());
|
||||
gbc.gridx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(uiThemeBox, gbc);
|
||||
|
||||
// Font family
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
JLabel fontLabel = new JLabel("Terminal Font:");
|
||||
panel.add(fontLabel, gbc);
|
||||
|
||||
@@ -253,7 +146,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Font size
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.gridy = 2;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
JLabel sizeLabel = new JLabel("Font Size:");
|
||||
panel.add(sizeLabel, gbc);
|
||||
@@ -264,8 +157,21 @@ public class SettingsDialog extends JDialog {
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(fontSizeSpinner, gbc);
|
||||
|
||||
// Graphics Mode
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 3;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
JLabel graphicsLabel = new JLabel("Graphics Mode:");
|
||||
panel.add(graphicsLabel, gbc);
|
||||
|
||||
graphicsModeBox = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||
graphicsModeBox.setSelectedItem(Settings.getGraphicsMode());
|
||||
gbc.gridx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(graphicsModeBox, gbc);
|
||||
|
||||
// Fill remaining space
|
||||
gbc.gridy = 2;
|
||||
gbc.gridy = 4;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(Box.createGlue(), gbc);
|
||||
|
||||
@@ -356,7 +262,7 @@ public class SettingsDialog extends JDialog {
|
||||
hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors"));
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Color init = org.pubvm.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
|
||||
Color init = haus.nightmare.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
|
||||
tempHostColors[i] = init;
|
||||
JPanel cb = new JPanel();
|
||||
cb.setName("colorSwatch");
|
||||
@@ -388,7 +294,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
for (int i = 0; i < monoKeys.length; i++) {
|
||||
String mk = monoKeys[i];
|
||||
Color init = org.pubvm.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]);
|
||||
Color init = haus.nightmare.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]);
|
||||
tempMonoColors.put(mk, init);
|
||||
|
||||
JPanel p = new JPanel(new BorderLayout());
|
||||
@@ -415,6 +321,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton btnResetColors = new JButton("Reset to Defaults");
|
||||
ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnResetColors.addActionListener(e -> {
|
||||
for (int i=0; i<16; i++) {
|
||||
tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i];
|
||||
@@ -422,8 +329,6 @@ public class SettingsDialog extends JDialog {
|
||||
for (int i=0; i<monoKeys.length; i++) {
|
||||
tempMonoColors.put(monoKeys[i], monoDefs[i]);
|
||||
}
|
||||
// Repaint container implicitly handled if we trigger a UI update,
|
||||
// but for simplicity user can just close/reopen or have it refresh on save
|
||||
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
|
||||
});
|
||||
resetPanel.add(btnResetColors);
|
||||
@@ -439,7 +344,8 @@ public class SettingsDialog extends JDialog {
|
||||
JPanel main = new JPanel(new BorderLayout());
|
||||
|
||||
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) {
|
||||
@Override
|
||||
@@ -451,8 +357,15 @@ public class SettingsDialog extends JDialog {
|
||||
// Populate table from Settings or Defaults
|
||||
for(String act : actions) {
|
||||
String def = act;
|
||||
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example
|
||||
String current = org.pubvm.j3270.config.Settings.getKeyBinding(act, def);
|
||||
if (def.equals("ERASE_INPUT")) def = "alt E";
|
||||
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);
|
||||
keymapModel.addRow(new Object[]{act, current});
|
||||
}
|
||||
@@ -464,18 +377,19 @@ public class SettingsDialog extends JDialog {
|
||||
defaultBinding = "shift F" + (i - 12);
|
||||
}
|
||||
String pf = "PF"+i;
|
||||
String cur = org.pubvm.j3270.config.Settings.getKeyBinding(pf, defaultBinding);
|
||||
String cur = haus.nightmare.j3270.config.Settings.getKeyBinding(pf, defaultBinding);
|
||||
tempKeyBindings.put(pf, cur);
|
||||
keymapModel.addRow(new Object[]{pf, cur});
|
||||
}
|
||||
for(int i=1;i<=3;i++){
|
||||
String pa = "PA"+i;
|
||||
String curPa = org.pubvm.j3270.config.Settings.getKeyBinding(pa, "alt "+i);
|
||||
String curPa = haus.nightmare.j3270.config.Settings.getKeyBinding(pa, "alt "+i);
|
||||
tempKeyBindings.put(pa, curPa);
|
||||
keymapModel.addRow(new Object[]{pa, curPa});
|
||||
}
|
||||
|
||||
JTable table = new JTable(keymapModel);
|
||||
ThemeManager.styleTable(table);
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setFillsViewportHeight(true);
|
||||
|
||||
@@ -488,9 +402,8 @@ public class SettingsDialog extends JDialog {
|
||||
captureDialog.setSize(320, 100);
|
||||
captureDialog.setLocationRelativeTo(this);
|
||||
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
|
||||
lbl.setForeground(DARK_FG);
|
||||
captureDialog.getContentPane().setBackground(DARK_BG);
|
||||
captureDialog.add(lbl);
|
||||
ThemeManager.applyThemeToWindow(captureDialog);
|
||||
captureDialog.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
@@ -510,6 +423,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Replace Binding — sets the action to exactly one new key
|
||||
JButton btnReplace = new JButton("Replace Binding");
|
||||
ThemeManager.styleButton(btnReplace, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnReplace.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
@@ -523,6 +437,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Add Binding — appends an additional key to the existing binding(s)
|
||||
JButton btnAdd = new JButton("Add Binding");
|
||||
ThemeManager.styleButton(btnAdd, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnAdd.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
@@ -543,6 +458,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Remove Last — removes the last comma-separated binding entry
|
||||
JButton btnRemoveLast = new JButton("Remove Last");
|
||||
ThemeManager.styleButton(btnRemoveLast, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnRemoveLast.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
@@ -563,6 +479,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Unbind All — clears all bindings for the action
|
||||
JButton btnUnbind = new JButton("Unbind All");
|
||||
ThemeManager.styleButton(btnUnbind, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnUnbind.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0) return;
|
||||
@@ -574,6 +491,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Reset Keymaps — restore all defaults
|
||||
JButton btnReset = new JButton("Reset All");
|
||||
ThemeManager.styleButton(btnReset, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnReset.addActionListener(e -> {
|
||||
for (int row = 0; row < keymapModel.getRowCount(); row++) {
|
||||
String action = (String) keymapModel.getValueAt(row, 0);
|
||||
@@ -606,11 +524,20 @@ public class SettingsDialog extends JDialog {
|
||||
private boolean applySettings() {
|
||||
try {
|
||||
// Apply Appearance
|
||||
UITheme selectedTheme = (UITheme) uiThemeBox.getSelectedItem();
|
||||
if (selectedTheme != null) {
|
||||
Settings.setJavaUiTheme(selectedTheme);
|
||||
ThemeManager.setTheme(selectedTheme);
|
||||
}
|
||||
|
||||
String fontFam = (String) fontBox.getSelectedItem();
|
||||
if (fontFam != null) {
|
||||
Settings.setFontFamily(fontFam);
|
||||
}
|
||||
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
|
||||
if (graphicsModeBox.getSelectedItem() != null) {
|
||||
Settings.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsModeBox.getSelectedItem());
|
||||
}
|
||||
|
||||
// Apply Behavior
|
||||
Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem();
|
||||
@@ -631,18 +558,19 @@ public class SettingsDialog extends JDialog {
|
||||
// Propagate visual changes to the app
|
||||
// Save Colors
|
||||
for (int i=0; i<16; i++) {
|
||||
org.pubvm.j3270.config.Settings.setColorOverride(i, tempHostColors[i]);
|
||||
haus.nightmare.j3270.config.Settings.setColorOverride(i, tempHostColors[i]);
|
||||
}
|
||||
for (Map.Entry<String, Color> entry : tempMonoColors.entrySet()) {
|
||||
org.pubvm.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue());
|
||||
haus.nightmare.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
// Save Keybindings
|
||||
for (Map.Entry<String, String> entry : tempKeyBindings.entrySet()) {
|
||||
org.pubvm.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue());
|
||||
haus.nightmare.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
parentApp.getTerminalPanel().reloadSettings();
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
@@ -0,0 +1,236 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Status bar displaying connection state, cursor position, timing, and lock status.
|
||||
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
|
||||
*/
|
||||
public class StatusBar extends JPanel {
|
||||
|
||||
private final JLabel connectionStatus;
|
||||
private final JLabel tlsStatus;
|
||||
private final JLabel luName;
|
||||
private final JLabel lockStatus;
|
||||
private final JLabel fieldTypeStatus;
|
||||
private final JLabel codePageInfo;
|
||||
private final JLabel modelInfo;
|
||||
private final JLabel cursorPosition;
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
|
||||
public StatusBar() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
setBackground(ThemeManager.getStatusBarBg());
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
|
||||
setPreferredSize(new Dimension(800, 22));
|
||||
|
||||
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
|
||||
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
lockStatus = createLabel("", oiaFont, ThemeManager.getOiaFgAlert());
|
||||
fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
|
||||
codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
|
||||
modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
|
||||
cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
|
||||
add(Box.createHorizontalStrut(6));
|
||||
add(connectionStatus);
|
||||
add(Box.createHorizontalStrut(10));
|
||||
add(tlsStatus);
|
||||
add(Box.createHorizontalStrut(10));
|
||||
add(luName);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(lockStatus);
|
||||
add(Box.createHorizontalStrut(10));
|
||||
add(fieldTypeStatus);
|
||||
add(Box.createHorizontalGlue());
|
||||
add(codePageInfo);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(modelInfo);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(cursorPosition);
|
||||
add(Box.createHorizontalStrut(6));
|
||||
}
|
||||
|
||||
private JLabel createLabel(String text, Font font, Color fg) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.setFont(font);
|
||||
label.setForeground(fg);
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
public void applyTheme(UITheme theme) {
|
||||
setBackground(ThemeManager.getStatusBarBg(theme));
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
UITheme theme = ThemeManager.getTheme();
|
||||
if (client == null) {
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
tlsStatus.setText("");
|
||||
luName.setText("");
|
||||
lockStatus.setText("");
|
||||
fieldTypeStatus.setText("");
|
||||
codePageInfo.setText("");
|
||||
modelInfo.setText("");
|
||||
cursorPosition.setText("001/001");
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection state
|
||||
ConnectionState state = client.getConnectionState();
|
||||
switch (state) {
|
||||
case NOT_CONNECTED:
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
break;
|
||||
case TCP_PENDING:
|
||||
case TELNET_PENDING:
|
||||
connectionStatus.setText("Connecting...");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
|
||||
break;
|
||||
case CONNECTED_3270:
|
||||
connectionStatus.setText("TN3270");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
break;
|
||||
case CONNECTED_TN3270E:
|
||||
connectionStatus.setText("TN3270E");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
break;
|
||||
case CONNECTED_SSCP:
|
||||
connectionStatus.setText("SSCP-LU");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
break;
|
||||
case CONNECTED_NVT:
|
||||
case CONNECTED_NVT_CHAR:
|
||||
case CONNECTED_E_NVT:
|
||||
connectionStatus.setText("NVT");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
break;
|
||||
case CONNECTED_UNBOUND:
|
||||
connectionStatus.setText("Unbound");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
break;
|
||||
default:
|
||||
connectionStatus.setText(state.name());
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
break;
|
||||
}
|
||||
|
||||
// TLS Status
|
||||
if (state.isConnected() && client.getConfig().isUseTls()) {
|
||||
boolean verified = client.getConfig().isTlsVerifyCert();
|
||||
javax.net.ssl.SSLSession session = client.getSslSession();
|
||||
String cipher = session != null ? session.getCipherSuite() : "TLS";
|
||||
String protocol = session != null ? session.getProtocol() : "TLS";
|
||||
if (verified) {
|
||||
tlsStatus.setText("🔒 TLS");
|
||||
tlsStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
|
||||
} else {
|
||||
tlsStatus.setText("🔓 TLS (Unverified)");
|
||||
tlsStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
|
||||
}
|
||||
} else {
|
||||
tlsStatus.setText("");
|
||||
tlsStatus.setToolTipText(null);
|
||||
}
|
||||
|
||||
// LU Name
|
||||
String lu = "";
|
||||
if (client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null && !client.getTelnetFSM().getConnectedLu().isEmpty()) {
|
||||
lu = "LU:" + client.getTelnetFSM().getConnectedLu();
|
||||
} else if (client.getConfig().getLuName() != null && !client.getConfig().getLuName().isEmpty()) {
|
||||
lu = "LU:" + client.getConfig().getLuName();
|
||||
}
|
||||
luName.setText(lu);
|
||||
luName.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
|
||||
// Lock / Inhibit status
|
||||
int inhibit = client.getOIA().getInputInhibited();
|
||||
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
|
||||
switch (inhibit) {
|
||||
case ECLConstants.INHIBIT_SYSTEM_LOCK:
|
||||
lockStatus.setText("X SYSTEM");
|
||||
break;
|
||||
case ECLConstants.INHIBIT_COMM_CHECK:
|
||||
lockStatus.setText("X COMM");
|
||||
break;
|
||||
case ECLConstants.INHIBIT_NUMERIC_ONLY:
|
||||
lockStatus.setText("X NUM");
|
||||
break;
|
||||
case ECLConstants.INHIBIT_PROTECTED_FIELD:
|
||||
lockStatus.setText("X PROT");
|
||||
break;
|
||||
case ECLConstants.INHIBIT_OVERFLOW:
|
||||
lockStatus.setText("X >");
|
||||
break;
|
||||
case ECLConstants.INHIBIT_OPERATOR_DUE:
|
||||
lockStatus.setText("X OP");
|
||||
break;
|
||||
default:
|
||||
lockStatus.setText("X LOCKED");
|
||||
break;
|
||||
}
|
||||
lockStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
|
||||
} else if (client.getInputProcessor().isInsertMode()) {
|
||||
lockStatus.setText("INSERT");
|
||||
lockStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
} else {
|
||||
lockStatus.setText("");
|
||||
}
|
||||
|
||||
// Field status: Numeric vs Alphanumeric
|
||||
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
|
||||
if (client.getOIA().isNumeric()) {
|
||||
fieldTypeStatus.setText("NUM");
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
} else {
|
||||
fieldTypeStatus.setText("ALPHA");
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
}
|
||||
} else {
|
||||
fieldTypeStatus.setText("");
|
||||
}
|
||||
|
||||
// Active Code Page
|
||||
String cp = client.getCodePage();
|
||||
codePageInfo.setText(cp != null ? "CP" + cp : "");
|
||||
codePageInfo.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
|
||||
|
||||
// Model & Dimensions info
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
|
||||
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
|
||||
// Cursor position and buffer address
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int row = sb.getCursorRow() + 1;
|
||||
int col = sb.getCursorCol() + 1;
|
||||
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
|
||||
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,955 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.border.CompoundBorder;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import javax.swing.plaf.basic.BasicButtonUI;
|
||||
import javax.swing.plaf.basic.BasicMenuBarUI;
|
||||
import javax.swing.plaf.basic.BasicMenuItemUI;
|
||||
import javax.swing.plaf.basic.BasicMenuUI;
|
||||
import javax.swing.plaf.basic.BasicPopupMenuUI;
|
||||
import javax.swing.plaf.basic.BasicTabbedPaneUI;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Centralized theme manager for j3270 Java desktop UI.
|
||||
* Provides high-contrast, platform-independent Light and Dark theme palettes,
|
||||
* custom Swing UI delegates, and recursive styling helpers for all dialogs and menus.
|
||||
*/
|
||||
public final class ThemeManager {
|
||||
|
||||
private static UITheme currentTheme = UITheme.DARK;
|
||||
private static final List<Consumer<UITheme>> themeChangeListeners = new ArrayList<>();
|
||||
|
||||
private ThemeManager() {}
|
||||
|
||||
public static UITheme getTheme() {
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
public static boolean isDark() {
|
||||
return currentTheme == UITheme.DARK;
|
||||
}
|
||||
|
||||
public static boolean isLight() {
|
||||
return currentTheme == UITheme.LIGHT;
|
||||
}
|
||||
|
||||
public static void setTheme(UITheme theme) {
|
||||
if (theme == null) theme = UITheme.DARK;
|
||||
if (currentTheme != theme) {
|
||||
currentTheme = theme;
|
||||
applyUIManagerDefaults(theme);
|
||||
for (Consumer<UITheme> listener : new ArrayList<>(themeChangeListeners)) {
|
||||
try {
|
||||
listener.accept(theme);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void addThemeChangeListener(Consumer<UITheme> listener) {
|
||||
if (listener != null && !themeChangeListeners.contains(listener)) {
|
||||
themeChangeListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeThemeChangeListener(Consumer<UITheme> listener) {
|
||||
themeChangeListeners.remove(listener);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Color Palette Tokens
|
||||
// =========================================================================
|
||||
|
||||
public static Color getBgMain() { return getBgMain(currentTheme); }
|
||||
public static Color getBgMain(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(28, 28, 28) : new Color(242, 242, 244);
|
||||
}
|
||||
|
||||
public static Color getBgPanel() { return getBgPanel(currentTheme); }
|
||||
public static Color getBgPanel(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(36, 36, 36) : new Color(250, 250, 252);
|
||||
}
|
||||
|
||||
public static Color getBgComponent() { return getBgComponent(currentTheme); }
|
||||
public static Color getBgComponent(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(46, 46, 46) : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getBgComponentLighter() { return getBgComponentLighter(currentTheme); }
|
||||
public static Color getBgComponentLighter(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(56, 56, 56) : new Color(236, 236, 238);
|
||||
}
|
||||
|
||||
public static Color getFgMain() { return getFgMain(currentTheme); }
|
||||
public static Color getFgMain(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(238, 238, 238) : new Color(24, 24, 24);
|
||||
}
|
||||
|
||||
public static Color getFgMuted() { return getFgMuted(currentTheme); }
|
||||
public static Color getFgMuted(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(175, 175, 175) : new Color(90, 90, 90);
|
||||
}
|
||||
|
||||
public static Color getFgDisabled() { return getFgDisabled(currentTheme); }
|
||||
public static Color getFgDisabled(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(160, 160, 160);
|
||||
}
|
||||
|
||||
public static Color getBorder() { return getBorder(currentTheme); }
|
||||
public static Color getBorder(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(65, 65, 65) : new Color(205, 205, 205);
|
||||
}
|
||||
public static Color getBorderColor() { return getBorder(currentTheme); }
|
||||
public static Color getBorderColor(UITheme t) { return getBorder(t); }
|
||||
|
||||
public static Color getBorderFocused() { return getBorderFocused(currentTheme); }
|
||||
public static Color getBorderFocused(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(80, 140, 230) : new Color(50, 115, 220);
|
||||
}
|
||||
|
||||
public static Color getSelectionBg() { return getSelectionBg(currentTheme); }
|
||||
public static Color getSelectionBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(55, 105, 180) : new Color(60, 125, 215);
|
||||
}
|
||||
|
||||
public static Color getSelectionFg() { return getSelectionFg(currentTheme); }
|
||||
public static Color getSelectionFg(UITheme t) {
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getMenuBarBg() { return getMenuBarBg(currentTheme); }
|
||||
public static Color getMenuBarBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(26, 26, 26) : new Color(240, 240, 242);
|
||||
}
|
||||
|
||||
public static Color getMenuBarFg() { return getMenuBarFg(currentTheme); }
|
||||
public static Color getMenuBarFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(25, 25, 25);
|
||||
}
|
||||
|
||||
public static Color getMenuPopupBg() { return getMenuPopupBg(currentTheme); }
|
||||
public static Color getMenuPopupBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getMenuItemFg() { return getMenuItemFg(currentTheme); }
|
||||
public static Color getMenuItemFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(230, 230, 230) : new Color(30, 30, 30);
|
||||
}
|
||||
|
||||
public static Color getMenuItemHoverBg() { return getMenuItemHoverBg(currentTheme); }
|
||||
public static Color getMenuItemHoverBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(55, 95, 160) : new Color(220, 232, 250);
|
||||
}
|
||||
|
||||
public static Color getMenuItemHoverFg() { return getMenuItemHoverFg(currentTheme); }
|
||||
public static Color getMenuItemHoverFg(UITheme t) {
|
||||
return t == UITheme.DARK ? Color.WHITE : new Color(15, 15, 15);
|
||||
}
|
||||
|
||||
public static Color getTabBg() { return getTabBg(currentTheme); }
|
||||
public static Color getTabBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(34, 34, 34) : new Color(230, 230, 232);
|
||||
}
|
||||
|
||||
public static Color getTabFg() { return getTabFg(currentTheme); }
|
||||
public static Color getTabFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(185, 185, 185) : new Color(75, 75, 75);
|
||||
}
|
||||
|
||||
public static Color getTabSelectedBg() { return getTabSelectedBg(currentTheme); }
|
||||
public static Color getTabSelectedBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(50, 50, 50) : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getTabSelectedFg() { return getTabSelectedFg(currentTheme); }
|
||||
public static Color getTabSelectedFg(UITheme t) {
|
||||
return t == UITheme.DARK ? Color.WHITE : new Color(20, 20, 20);
|
||||
}
|
||||
|
||||
public static Color getTableHeaderBg() { return getTableHeaderBg(currentTheme); }
|
||||
public static Color getTableHeaderBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(46, 46, 46) : new Color(232, 232, 236);
|
||||
}
|
||||
|
||||
public static Color getTableHeaderFg() { return getTableHeaderFg(currentTheme); }
|
||||
public static Color getTableHeaderFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
|
||||
}
|
||||
|
||||
public static Color getTableRowEven() { return getTableRowEven(currentTheme); }
|
||||
public static Color getTableRowEven(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(38, 38, 38) : Color.WHITE;
|
||||
}
|
||||
|
||||
public static Color getTableRowOdd() { return getTableRowOdd(currentTheme); }
|
||||
public static Color getTableRowOdd(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(33, 33, 33) : new Color(246, 247, 249);
|
||||
}
|
||||
|
||||
public static Color getTableGrid() { return getTableGrid(currentTheme); }
|
||||
public static Color getTableGrid(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(55, 55, 55) : new Color(225, 225, 225);
|
||||
}
|
||||
|
||||
public static Color getCodeAreaBg() { return getCodeAreaBg(currentTheme); }
|
||||
public static Color getCodeAreaBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(22, 22, 22) : new Color(252, 252, 252);
|
||||
}
|
||||
|
||||
public static Color getCodeAreaFg() { return getCodeAreaFg(currentTheme); }
|
||||
public static Color getCodeAreaFg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(80, 230, 80) : new Color(0, 120, 0);
|
||||
}
|
||||
|
||||
public static Color getStatusBarBg() { return getStatusBarBg(currentTheme); }
|
||||
public static Color getStatusBarBg(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(20, 20, 20) : new Color(235, 235, 238);
|
||||
}
|
||||
|
||||
public static Color getStatusBarBorder() { return getStatusBarBorder(currentTheme); }
|
||||
public static Color getStatusBarBorder(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(42, 42, 42) : new Color(210, 210, 215);
|
||||
}
|
||||
|
||||
public static Color getOiaFgNormal() { return getOiaFgNormal(currentTheme); }
|
||||
public static Color getOiaFgNormal(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(50, 205, 50) : new Color(0, 130, 0);
|
||||
}
|
||||
|
||||
public static Color getOiaFgDim() { return getOiaFgDim(currentTheme); }
|
||||
public static Color getOiaFgDim(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(110, 110, 110) : new Color(90, 90, 90);
|
||||
}
|
||||
|
||||
public static Color getOiaFgAlert() { return getOiaFgAlert(currentTheme); }
|
||||
public static Color getOiaFgAlert(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(255, 80, 80) : new Color(190, 20, 20);
|
||||
}
|
||||
|
||||
public static Color getOiaFgWarn() { return getOiaFgWarn(currentTheme); }
|
||||
public static Color getOiaFgWarn(UITheme t) {
|
||||
return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Button Variants & Color helpers
|
||||
// =========================================================================
|
||||
|
||||
public enum ButtonVariant {
|
||||
DEFAULT,
|
||||
PRIMARY,
|
||||
CANCEL,
|
||||
DANGER,
|
||||
ACCENT
|
||||
}
|
||||
|
||||
public static Color getButtonBg(ButtonVariant variant) {
|
||||
return getButtonBg(variant, currentTheme, false, false);
|
||||
}
|
||||
|
||||
public static Color getButtonBg(ButtonVariant variant, UITheme t) {
|
||||
return getButtonBg(variant, t, false, false);
|
||||
}
|
||||
|
||||
public static Color getButtonBg(ButtonVariant variant, UITheme t, boolean hover, boolean pressed) {
|
||||
switch (variant) {
|
||||
case PRIMARY:
|
||||
if (pressed) return new Color(25, 105, 35);
|
||||
if (hover) return new Color(38, 145, 48);
|
||||
return new Color(32, 128, 42);
|
||||
|
||||
case DANGER:
|
||||
if (pressed) return new Color(125, 30, 30);
|
||||
if (hover) return new Color(165, 45, 45);
|
||||
return new Color(145, 38, 38);
|
||||
|
||||
case ACCENT:
|
||||
if (pressed) return new Color(35, 80, 145);
|
||||
if (hover) return new Color(55, 115, 195);
|
||||
return new Color(45, 100, 175);
|
||||
|
||||
case CANCEL:
|
||||
case DEFAULT:
|
||||
default:
|
||||
if (t == UITheme.DARK) {
|
||||
if (pressed) return new Color(42, 42, 42);
|
||||
if (hover) return new Color(68, 68, 68);
|
||||
return new Color(56, 56, 56);
|
||||
} else {
|
||||
if (pressed) return new Color(210, 210, 212);
|
||||
if (hover) return new Color(228, 228, 232);
|
||||
return new Color(238, 238, 240);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Color getButtonFg(ButtonVariant variant, UITheme t) {
|
||||
switch (variant) {
|
||||
case PRIMARY:
|
||||
case DANGER:
|
||||
case ACCENT:
|
||||
return Color.WHITE;
|
||||
case CANCEL:
|
||||
case DEFAULT:
|
||||
default:
|
||||
return t == UITheme.DARK ? new Color(235, 235, 235) : new Color(30, 30, 30);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// UIManager Defaults Configuration
|
||||
// =========================================================================
|
||||
|
||||
public static void applyUIManagerDefaults(UITheme theme) {
|
||||
Color bgMain = getBgMain(theme);
|
||||
Color bgPanel = getBgPanel(theme);
|
||||
Color bgComp = getBgComponent(theme);
|
||||
Color fgMain = getFgMain(theme);
|
||||
Color fgMuted = getFgMuted(theme);
|
||||
Color border = getBorder(theme);
|
||||
Color selBg = getSelectionBg(theme);
|
||||
Color selFg = getSelectionFg(theme);
|
||||
|
||||
UIManager.put("Panel.background", bgPanel);
|
||||
UIManager.put("Panel.foreground", fgMain);
|
||||
UIManager.put("Label.foreground", fgMain);
|
||||
|
||||
UIManager.put("TextField.background", bgComp);
|
||||
UIManager.put("TextField.foreground", fgMain);
|
||||
UIManager.put("TextField.caretForeground", fgMain);
|
||||
UIManager.put("TextField.selectionBackground", selBg);
|
||||
UIManager.put("TextField.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("TextArea.background", bgComp);
|
||||
UIManager.put("TextArea.foreground", fgMain);
|
||||
UIManager.put("TextArea.caretForeground", fgMain);
|
||||
UIManager.put("TextArea.selectionBackground", selBg);
|
||||
UIManager.put("TextArea.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("Button.background", getBgComponentLighter(theme));
|
||||
UIManager.put("Button.foreground", fgMain);
|
||||
UIManager.put("Button.select", selBg);
|
||||
|
||||
UIManager.put("ComboBox.background", bgComp);
|
||||
UIManager.put("ComboBox.foreground", fgMain);
|
||||
UIManager.put("ComboBox.selectionBackground", selBg);
|
||||
UIManager.put("ComboBox.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("Table.background", bgComp);
|
||||
UIManager.put("Table.foreground", fgMain);
|
||||
UIManager.put("Table.selectionBackground", selBg);
|
||||
UIManager.put("Table.selectionForeground", selFg);
|
||||
UIManager.put("Table.gridColor", getTableGrid(theme));
|
||||
|
||||
UIManager.put("TableHeader.background", getTableHeaderBg(theme));
|
||||
UIManager.put("TableHeader.foreground", getTableHeaderFg(theme));
|
||||
|
||||
UIManager.put("ScrollPane.background", bgPanel);
|
||||
UIManager.put("Viewport.background", bgComp);
|
||||
|
||||
UIManager.put("MenuBar.background", getMenuBarBg(theme));
|
||||
UIManager.put("MenuBar.foreground", getMenuBarFg(theme));
|
||||
UIManager.put("Menu.background", getMenuBarBg(theme));
|
||||
UIManager.put("Menu.foreground", getMenuBarFg(theme));
|
||||
UIManager.put("Menu.selectionBackground", selBg);
|
||||
UIManager.put("Menu.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("PopupMenu.background", getMenuPopupBg(theme));
|
||||
UIManager.put("PopupMenu.foreground", getMenuItemFg(theme));
|
||||
|
||||
UIManager.put("MenuItem.background", getMenuPopupBg(theme));
|
||||
UIManager.put("MenuItem.foreground", getMenuItemFg(theme));
|
||||
UIManager.put("MenuItem.selectionBackground", selBg);
|
||||
UIManager.put("MenuItem.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("CheckBoxMenuItem.background", getMenuPopupBg(theme));
|
||||
UIManager.put("CheckBoxMenuItem.foreground", getMenuItemFg(theme));
|
||||
UIManager.put("CheckBoxMenuItem.selectionBackground", selBg);
|
||||
UIManager.put("CheckBoxMenuItem.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("RadioButtonMenuItem.background", getMenuPopupBg(theme));
|
||||
UIManager.put("RadioButtonMenuItem.foreground", getMenuItemFg(theme));
|
||||
UIManager.put("RadioButtonMenuItem.selectionBackground", selBg);
|
||||
UIManager.put("RadioButtonMenuItem.selectionForeground", selFg);
|
||||
|
||||
UIManager.put("Separator.background", border);
|
||||
UIManager.put("Separator.foreground", border);
|
||||
|
||||
UIManager.put("TabbedPane.background", getTabBg(theme));
|
||||
UIManager.put("TabbedPane.foreground", getTabFg(theme));
|
||||
UIManager.put("TabbedPane.selected", getTabSelectedBg(theme));
|
||||
UIManager.put("TabbedPane.selectHighlight", getTabSelectedBg(theme));
|
||||
|
||||
UIManager.put("CheckBox.background", bgPanel);
|
||||
UIManager.put("CheckBox.foreground", fgMain);
|
||||
UIManager.put("RadioButton.background", bgPanel);
|
||||
UIManager.put("RadioButton.foreground", fgMain);
|
||||
|
||||
UIManager.put("TitledBorder.titleColor", fgMain);
|
||||
UIManager.put("OptionPane.background", bgPanel);
|
||||
UIManager.put("OptionPane.messageForeground", fgMain);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Component Styling & Custom UI Delegates
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Styles a button with custom rendering, ensuring consistent high contrast
|
||||
* and proper background color across Windows, macOS, and Linux.
|
||||
*/
|
||||
public static JButton styleButton(JButton button, ButtonVariant variant) {
|
||||
if (button == null) return null;
|
||||
button.setUI(new StyledButtonUI(variant));
|
||||
button.setFocusPainted(false);
|
||||
button.setOpaque(false);
|
||||
button.setContentAreaFilled(false);
|
||||
button.setBorder(BorderFactory.createEmptyBorder(6, 14, 6, 14));
|
||||
button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
return button;
|
||||
}
|
||||
|
||||
public static JButton createStyledButton(String text, ButtonVariant variant, Runnable action) {
|
||||
JButton btn = new JButton(text);
|
||||
styleButton(btn, variant);
|
||||
if (action != null) {
|
||||
btn.addActionListener(e -> action.run());
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
|
||||
public static JTextField styleTextField(JTextField field) {
|
||||
if (field == null) return null;
|
||||
field.setBackground(getBgComponent());
|
||||
field.setForeground(getFgMain());
|
||||
field.setCaretColor(getFgMain());
|
||||
field.setSelectionColor(getSelectionBg());
|
||||
field.setSelectedTextColor(getSelectionFg());
|
||||
field.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(getBorder(), 1, true),
|
||||
new EmptyBorder(4, 8, 4, 8)));
|
||||
return field;
|
||||
}
|
||||
|
||||
public static JTextArea styleTextArea(JTextArea area) {
|
||||
if (area == null) return null;
|
||||
area.setBackground(getCodeAreaBg());
|
||||
area.setForeground(getCodeAreaFg());
|
||||
area.setCaretColor(getFgMain());
|
||||
area.setSelectionColor(getSelectionBg());
|
||||
area.setSelectedTextColor(getSelectionFg());
|
||||
return area;
|
||||
}
|
||||
|
||||
public static JComboBox<?> styleComboBox(JComboBox<?> box) {
|
||||
if (box == null) return null;
|
||||
box.setBackground(getBgComponent());
|
||||
box.setForeground(getFgMain());
|
||||
box.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||
setBorder(new EmptyBorder(4, 8, 4, 8));
|
||||
if (isSelected) {
|
||||
setBackground(getSelectionBg());
|
||||
setForeground(getSelectionFg());
|
||||
} else {
|
||||
setBackground(getBgComponent());
|
||||
setForeground(getFgMain());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
public static JTable styleTable(JTable table) {
|
||||
if (table == null) return null;
|
||||
table.setBackground(getBgComponent());
|
||||
table.setForeground(getFgMain());
|
||||
table.setSelectionBackground(getSelectionBg());
|
||||
table.setSelectionForeground(getSelectionFg());
|
||||
table.setGridColor(getTableGrid());
|
||||
table.setRowHeight(24);
|
||||
|
||||
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
|
||||
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
|
||||
setBorder(new EmptyBorder(2, 6, 2, 6));
|
||||
if (isSel) {
|
||||
setBackground(getSelectionBg());
|
||||
setForeground(getSelectionFg());
|
||||
} else {
|
||||
setBackground(row % 2 == 0 ? getTableRowEven() : getTableRowOdd());
|
||||
setForeground(getFgMain());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
if (header != null) {
|
||||
header.setBackground(getTableHeaderBg());
|
||||
header.setForeground(getTableHeaderFg());
|
||||
header.setFont(header.getFont().deriveFont(Font.BOLD));
|
||||
header.setDefaultRenderer(new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
|
||||
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
|
||||
setBackground(getTableHeaderBg());
|
||||
setForeground(getTableHeaderFg());
|
||||
setFont(getFont().deriveFont(Font.BOLD));
|
||||
setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
|
||||
new EmptyBorder(4, 6, 4, 6)));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
|
||||
if (tp == null) return null;
|
||||
tp.setUI(new StyledTabbedPaneUI());
|
||||
tp.setBackground(getBgMain());
|
||||
tp.setForeground(getFgMain());
|
||||
return tp;
|
||||
}
|
||||
|
||||
public static JMenuBar styleMenuBar(JMenuBar bar) {
|
||||
if (bar == null) return null;
|
||||
bar.setUI(new StyledMenuBarUI());
|
||||
bar.setBackground(getMenuBarBg());
|
||||
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
|
||||
return bar;
|
||||
}
|
||||
|
||||
public static JMenu styleMenu(JMenu menu) {
|
||||
if (menu == null) return null;
|
||||
menu.setUI(new StyledMenuUI());
|
||||
menu.setForeground(getMenuBarFg());
|
||||
menu.setBackground(getMenuBarBg());
|
||||
menu.setOpaque(false);
|
||||
return menu;
|
||||
}
|
||||
|
||||
public static JMenuItem styleMenuItem(JMenuItem item) {
|
||||
if (item == null) return null;
|
||||
item.setUI(new StyledMenuItemUI());
|
||||
item.setBackground(getMenuPopupBg());
|
||||
item.setForeground(getMenuItemFg());
|
||||
return item;
|
||||
}
|
||||
|
||||
public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
|
||||
if (popup == null) return null;
|
||||
popup.setUI(new StyledPopupMenuUI());
|
||||
popup.setBackground(getMenuPopupBg());
|
||||
popup.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(getBorder(), 1),
|
||||
new EmptyBorder(4, 0, 4, 0)));
|
||||
return popup;
|
||||
}
|
||||
|
||||
public static JScrollPane styleScrollPane(JScrollPane sp) {
|
||||
if (sp == null) return null;
|
||||
sp.setBackground(getBgPanel());
|
||||
if (sp.getViewport() != null) {
|
||||
sp.getViewport().setBackground(getBgComponent());
|
||||
}
|
||||
sp.setBorder(new LineBorder(getBorder(), 1));
|
||||
return sp;
|
||||
}
|
||||
|
||||
public static JSpinner styleSpinner(JSpinner sp) {
|
||||
if (sp == null) return null;
|
||||
sp.setBackground(getBgComponent());
|
||||
sp.setForeground(getFgMain());
|
||||
sp.setBorder(new LineBorder(getBorder(), 1));
|
||||
JComponent editor = sp.getEditor();
|
||||
if (editor instanceof JSpinner.DefaultEditor) {
|
||||
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
|
||||
tf.setBackground(getBgComponent());
|
||||
tf.setForeground(getFgMain());
|
||||
tf.setCaretColor(getFgMain());
|
||||
tf.setBorder(new EmptyBorder(2, 4, 2, 4));
|
||||
}
|
||||
return sp;
|
||||
}
|
||||
|
||||
public static JCheckBox styleCheckBox(JCheckBox cb) {
|
||||
if (cb == null) return null;
|
||||
cb.setOpaque(false);
|
||||
cb.setForeground(getFgMain());
|
||||
cb.setFocusPainted(false);
|
||||
return cb;
|
||||
}
|
||||
|
||||
public static JRadioButton styleRadioButton(JRadioButton rb) {
|
||||
if (rb == null) return null;
|
||||
rb.setOpaque(false);
|
||||
rb.setForeground(getFgMain());
|
||||
rb.setFocusPainted(false);
|
||||
return rb;
|
||||
}
|
||||
|
||||
public static Border createTitledBorder(String title) {
|
||||
return BorderFactory.createTitledBorder(
|
||||
new LineBorder(getBorder(), 1),
|
||||
title,
|
||||
TitledBorder.DEFAULT_JUSTIFICATION,
|
||||
TitledBorder.DEFAULT_POSITION,
|
||||
new Font(Font.SANS_SERIF, Font.BOLD, 12),
|
||||
getFgMain());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Recursive Component Styling
|
||||
// =========================================================================
|
||||
|
||||
public static void applyTheme(Component comp) {
|
||||
applyTheme(comp, currentTheme);
|
||||
}
|
||||
|
||||
public static void applyTheme(Component comp, UITheme theme) {
|
||||
if (comp == null) return;
|
||||
|
||||
// Skip swatch preview panels whose background is the user-configured color
|
||||
if ("colorSwatch".equals(comp.getName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JMenuBar) {
|
||||
styleMenuBar((JMenuBar) comp);
|
||||
for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) {
|
||||
applyTheme(((JMenuBar) comp).getMenu(i), theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JMenu) {
|
||||
styleMenu((JMenu) comp);
|
||||
JMenu menu = (JMenu) comp;
|
||||
for (int i = 0; i < menu.getItemCount(); i++) {
|
||||
JMenuItem item = menu.getItem(i);
|
||||
if (item != null) applyTheme(item, theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JMenuItem) {
|
||||
styleMenuItem((JMenuItem) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JPopupMenu) {
|
||||
stylePopupMenu((JPopupMenu) comp);
|
||||
for (Component child : ((JPopupMenu) comp).getComponents()) {
|
||||
applyTheme(child, theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTabbedPane) {
|
||||
styleTabbedPane((JTabbedPane) comp);
|
||||
JTabbedPane tp = (JTabbedPane) comp;
|
||||
for (int i = 0; i < tp.getTabCount(); i++) {
|
||||
applyTheme(tp.getComponentAt(i), theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTable) {
|
||||
styleTable((JTable) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JScrollPane) {
|
||||
styleScrollPane((JScrollPane) comp);
|
||||
JScrollPane sp = (JScrollPane) comp;
|
||||
if (sp.getViewport() != null) {
|
||||
applyTheme(sp.getViewport().getView(), theme);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JButton) {
|
||||
JButton btn = (JButton) comp;
|
||||
if (!(btn.getUI() instanceof StyledButtonUI)) {
|
||||
styleButton(btn, ButtonVariant.DEFAULT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JComboBox) {
|
||||
styleComboBox((JComboBox<?>) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JSpinner) {
|
||||
styleSpinner((JSpinner) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTextArea) {
|
||||
styleTextArea((JTextArea) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JTextField) {
|
||||
styleTextField((JTextField) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JCheckBox) {
|
||||
styleCheckBox((JCheckBox) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JRadioButton) {
|
||||
styleRadioButton((JRadioButton) comp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JLabel) {
|
||||
comp.setForeground(getFgMain(theme));
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JPanel) {
|
||||
JPanel panel = (JPanel) comp;
|
||||
if (panel.isOpaque()) {
|
||||
panel.setBackground(getBgPanel(theme));
|
||||
}
|
||||
panel.setForeground(getFgMain(theme));
|
||||
Border b = panel.getBorder();
|
||||
if (b instanceof TitledBorder) {
|
||||
TitledBorder tb = (TitledBorder) b;
|
||||
tb.setTitleColor(getFgMain(theme));
|
||||
tb.setBorder(new LineBorder(getBorder(theme), 1));
|
||||
}
|
||||
} else if (comp instanceof Container) {
|
||||
comp.setBackground(getBgMain(theme));
|
||||
comp.setForeground(getFgMain(theme));
|
||||
}
|
||||
|
||||
if (comp instanceof Container) {
|
||||
for (Component child : ((Container) comp).getComponents()) {
|
||||
applyTheme(child, theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyThemeToWindow(Window window) {
|
||||
if (window == null) return;
|
||||
window.setBackground(getBgMain());
|
||||
if (window instanceof RootPaneContainer) {
|
||||
RootPaneContainer rpc = (RootPaneContainer) window;
|
||||
if (rpc.getContentPane() != null) {
|
||||
rpc.getContentPane().setBackground(getBgMain());
|
||||
applyTheme(rpc.getContentPane());
|
||||
}
|
||||
if (rpc.getRootPane() != null && rpc.getRootPane().getJMenuBar() != null) {
|
||||
applyTheme(rpc.getRootPane().getJMenuBar());
|
||||
}
|
||||
}
|
||||
window.repaint();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Custom UI Implementations
|
||||
// =========================================================================
|
||||
|
||||
public static class StyledButtonUI extends BasicButtonUI {
|
||||
private final ButtonVariant variant;
|
||||
private boolean hover = false;
|
||||
|
||||
public StyledButtonUI(ButtonVariant variant) {
|
||||
this.variant = variant != null ? variant : ButtonVariant.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installUI(JComponent c) {
|
||||
super.installUI(c);
|
||||
AbstractButton b = (AbstractButton) c;
|
||||
b.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
hover = true;
|
||||
b.repaint();
|
||||
}
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
hover = false;
|
||||
b.repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics g, JComponent c) {
|
||||
AbstractButton b = (AbstractButton) c;
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
int w = c.getWidth();
|
||||
int h = c.getHeight();
|
||||
|
||||
ButtonModel model = b.getModel();
|
||||
boolean pressed = model.isArmed() && model.isPressed();
|
||||
boolean enabled = b.isEnabled();
|
||||
|
||||
UITheme t = ThemeManager.getTheme();
|
||||
Color bg = enabled ? ThemeManager.getButtonBg(variant, t, hover, pressed) : (t == UITheme.DARK ? new Color(40, 40, 40) : new Color(220, 220, 220));
|
||||
Color fg = enabled ? ThemeManager.getButtonFg(variant, t) : ThemeManager.getFgDisabled(t);
|
||||
|
||||
// Background
|
||||
g2.setColor(bg);
|
||||
g2.fillRoundRect(0, 0, w, h, 6, 6);
|
||||
|
||||
// Border
|
||||
if (variant == ButtonVariant.DEFAULT || variant == ButtonVariant.CANCEL) {
|
||||
g2.setColor(ThemeManager.getBorder(t));
|
||||
g2.drawRoundRect(0, 0, w - 1, h - 1, 6, 6);
|
||||
}
|
||||
|
||||
// Text / Icon
|
||||
FontMetrics fm = g2.getFontMetrics(b.getFont());
|
||||
String text = b.getText();
|
||||
if (text != null && !text.isEmpty()) {
|
||||
g2.setColor(fg);
|
||||
g2.setFont(b.getFont());
|
||||
int tx = (w - fm.stringWidth(text)) / 2;
|
||||
int ty = (h - fm.getHeight()) / 2 + fm.getAscent();
|
||||
g2.drawString(text, tx, ty);
|
||||
}
|
||||
|
||||
g2.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledMenuBarUI extends BasicMenuBarUI {
|
||||
@Override
|
||||
public void paint(Graphics g, JComponent c) {
|
||||
g.setColor(ThemeManager.getMenuBarBg());
|
||||
g.fillRect(0, 0, c.getWidth(), c.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledMenuUI extends BasicMenuUI {
|
||||
@Override
|
||||
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
|
||||
ButtonModel model = menuItem.getModel();
|
||||
if (model.isArmed() || model.isSelected()) {
|
||||
g.setColor(ThemeManager.getMenuItemHoverBg());
|
||||
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
|
||||
} else {
|
||||
g.setColor(ThemeManager.getMenuBarBg());
|
||||
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
|
||||
ButtonModel model = menuItem.getModel();
|
||||
if (model.isArmed() || model.isSelected()) {
|
||||
g.setColor(ThemeManager.getMenuItemHoverFg());
|
||||
} else {
|
||||
g.setColor(ThemeManager.getMenuBarFg());
|
||||
}
|
||||
super.paintText(g, menuItem, textRect, text);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledMenuItemUI extends BasicMenuItemUI {
|
||||
@Override
|
||||
protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor) {
|
||||
ButtonModel model = menuItem.getModel();
|
||||
if (model.isArmed() || model.isSelected()) {
|
||||
g.setColor(ThemeManager.getSelectionBg());
|
||||
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
|
||||
} else {
|
||||
g.setColor(ThemeManager.getMenuPopupBg());
|
||||
g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text) {
|
||||
ButtonModel model = menuItem.getModel();
|
||||
if (model.isArmed() || model.isSelected()) {
|
||||
g.setColor(ThemeManager.getSelectionFg());
|
||||
} else {
|
||||
g.setColor(ThemeManager.getMenuItemFg());
|
||||
}
|
||||
super.paintText(g, menuItem, textRect, text);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledPopupMenuUI extends BasicPopupMenuUI {
|
||||
@Override
|
||||
public void paint(Graphics g, JComponent c) {
|
||||
g.setColor(ThemeManager.getMenuPopupBg());
|
||||
g.fillRect(0, 0, c.getWidth(), c.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
public static class StyledTabbedPaneUI extends BasicTabbedPaneUI {
|
||||
@Override
|
||||
protected void installDefaults() {
|
||||
super.installDefaults();
|
||||
tabInsets = new Insets(6, 16, 6, 16);
|
||||
selectedTabPadInsets = new Insets(2, 2, 2, 2);
|
||||
tabAreaInsets = new Insets(4, 4, 0, 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintTabBackground(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) {
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setColor(isSelected ? ThemeManager.getTabSelectedBg() : ThemeManager.getTabBg());
|
||||
g2.fillRoundRect(x, y, w, h + 4, 6, 6);
|
||||
g2.setColor(ThemeManager.getBorder());
|
||||
g2.drawRoundRect(x, y, w - 1, h + 4, 6, 6);
|
||||
g2.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintText(Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected) {
|
||||
g.setFont(font);
|
||||
g.setColor(isSelected ? ThemeManager.getTabSelectedFg() : ThemeManager.getTabFg());
|
||||
g.drawString(title, textRect.x, textRect.y + metrics.getAscent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
|
||||
int width = tabPane.getWidth();
|
||||
int height = tabPane.getHeight();
|
||||
Insets insets = tabPane.getInsets();
|
||||
int x = insets.left;
|
||||
int y = insets.top + calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
|
||||
int w = width - insets.right - insets.left;
|
||||
int h = height - insets.top - insets.bottom - calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
|
||||
|
||||
g.setColor(ThemeManager.getTabSelectedBg());
|
||||
g.fillRect(x, y, w, h);
|
||||
g.setColor(ThemeManager.getBorder());
|
||||
g.drawRect(x, y, w - 1, h - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
/**
|
||||
* Supported UI themes for the j3270 Java desktop interface.
|
||||
*/
|
||||
public enum UITheme {
|
||||
DARK("Dark Mode"),
|
||||
LIGHT("Light Mode");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
UITheme(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static UITheme fromString(String name) {
|
||||
if (name == null) return DARK;
|
||||
String s = name.trim().toUpperCase();
|
||||
if ("LIGHT".equals(s) || "LIGHT MODE".equals(s) || "LIGHT_MODE".equals(s)) {
|
||||
return LIGHT;
|
||||
}
|
||||
return DARK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Modal dialog for inspecting and confirming untrusted TLS/SSL server certificates.
|
||||
*/
|
||||
public class UntrustedCertificateDialog extends JDialog {
|
||||
|
||||
private boolean accepted = false;
|
||||
|
||||
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
super(parent, "Untrusted SSL/TLS Certificate", true);
|
||||
buildUI(host, port, chain, exception);
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
setResizable(false);
|
||||
}
|
||||
|
||||
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
|
||||
// Header
|
||||
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
|
||||
headerPanel.setOpaque(false);
|
||||
JLabel iconLabel = new JLabel("⚠️");
|
||||
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
|
||||
headerPanel.add(iconLabel);
|
||||
|
||||
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
|
||||
titleBox.setOpaque(false);
|
||||
JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
|
||||
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
|
||||
titleLabel.setForeground(ThemeManager.getOiaFgWarn());
|
||||
titleBox.add(titleLabel);
|
||||
|
||||
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
|
||||
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
subtitleLabel.setForeground(ThemeManager.getFgMuted());
|
||||
titleBox.add(subtitleLabel);
|
||||
headerPanel.add(titleBox);
|
||||
mainPanel.add(headerPanel, BorderLayout.NORTH);
|
||||
|
||||
// Certificate Details Area
|
||||
X509Certificate cert = (chain != null && chain.length > 0) ? chain[0] : null;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (exception != null) {
|
||||
sb.append("Validation Error:\n ").append(exception.getMessage() != null ? exception.getMessage() : exception.toString()).append("\n\n");
|
||||
}
|
||||
if (cert != null) {
|
||||
sb.append("Subject:\n ").append(cert.getSubjectX500Principal().getName()).append("\n\n");
|
||||
sb.append("Issuer:\n ").append(cert.getIssuerX500Principal().getName()).append("\n\n");
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||
sb.append("Validity:\n From: ").append(sdf.format(cert.getNotBefore()))
|
||||
.append("\n To: ").append(sdf.format(cert.getNotAfter())).append("\n\n");
|
||||
sb.append("Serial Number:\n ").append(cert.getSerialNumber().toString(16).toUpperCase()).append("\n\n");
|
||||
sb.append("SHA-256 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-256")).append("\n\n");
|
||||
sb.append("SHA-1 Fingerprint:\n ").append(computeFingerprint(cert, "SHA-1"));
|
||||
} else {
|
||||
sb.append("No peer certificate information available.");
|
||||
}
|
||||
|
||||
JTextArea detailsArea = new JTextArea(sb.toString());
|
||||
detailsArea.setEditable(false);
|
||||
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
ThemeManager.styleTextArea(detailsArea);
|
||||
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(detailsArea);
|
||||
scrollPane.setPreferredSize(new Dimension(520, 260));
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel Connection");
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
accepted = false;
|
||||
dispose();
|
||||
});
|
||||
|
||||
JButton trustBtn = new JButton("Connect Anyway");
|
||||
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||
trustBtn.addActionListener(e -> {
|
||||
accepted = true;
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(trustBtn);
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
getRootPane().setDefaultButton(trustBtn);
|
||||
}
|
||||
|
||||
public boolean isAccepted() {
|
||||
return accepted;
|
||||
}
|
||||
|
||||
private static String computeFingerprint(X509Certificate cert, String algorithm) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(algorithm);
|
||||
byte[] digest = md.digest(cert.getEncoded());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < digest.length; i++) {
|
||||
if (i > 0) sb.append(':');
|
||||
sb.append(String.format("%02X", digest[i]));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "Unable to compute fingerprint: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the prompt modal dialog on the Swing EDT and return user choice.
|
||||
*/
|
||||
public static boolean showPrompt(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
|
||||
dialog.setVisible(true);
|
||||
return dialog.isAccepted();
|
||||
} else {
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
UntrustedCertificateDialog dialog = new UntrustedCertificateDialog(parent, host, port, chain, exception);
|
||||
dialog.setVisible(true);
|
||||
result.set(dialog.isAccepted());
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
return result.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,521 +0,0 @@
|
||||
package org.pubvm.j3270;
|
||||
|
||||
import org.lib3270j.*;
|
||||
import org.lib3270j.listener.ConnectionListener;
|
||||
import org.lib3270j.listener.ScreenUpdateListener;
|
||||
import org.pubvm.j3270.ui.ConnectDialog;
|
||||
import org.pubvm.j3270.ui.StatusBar;
|
||||
import org.pubvm.j3270.ui.TerminalPanel;
|
||||
import org.pubvm.j3270.ft.FileTransfer;
|
||||
import org.pubvm.j3270.ft.FileTransferDialog;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.*;
|
||||
|
||||
/**
|
||||
* j3270 — Java TN3270 Terminal Emulator.
|
||||
* Main application class. Creates the main window with terminal display,
|
||||
* status bar, menu bar, and manages the Telnet3270Client lifecycle.
|
||||
*/
|
||||
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
|
||||
|
||||
private static final Logger log = Logger.getLogger(J3270App.class.getName());
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
private StatusBar statusBar;
|
||||
private Timer refreshTimer;
|
||||
|
||||
// Last connection details for reconnect
|
||||
private String lastHost = "";
|
||||
private int lastPort = 23;
|
||||
|
||||
private FileTransfer fileTransfer;
|
||||
|
||||
public J3270App() {
|
||||
super("j3270 — Java TN3270 Terminal Emulator");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setBackground(new Color(10, 10, 10));
|
||||
|
||||
buildUI();
|
||||
buildMenuBar();
|
||||
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
setMinimumSize(new Dimension(640, 400));
|
||||
|
||||
// Status refresh timer — also ensures focus stays on terminal
|
||||
refreshTimer = new Timer(100, e -> {
|
||||
if (client != null) {
|
||||
statusBar.updateStatus();
|
||||
// Keep focus on terminal panel when window is active
|
||||
if (isActive() && !terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
refreshTimer.start();
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
disconnect();
|
||||
if (refreshTimer != null)
|
||||
refreshTimer.stop();
|
||||
terminalPanel.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
// When window gains focus, push to terminal panel
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
terminalPanel = new TerminalPanel();
|
||||
|
||||
// Status bar
|
||||
statusBar = new StatusBar();
|
||||
|
||||
// Layout
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().setBackground(new Color(10, 10, 10));
|
||||
getContentPane().add(terminalPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(statusBar, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private void buildMenuBar() {
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
menuBar.setBackground(new Color(30, 30, 30));
|
||||
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
|
||||
|
||||
// File menu
|
||||
JMenu fileMenu = createMenu("File");
|
||||
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
||||
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
||||
disconnect();
|
||||
System.exit(0);
|
||||
}));
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
// View menu
|
||||
JMenu viewMenu = createMenu("View");
|
||||
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
|
||||
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
|
||||
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
|
||||
terminalPanel.setFontSize(16);
|
||||
terminalPanel.guardedPack();
|
||||
}));
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
// Actions menu
|
||||
JMenu actionsMenu = createMenu("Actions");
|
||||
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_L, () -> {
|
||||
if (client != null)
|
||||
client.sendClear();
|
||||
terminalPanel.repaint();
|
||||
}));
|
||||
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
|
||||
if (client != null)
|
||||
client.reset();
|
||||
terminalPanel.repaint();
|
||||
}));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
// Help menu
|
||||
JMenu helpMenu = createMenu("Help");
|
||||
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
|
||||
helpMenu.add(createMenuItem("About", -1, this::showAbout));
|
||||
menuBar.add(helpMenu);
|
||||
|
||||
setJMenuBar(menuBar);
|
||||
}
|
||||
|
||||
private JMenu createMenu(String name) {
|
||||
JMenu menu = new JMenu(name);
|
||||
menu.setForeground(new Color(200, 200, 200));
|
||||
return menu;
|
||||
}
|
||||
|
||||
private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) {
|
||||
JMenuItem item = new JMenuItem(text);
|
||||
item.setBackground(new Color(40, 40, 40));
|
||||
item.setForeground(new Color(200, 200, 200));
|
||||
if (acceleratorKey > 0) {
|
||||
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey,
|
||||
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
|
||||
}
|
||||
item.addActionListener(e -> action.run());
|
||||
return item;
|
||||
}
|
||||
|
||||
private void showFileTransferDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
|
||||
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileTransfer == null) {
|
||||
fileTransfer = new FileTransfer(client, new FileTransfer.FileTransferCallback() {
|
||||
private ProgressMonitor monitor;
|
||||
|
||||
@Override
|
||||
public void onTransferStarted() {
|
||||
monitor = new ProgressMonitor(J3270App.this, "Starting IND$FILE transfer...",
|
||||
"Waiting for host...", 0, 100);
|
||||
monitor.setMillisToDecideToPopup(0);
|
||||
monitor.setMillisToPopup(0);
|
||||
|
||||
// Start a timer to check for manual cancellation
|
||||
Timer cancelCheckTimer = new Timer(500, e -> {
|
||||
if (monitor != null && monitor.isCanceled() && fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
((Timer)e.getSource()).stop();
|
||||
}
|
||||
});
|
||||
cancelCheckTimer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferRunning() {
|
||||
if (monitor != null) {
|
||||
monitor.setNote("Transferring data...");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBytesTransferred(long bytes) {
|
||||
if (monitor != null) {
|
||||
monitor.setNote(bytes + " bytes transferred");
|
||||
// We don't always know max size in IND$FILE, so we just pulse/update note
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferComplete(String message) {
|
||||
if (monitor != null) monitor.close();
|
||||
monitor = null;
|
||||
JOptionPane.showMessageDialog(J3270App.this, message, "Transfer Complete", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransferAborted(String error) {
|
||||
if (monitor != null) monitor.close();
|
||||
monitor = null;
|
||||
JOptionPane.showMessageDialog(J3270App.this, "Transfer Failed: " + error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FileTransferDialog dialog = new FileTransferDialog(this, fileTransfer);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
// ========== Connection management ==========
|
||||
|
||||
private void showConnectDialog() {
|
||||
ConnectDialog dialog = new ConnectDialog(this);
|
||||
dialog.setInitialHost(lastHost);
|
||||
dialog.setInitialPort(lastPort);
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed()) {
|
||||
ConnectionConfig config = dialog.getConnectionConfig();
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
connect(config);
|
||||
}
|
||||
}
|
||||
|
||||
private void showSettingsDialog() {
|
||||
org.pubvm.j3270.ui.SettingsDialog dialog = new org.pubvm.j3270.ui.SettingsDialog(this);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
public TerminalPanel getTerminalPanel() {
|
||||
return terminalPanel;
|
||||
}
|
||||
|
||||
void connect(ConnectionConfig config) {
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
|
||||
// Save for auto-connect
|
||||
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
|
||||
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
|
||||
|
||||
// Disconnect existing connection
|
||||
disconnect();
|
||||
|
||||
client = new Telnet3270Client(config);
|
||||
client.addConnectionListener(this);
|
||||
client.addScreenUpdateListener(this);
|
||||
|
||||
terminalPanel.setClient(client);
|
||||
statusBar.setClient(client);
|
||||
|
||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
|
||||
|
||||
// Resize window to match model
|
||||
terminalPanel.guardedPack();
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.connect();
|
||||
} catch (IOException e) {
|
||||
log.log(Level.WARNING, "Connection failed", e);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Connection failed: " + e.getMessage(),
|
||||
"Connection Error", JOptionPane.ERROR_MESSAGE);
|
||||
});
|
||||
}
|
||||
}, "Connect-Thread").start();
|
||||
|
||||
// Focus the terminal panel
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
if (client != null) {
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
client.disconnect();
|
||||
client = null;
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null);
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustFontSize(int delta) {
|
||||
int current = terminalPanel.getFontSize();
|
||||
int newSize = Math.max(8, Math.min(72, current + delta));
|
||||
terminalPanel.setFontSize(newSize);
|
||||
// Pack after font change — setFontSize sets the resize guard
|
||||
// to prevent the componentResized from re-triggering autoFitFont
|
||||
terminalPanel.guardedPack();
|
||||
}
|
||||
|
||||
// ========== ConnectionListener ==========
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
log.info("Connection state: " + oldState + " -> " + newState);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusBar.updateStatus();
|
||||
terminalPanel.repaint();
|
||||
|
||||
// Auto-resize and focus on first full session
|
||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||
terminalPanel.guardedPack();
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionError(String message) {
|
||||
log.warning("Connection error: " + message);
|
||||
SwingUtilities.invokeLater(statusBar::updateStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTN3270ENegotiated(String deviceType, String deviceName) {
|
||||
log.info("TN3270E negotiated: type=" + deviceType + " name=" + deviceName);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (deviceName != null) {
|
||||
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== ScreenUpdateListener ==========
|
||||
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// During an active file transfer, let the CUT/DFT handler drive
|
||||
// keyboard state. In x3270, ft_cut_data() runs before WCC
|
||||
// keyboard-restore is applied — the keyboard stays locked for the
|
||||
// entire CUT transfer.
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.onScreenUpdated();
|
||||
}
|
||||
if (client != null && (fileTransfer == null || !fileTransfer.isTransferActive())) {
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoundAlarm() {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
terminalPanel.guardedPack();
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Help dialogs ==========
|
||||
|
||||
private void showKeyMappings() {
|
||||
String text = "Key Mappings:\n\n" +
|
||||
"Enter — Send Enter (AID)\n" +
|
||||
"F1-F12 — PF1-PF12\n" +
|
||||
"Shift+F1-F12 — PF13-PF24\n" +
|
||||
"Alt+1/2/3 — PA1/PA2/PA3\n" +
|
||||
"Tab — Tab to next field\n" +
|
||||
"Shift+Tab — Back-tab\n" +
|
||||
"Arrow keys — Cursor movement\n" +
|
||||
"Home — Cursor to home\n" +
|
||||
"End — Erase to end of field\n" +
|
||||
"Delete — Delete character\n" +
|
||||
"Backspace — Backspace\n" +
|
||||
"Insert — Toggle insert mode\n" +
|
||||
"Escape — Reset\n" +
|
||||
"PageUp/Down — PF7/PF8\n" +
|
||||
"Alt+C — Clear\n" +
|
||||
"Alt+1/2/3 — PA1/PA2/PA3\n" +
|
||||
"Cmd+D — Disconnect\n" +
|
||||
"Cmd+Q — Quit\n" +
|
||||
"Cmd+=/-/0 — Font size +/-/reset";
|
||||
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
area.setBackground(new Color(30, 30, 30));
|
||||
area.setForeground(new Color(200, 200, 200));
|
||||
JScrollPane sp = new JScrollPane(area);
|
||||
sp.setPreferredSize(new Dimension(360, 400));
|
||||
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
private void showAbout() {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"j3270 — Java TN3270 Terminal Emulator\n\n" +
|
||||
"A Java reimplementation of x3270.\n" +
|
||||
"lib3270j v0.1.0\n\n" +
|
||||
"Supports: TN3270, TN3270E (RFC 2355)\n" +
|
||||
"Models: IBM 3278/3279 Models 2-5\n" +
|
||||
"Colors, Extended Attributes, Query Replies",
|
||||
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
// ========== Main ==========
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean debug = false;
|
||||
String configFile = null;
|
||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
|
||||
debug = true;
|
||||
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
|
||||
configFile = args[++i];
|
||||
} else {
|
||||
remainingArgs.add(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure logging over the absolute root logger
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
}
|
||||
globalRoot.setLevel(debug ? Level.ALL : Level.WARNING);
|
||||
ConsoleHandler handler = new ConsoleHandler();
|
||||
handler.setLevel(Level.ALL);
|
||||
handler.setFormatter(new SimpleFormatter());
|
||||
globalRoot.addHandler(handler);
|
||||
|
||||
// Load INI config file if specified
|
||||
if (configFile != null) {
|
||||
try {
|
||||
org.pubvm.j3270.config.Settings.loadFromIniFile(configFile);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to load config file: " + configFile);
|
||||
System.err.println(e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Dark look and feel
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception e) {
|
||||
log.fine("Could not set system look and feel");
|
||||
}
|
||||
|
||||
// Use native macOS menu bar if applicable
|
||||
System.setProperty("apple.laf.useScreenMenuBar", "true");
|
||||
System.setProperty("apple.awt.application.name", "j3270");
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
J3270App app = new J3270App();
|
||||
app.setVisible(true);
|
||||
|
||||
// If host:port given on command line, connect directly
|
||||
if (!remainingArgs.isEmpty()) {
|
||||
String host = remainingArgs.get(0);
|
||||
int port = 23;
|
||||
if (remainingArgs.size() >= 2) {
|
||||
try {
|
||||
port = Integer.parseInt(remainingArgs.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
}
|
||||
}
|
||||
TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
if (remainingArgs.size() >= 3) {
|
||||
try {
|
||||
int modelNum = Integer.parseInt(remainingArgs.get(2));
|
||||
model = TerminalModel.forModel(modelNum, true);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, model);
|
||||
app.connect(config);
|
||||
} else {
|
||||
org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings
|
||||
.getStartupBehavior();
|
||||
switch (behavior) {
|
||||
case SHOW_CONNECT:
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
break;
|
||||
case AUTO_CONNECT:
|
||||
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
|
||||
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
|
||||
if (host != null && !host.isEmpty()) {
|
||||
app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4));
|
||||
} else {
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
}
|
||||
break;
|
||||
case DO_NOTHING:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
|
||||
import org.lib3270j.ConnectionConfig;
|
||||
import org.lib3270j.TerminalModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Connection dialog for entering host, port, model, and LU name.
|
||||
*/
|
||||
public class ConnectDialog extends JDialog {
|
||||
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JComboBox<TerminalModel> modelCombo;
|
||||
private JTextField luField;
|
||||
private boolean confirmed;
|
||||
private ConnectionConfig result;
|
||||
|
||||
public ConnectDialog(Frame parent) {
|
||||
super(parent, "Connect to Host", true);
|
||||
buildUI();
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
setResizable(false);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new GridBagLayout());
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
mainPanel.setBackground(new Color(30, 30, 30));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(4, 4, 4, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14);
|
||||
Color fg = new Color(200, 200, 200);
|
||||
|
||||
// Host
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel hostLabel = new JLabel("Host:");
|
||||
hostLabel.setForeground(fg);
|
||||
hostLabel.setFont(labelFont);
|
||||
mainPanel.add(hostLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
hostField = createDarkField(20);
|
||||
mainPanel.add(hostField, gbc);
|
||||
|
||||
// Port
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.weightx = 0;
|
||||
JLabel portLabel = new JLabel("Port:");
|
||||
portLabel.setForeground(fg);
|
||||
portLabel.setFont(labelFont);
|
||||
mainPanel.add(portLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
portField = createDarkField(6);
|
||||
portField.setText("23");
|
||||
mainPanel.add(portField, gbc);
|
||||
|
||||
// Model
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.weightx = 0;
|
||||
JLabel modelLabel = new JLabel("Model:");
|
||||
modelLabel.setForeground(fg);
|
||||
modelLabel.setFont(labelFont);
|
||||
mainPanel.add(modelLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
modelCombo = new JComboBox<>(TerminalModel.values());
|
||||
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
|
||||
modelCombo.setBackground(new Color(45, 45, 45));
|
||||
modelCombo.setForeground(fg);
|
||||
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
mainPanel.add(modelCombo, gbc);
|
||||
|
||||
// LU Name
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 3;
|
||||
gbc.weightx = 0;
|
||||
JLabel luLabel = new JLabel("LU Name:");
|
||||
luLabel.setForeground(fg);
|
||||
luLabel.setFont(labelFont);
|
||||
mainPanel.add(luLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
luField = createDarkField(12);
|
||||
mainPanel.add(luField, gbc);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||
|
||||
JButton connectBtn = new JButton("Connect");
|
||||
connectBtn.setBackground(new Color(50, 120, 50));
|
||||
connectBtn.setForeground(Color.WHITE);
|
||||
connectBtn.setFont(labelFont);
|
||||
connectBtn.addActionListener(e -> onConnect());
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel");
|
||||
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||
cancelBtn.setForeground(fg);
|
||||
cancelBtn.setFont(labelFont);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
confirmed = false;
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(connectBtn);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridwidth = 2;
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
|
||||
// Enter key triggers connect
|
||||
getRootPane().setDefaultButton(connectBtn);
|
||||
}
|
||||
|
||||
private JTextField createDarkField(int cols) {
|
||||
JTextField field = new JTextField(cols);
|
||||
field.setBackground(new Color(45, 45, 45));
|
||||
field.setForeground(new Color(200, 200, 200));
|
||||
field.setCaretColor(new Color(200, 200, 200));
|
||||
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
|
||||
field.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
|
||||
return field;
|
||||
}
|
||||
|
||||
private void onConnect() {
|
||||
String host = hostField.getText().trim();
|
||||
if (host.isEmpty()) {
|
||||
hostField.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
int port;
|
||||
try {
|
||||
port = Integer.parseInt(portField.getText().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
portField.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem());
|
||||
String lu = luField.getText().trim();
|
||||
if (!lu.isEmpty()) {
|
||||
result.setLuName(lu);
|
||||
}
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
public ConnectionConfig getConnectionConfig() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Set initial values. */
|
||||
public void setInitialHost(String host) {
|
||||
hostField.setText(host);
|
||||
}
|
||||
|
||||
public void setInitialPort(int port) {
|
||||
portField.setText(String.valueOf(port));
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
|
||||
import org.lib3270j.ConnectionState;
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Status bar displaying connection state, cursor position, timing, and lock
|
||||
* status.
|
||||
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
|
||||
*/
|
||||
public class StatusBar extends JPanel {
|
||||
|
||||
private final JLabel connectionStatus;
|
||||
private final JLabel cursorPosition;
|
||||
private final JLabel luName;
|
||||
private final JLabel lockStatus;
|
||||
private final JLabel modelInfo;
|
||||
|
||||
private Telnet3270Client client;
|
||||
|
||||
// OIA colors
|
||||
private static final Color OIA_BG = new Color(20, 20, 20);
|
||||
private static final Color OIA_FG = new Color(50, 205, 50);
|
||||
private static final Color OIA_DIM = new Color(80, 80, 80);
|
||||
private static final Color OIA_ALERT = new Color(255, 80, 80);
|
||||
|
||||
public StatusBar() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
setBackground(OIA_BG);
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40)));
|
||||
setPreferredSize(new Dimension(800, 22));
|
||||
|
||||
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
|
||||
luName = createLabel("", oiaFont, OIA_FG);
|
||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
|
||||
|
||||
add(Box.createHorizontalStrut(6));
|
||||
add(connectionStatus);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(luName);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(lockStatus);
|
||||
add(Box.createHorizontalGlue());
|
||||
add(modelInfo);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(cursorPosition);
|
||||
add(Box.createHorizontalStrut(6));
|
||||
}
|
||||
|
||||
private JLabel createLabel(String text, Font font, Color fg) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.setFont(font);
|
||||
label.setForeground(fg);
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
if (client == null)
|
||||
return;
|
||||
|
||||
// Connection state
|
||||
ConnectionState state = client.getConnectionState();
|
||||
switch (state) {
|
||||
case NOT_CONNECTED:
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
break;
|
||||
case TCP_PENDING:
|
||||
case TELNET_PENDING:
|
||||
connectionStatus.setText("Connecting...");
|
||||
connectionStatus.setForeground(OIA_ALERT);
|
||||
break;
|
||||
case CONNECTED_3270:
|
||||
connectionStatus.setText("TN3270");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
break;
|
||||
case CONNECTED_TN3270E:
|
||||
connectionStatus.setText("TN3270E");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
break;
|
||||
case CONNECTED_SSCP:
|
||||
connectionStatus.setText("SSCP-LU");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
break;
|
||||
case CONNECTED_NVT:
|
||||
case CONNECTED_NVT_CHAR:
|
||||
case CONNECTED_E_NVT:
|
||||
connectionStatus.setText("NVT");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
break;
|
||||
case CONNECTED_UNBOUND:
|
||||
connectionStatus.setText("Unbound");
|
||||
connectionStatus.setForeground(new Color(255, 255, 80));
|
||||
break;
|
||||
default:
|
||||
connectionStatus.setText(state.name());
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
break;
|
||||
}
|
||||
|
||||
// LU name
|
||||
String lu = "";
|
||||
if (client.getConnectionState().isTn3270e()) {
|
||||
// lu would come from FSM
|
||||
}
|
||||
luName.setText(lu);
|
||||
|
||||
// Lock status
|
||||
if (client.getInputProcessor().isKeyboardLocked()) {
|
||||
lockStatus.setText("X SYSTEM");
|
||||
lockStatus.setForeground(OIA_ALERT);
|
||||
} else if (client.getInputProcessor().isInsertMode()) {
|
||||
lockStatus.setText("INSERT");
|
||||
lockStatus.setForeground(OIA_FG);
|
||||
} else {
|
||||
lockStatus.setText("");
|
||||
}
|
||||
|
||||
// Model info
|
||||
modelInfo.setText(client.getConfig().getModel().getTerminalType());
|
||||
|
||||
// Cursor position
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int row = sb.getCursorRow() + 1;
|
||||
int col = sb.getCursorCol() + 1;
|
||||
cursorPosition.setText(String.format("%03d/%03d", row, col));
|
||||
}
|
||||
}
|
||||
@@ -1,991 +0,0 @@
|
||||
package org.pubvm.j3270.ui;
|
||||
|
||||
import org.lib3270j.Telnet3270Client;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Custom JPanel that renders the 3270 screen buffer.
|
||||
* Supports colors, bold, underline, reverse, blink, and all 3278/3279
|
||||
* attributes.
|
||||
*/
|
||||
public class TerminalPanel extends JPanel {
|
||||
|
||||
private Telnet3270Client client;
|
||||
|
||||
// Font and cell dimensions
|
||||
private Font terminalFont;
|
||||
private int cellWidth;
|
||||
private int cellHeight;
|
||||
private int fontAscent;
|
||||
private int fontDescent;
|
||||
private int padding = 4;
|
||||
private int currentFontSize = 16;
|
||||
|
||||
// Cursor state
|
||||
private boolean cursorVisible = true;
|
||||
private Timer blinkTimer;
|
||||
|
||||
// Host color mapping
|
||||
private final Color[] hostColors = new Color[16];
|
||||
|
||||
// 3278 monochrome colors (based on field attributes)
|
||||
private Color monoNormal;
|
||||
private Color monoIntensify;
|
||||
private Color monoProtected;
|
||||
private Color monoProtectedHigh;
|
||||
|
||||
// Background
|
||||
private Color bgColor;
|
||||
|
||||
// ========== Selection / Copy-Paste state ==========
|
||||
private boolean blockSelectMode = false;
|
||||
private int selectionStartRow = -1, selectionStartCol = -1;
|
||||
private int selectionEndRow = -1, selectionEndCol = -1;
|
||||
private boolean isDragging = false;
|
||||
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
|
||||
|
||||
// ========== Resize guard ==========
|
||||
private boolean resizeGuard = false;
|
||||
|
||||
/**
|
||||
* Compute the horizontal render offset to center the grid within the panel.
|
||||
* Any leftover pixels (from integer font sizing) are split evenly.
|
||||
*/
|
||||
private int getRenderOffsetX() {
|
||||
int termCols = 80;
|
||||
if (client != null) termCols = client.getScreenBuffer().getCols();
|
||||
int gridW = termCols * cellWidth;
|
||||
int extra = getWidth() - gridW;
|
||||
return Math.max(padding, extra / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the vertical render offset to center the grid within the panel.
|
||||
*/
|
||||
private int getRenderOffsetY() {
|
||||
int termRows = 24;
|
||||
if (client != null) termRows = client.getScreenBuffer().getRows();
|
||||
int gridH = termRows * cellHeight;
|
||||
int extra = getHeight() - gridH;
|
||||
return Math.max(padding, extra / 2);
|
||||
}
|
||||
|
||||
// Default Host color mapping
|
||||
public static final Color[] DEFAULT_HOST_COLORS = {
|
||||
new Color(0, 0, 0), // 0: Neutral Black
|
||||
new Color(80, 120, 255), // 1: Blue
|
||||
new Color(255, 50, 50), // 2: Red
|
||||
new Color(255, 130, 180), // 3: Pink
|
||||
new Color(50, 205, 50), // 4: Green
|
||||
new Color(64, 224, 208), // 5: Turquoise
|
||||
new Color(255, 255, 80), // 6: Yellow
|
||||
new Color(255, 255, 255), // 7: Neutral White
|
||||
new Color(0, 0, 0), // 8: Black
|
||||
new Color(30, 60, 180), // 9: Deep Blue
|
||||
new Color(255, 165, 0), // 10: Orange
|
||||
new Color(180, 130, 255), // 11: Purple
|
||||
new Color(144, 238, 144), // 12: Pale Green
|
||||
new Color(175, 238, 238), // 13: Pale Turquoise
|
||||
new Color(170, 170, 170), // 14: Grey
|
||||
new Color(255, 255, 255), // 15: White
|
||||
};
|
||||
|
||||
// Default 3278 monochrome colors
|
||||
public static final Color DEFAULT_MONO_NORMAL = new Color(50, 205, 50);
|
||||
public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 255, 255);
|
||||
public static final Color DEFAULT_MONO_PROTECTED = new Color(80, 120, 255);
|
||||
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
|
||||
|
||||
// Default Background
|
||||
public static final Color DEFAULT_BG_COLOR = new Color(10, 10, 10);
|
||||
|
||||
public TerminalPanel() {
|
||||
setupColors();
|
||||
setBackground(bgColor);
|
||||
setFocusable(true);
|
||||
setDoubleBuffered(true);
|
||||
setFocusTraversalKeysEnabled(false);
|
||||
|
||||
// Use key bindings instead of KeyListener for reliable key handling
|
||||
// This avoids focus/event issues with JScrollPane
|
||||
setOpaque(true);
|
||||
setupKeyBindings();
|
||||
setupFont();
|
||||
setupCursorBlink();
|
||||
|
||||
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
||||
|
||||
// Handle mouse clicks to position cursor, selection, and grab focus
|
||||
MouseAdapter mouseHandler = new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
requestFocusInWindow();
|
||||
|
||||
// Right-click context menu
|
||||
if (SwingUtilities.isRightMouseButton(e)) {
|
||||
showContextMenu(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
int col = (e.getX() - ox) / cellWidth;
|
||||
int row = (e.getY() - oy) / cellHeight;
|
||||
col = Math.max(0, Math.min(col, sb.getCols() - 1));
|
||||
row = Math.max(0, Math.min(row, sb.getRows() - 1));
|
||||
|
||||
// Start selection
|
||||
selectionStartRow = row;
|
||||
selectionStartCol = col;
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
isDragging = true;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (isDragging && client != null && client.getConnectionState().isFullSession()) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
int col = (e.getX() - ox) / cellWidth;
|
||||
int row = (e.getY() - oy) / cellHeight;
|
||||
col = Math.max(0, Math.min(col, sb.getCols() - 1));
|
||||
row = Math.max(0, Math.min(row, sb.getRows() - 1));
|
||||
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
// If start == end, treat as a click (position cursor, clear selection)
|
||||
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol);
|
||||
clearSelection();
|
||||
}
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
};
|
||||
addMouseListener(mouseHandler);
|
||||
addMouseMotionListener(mouseHandler);
|
||||
|
||||
// Handle resize: auto-fit font to window size
|
||||
addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
if (resizeGuard) return;
|
||||
autoFitFont();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Selection / Copy-Paste ==========
|
||||
|
||||
private void clearSelection() {
|
||||
selectionStartRow = -1;
|
||||
selectionStartCol = -1;
|
||||
selectionEndRow = -1;
|
||||
selectionEndCol = -1;
|
||||
}
|
||||
|
||||
private boolean hasSelection() {
|
||||
return selectionStartRow >= 0 && selectionEndRow >= 0 &&
|
||||
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected text from the screen buffer.
|
||||
* In block mode: rectangular selection with newlines between rows.
|
||||
* In line mode: stream selection, flowing left-to-right, top-to-bottom.
|
||||
*/
|
||||
public String getSelectedText() {
|
||||
if (!hasSelection() || client == null) return "";
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int cols = sb.getCols();
|
||||
|
||||
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
int c1, c2;
|
||||
|
||||
if (blockSelectMode) {
|
||||
// Block mode: rectangular selection
|
||||
c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int r = r1; r <= r2; r++) {
|
||||
if (r > r1) result.append('\n');
|
||||
for (int c = c1; c <= c2; c++) {
|
||||
int baddr = r * cols + c;
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
char ch = ea.ucs4;
|
||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||
result.append(' ');
|
||||
} else {
|
||||
result.append(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
} else {
|
||||
// Line/stream mode: flow from start to end
|
||||
int startAddr, endAddr;
|
||||
if (selectionStartRow < selectionEndRow ||
|
||||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
||||
startAddr = selectionStartRow * cols + selectionStartCol;
|
||||
endAddr = selectionEndRow * cols + selectionEndCol;
|
||||
} else {
|
||||
startAddr = selectionEndRow * cols + selectionEndCol;
|
||||
endAddr = selectionStartRow * cols + selectionStartCol;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
int prevRow = startAddr / cols;
|
||||
for (int addr = startAddr; addr <= endAddr; addr++) {
|
||||
int curRow = addr / cols;
|
||||
if (curRow != prevRow) {
|
||||
result.append('\n');
|
||||
prevRow = curRow;
|
||||
}
|
||||
ExtendedAttribute ea = sb.getCell(addr);
|
||||
char ch = ea.ucs4;
|
||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||
result.append(' ');
|
||||
} else {
|
||||
result.append(ch);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private void copySelection() {
|
||||
String text = getSelectedText();
|
||||
if (!text.isEmpty()) {
|
||||
StringSelection ss = new StringSelection(text);
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void pasteClipboard() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) return;
|
||||
try {
|
||||
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
.getData(DataFlavor.stringFlavor);
|
||||
if (text != null) {
|
||||
for (char ch : text.toCharArray()) {
|
||||
if (ch == '\n' || ch == '\r') {
|
||||
// Skip newlines in paste — user may paste multi-line text
|
||||
// but 3270 fields don't wrap the same way
|
||||
continue;
|
||||
}
|
||||
if (ch >= 0x20 && ch != 0x7F) {
|
||||
client.typeCharacter(ch);
|
||||
}
|
||||
}
|
||||
refreshScreen();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// Clipboard not available or wrong type — silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void showContextMenu(MouseEvent e) {
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
|
||||
JMenuItem copyItem = new JMenuItem("Copy");
|
||||
copyItem.setEnabled(hasSelection());
|
||||
copyItem.addActionListener(ev -> copySelection());
|
||||
popup.add(copyItem);
|
||||
|
||||
JMenuItem pasteItem = new JMenuItem("Paste");
|
||||
pasteItem.setEnabled(client != null && client.getConnectionState().isFullSession());
|
||||
pasteItem.addActionListener(ev -> pasteClipboard());
|
||||
popup.add(pasteItem);
|
||||
|
||||
popup.addSeparator();
|
||||
|
||||
JCheckBoxMenuItem blockModeItem = new JCheckBoxMenuItem("Block Select Mode", blockSelectMode);
|
||||
blockModeItem.addActionListener(ev -> {
|
||||
blockSelectMode = blockModeItem.isSelected();
|
||||
org.pubvm.j3270.config.Settings.setBlockSelectMode(blockSelectMode);
|
||||
});
|
||||
popup.add(blockModeItem);
|
||||
|
||||
popup.show(this, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
private boolean isCellSelected(int row, int col) {
|
||||
if (!hasSelection()) return false;
|
||||
|
||||
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
|
||||
if (blockSelectMode) {
|
||||
int c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
|
||||
} else {
|
||||
// Stream mode
|
||||
int cols = 80;
|
||||
if (client != null) cols = client.getScreenBuffer().getCols();
|
||||
int addr = row * cols + col;
|
||||
int startAddr, endAddr;
|
||||
if (selectionStartRow < selectionEndRow ||
|
||||
(selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) {
|
||||
startAddr = selectionStartRow * cols + selectionStartCol;
|
||||
endAddr = selectionEndRow * cols + selectionEndCol;
|
||||
} else {
|
||||
startAddr = selectionEndRow * cols + selectionEndCol;
|
||||
endAddr = selectionStartRow * cols + selectionStartCol;
|
||||
}
|
||||
return addr >= startAddr && addr <= endAddr;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Font auto-resize ==========
|
||||
|
||||
/**
|
||||
* Auto-fit the font size to fill the current panel dimensions
|
||||
* while respecting the terminal model's character grid.
|
||||
* Any leftover pixels are handled by centering the grid
|
||||
* (see getRenderOffsetX/Y).
|
||||
*/
|
||||
private void autoFitFont() {
|
||||
int panelW = getWidth();
|
||||
int panelH = getHeight();
|
||||
if (panelW <= 0 || panelH <= 0) return;
|
||||
|
||||
int termCols = 80;
|
||||
int termRows = 24;
|
||||
if (client != null) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
termCols = sb.getCols();
|
||||
termRows = sb.getRows();
|
||||
}
|
||||
|
||||
// Use minimal padding for the fit calculation
|
||||
int availW = panelW - 2 * padding;
|
||||
int availH = panelH - 2 * padding;
|
||||
if (availW <= 0 || availH <= 0) return;
|
||||
|
||||
// Find the largest font size where the grid fits
|
||||
int bestSize = 8;
|
||||
for (int testSize = 8; testSize <= 72; testSize++) {
|
||||
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
|
||||
FontMetrics fm = getFontMetrics(testFont);
|
||||
int testCellW = fm.charWidth('M');
|
||||
int testCellH = fm.getHeight();
|
||||
|
||||
if (testCellW * termCols <= availW && testCellH * termRows <= availH) {
|
||||
bestSize = testSize;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestSize != currentFontSize) {
|
||||
currentFontSize = bestSize;
|
||||
org.pubvm.j3270.config.Settings.setFontSize(bestSize);
|
||||
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
||||
updateCellSize();
|
||||
}
|
||||
// No window snap — any leftover pixels are centered via getRenderOffsetX/Y
|
||||
repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by J3270App when it needs to pack the frame (screen size changed,
|
||||
* connection established, etc.). Sets the resize guard to prevent
|
||||
* autoFitFont from firing during the pack.
|
||||
*/
|
||||
public void guardedPack() {
|
||||
resizeGuard = true;
|
||||
revalidate();
|
||||
Container top = getTopLevelAncestor();
|
||||
if (top instanceof java.awt.Window) {
|
||||
((java.awt.Window) top).pack();
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use InputMap/ActionMap (key bindings) instead of KeyListener.
|
||||
* This works reliably even inside a JScrollPane — the WHEN_FOCUSED
|
||||
* condition ensures our panel receives all key events when focused.
|
||||
*/
|
||||
private void bindKeyToMap(InputMap im, String action, String bindingStr) {
|
||||
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Support multiple bindings separated by commas
|
||||
String[] bindings = bindingStr.split(",");
|
||||
for (String binding : bindings) {
|
||||
binding = binding.trim();
|
||||
if (!binding.isEmpty() && !"UNBOUND".equals(binding)) {
|
||||
KeyStroke ks = KeyStroke.getKeyStroke(binding);
|
||||
if (ks != null) {
|
||||
im.put(ks, "j3270-" + action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupKeyBindings() {
|
||||
InputMap im = getInputMap(JComponent.WHEN_FOCUSED);
|
||||
ActionMap am = getActionMap();
|
||||
|
||||
im.clear();
|
||||
am.clear();
|
||||
|
||||
// Block the scroll pane from handling Tab, arrows, Page keys
|
||||
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
||||
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
||||
for (String key : navKeys) {
|
||||
String binding = org.pubvm.j3270.config.Settings.getKeyBinding(key, key);
|
||||
bindKeyToMap(im, key, binding);
|
||||
}
|
||||
|
||||
// PF keys 1-24
|
||||
// PF1-12 default to F1-F12, PF13-24 default to shift F1-shift F12
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
String defaultBinding;
|
||||
if (i <= 12) {
|
||||
defaultBinding = "F" + i;
|
||||
} else {
|
||||
defaultBinding = "shift F" + (i - 12);
|
||||
}
|
||||
String binding = org.pubvm.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
|
||||
bindKeyToMap(im, "PF" + i, binding);
|
||||
}
|
||||
|
||||
// PA keys: Alt+1, Alt+2, Alt+3
|
||||
String pa1Def = "alt 1";
|
||||
String pa2Def = "alt 2";
|
||||
String pa3Def = "alt 3";
|
||||
bindKeyToMap(im, "PA1", org.pubvm.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
|
||||
bindKeyToMap(im, "PA2", org.pubvm.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
|
||||
bindKeyToMap(im, "PA3", org.pubvm.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
|
||||
|
||||
// Clear
|
||||
bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
|
||||
|
||||
// Extended 3270 functions
|
||||
bindKeyToMap(im, "ERASE_INPUT", org.pubvm.j3270.config.Settings.getKeyBinding("ERASE_INPUT", "alt E"));
|
||||
bindKeyToMap(im, "NEWLINE", org.pubvm.j3270.config.Settings.getKeyBinding("NEWLINE", "shift ENTER"));
|
||||
bindKeyToMap(im, "DUP", org.pubvm.j3270.config.Settings.getKeyBinding("DUP", "alt D"));
|
||||
bindKeyToMap(im, "FIELD_MARK", org.pubvm.j3270.config.Settings.getKeyBinding("FIELD_MARK", "alt M"));
|
||||
bindKeyToMap(im, "ATTN", org.pubvm.j3270.config.Settings.getKeyBinding("ATTN", "alt A"));
|
||||
bindKeyToMap(im, "SYSREQ", org.pubvm.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
||||
|
||||
// Copy/Paste bindings — Cmd+C / Cmd+V (macOS) or Ctrl+C / Ctrl+V (others)
|
||||
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
|
||||
|
||||
// Create actions for all bound keys
|
||||
am.put("j3270-ENTER", createAction(this::handleEnter));
|
||||
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
||||
am.put("j3270-TAB", createAction(() -> handleTab(false)));
|
||||
am.put("j3270-shift TAB", createAction(() -> handleTab(true)));
|
||||
am.put("j3270-UP", createAction(() -> handleCursor("up")));
|
||||
am.put("j3270-DOWN", createAction(() -> handleCursor("down")));
|
||||
am.put("j3270-LEFT", createAction(() -> handleCursor("left")));
|
||||
am.put("j3270-RIGHT", createAction(() -> handleCursor("right")));
|
||||
am.put("j3270-HOME", createAction(() -> handleCursor("home")));
|
||||
am.put("j3270-PAGE_UP", createAction(() -> handlePF(7)));
|
||||
am.put("j3270-PAGE_DOWN", createAction(() -> handlePF(8)));
|
||||
am.put("j3270-END", createAction(this::handleEraseEOF));
|
||||
am.put("j3270-DELETE", createAction(this::handleDelete));
|
||||
am.put("j3270-BACK_SPACE", createAction(this::handleBackspace));
|
||||
am.put("j3270-INSERT", createAction(this::handleInsert));
|
||||
am.put("j3270-CLEAR", createAction(this::handleClear));
|
||||
am.put("j3270-ERASE_INPUT", createAction(this::handleEraseInput));
|
||||
am.put("j3270-NEWLINE", createAction(this::handleNewline));
|
||||
am.put("j3270-DUP", createAction(this::handleDup));
|
||||
am.put("j3270-FIELD_MARK", createAction(this::handleFieldMark));
|
||||
am.put("j3270-ATTN", createAction(this::handleAttn));
|
||||
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
|
||||
|
||||
// Copy/Paste actions
|
||||
am.put("j3270-COPY", createAction(this::copySelection));
|
||||
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
final int pf = i;
|
||||
am.put("j3270-PF" + i, createAction(() -> handlePF(pf)));
|
||||
}
|
||||
|
||||
am.put("j3270-PA1", createAction(() -> handlePA(1)));
|
||||
am.put("j3270-PA2", createAction(() -> handlePA(2)));
|
||||
am.put("j3270-PA3", createAction(() -> handlePA(3)));
|
||||
|
||||
// For printable character input, we override processKeyEvent
|
||||
enableEvents(AWTEvent.KEY_EVENT_MASK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processKeyEvent(KeyEvent e) {
|
||||
// Handle character typing via processKeyEvent to capture ALL typed chars
|
||||
if (e.getID() == KeyEvent.KEY_TYPED) {
|
||||
char ch = e.getKeyChar();
|
||||
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
|
||||
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.typeCharacter(ch);
|
||||
refreshScreen();
|
||||
e.consume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.processKeyEvent(e);
|
||||
}
|
||||
|
||||
private AbstractAction createAction(Runnable r) {
|
||||
return new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
r.run();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Key action handlers ==========
|
||||
|
||||
private void handleEnter() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendEnter();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReset() {
|
||||
if (client != null) {
|
||||
client.reset();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTab(boolean shift) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
if (shift)
|
||||
client.backTab();
|
||||
else
|
||||
client.tab();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCursor(String dir) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
switch (dir) {
|
||||
case "up":
|
||||
client.cursorUp();
|
||||
break;
|
||||
case "down":
|
||||
client.cursorDown();
|
||||
break;
|
||||
case "left":
|
||||
client.cursorLeft();
|
||||
break;
|
||||
case "right":
|
||||
client.cursorRight();
|
||||
break;
|
||||
case "home":
|
||||
client.cursorHome();
|
||||
break;
|
||||
}
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePF(int n) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendPF(n);
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePA(int n) {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendPA(n);
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEraseEOF() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.getInputProcessor().eraseEof();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDelete() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.getInputProcessor().deleteChar();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBackspace() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.getInputProcessor().backspace();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInsert() {
|
||||
if (client != null) {
|
||||
var ip = client.getInputProcessor();
|
||||
ip.setInsertMode(!ip.isInsertMode());
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClear() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendClear();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEraseInput() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.eraseInput();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNewline() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.newline();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDup() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.dup();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFieldMark() {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.fieldMark();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAttn() {
|
||||
if (client != null && client.isConnected()) {
|
||||
client.attn();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSysReq() {
|
||||
if (client != null && client.isConnected()) {
|
||||
client.sysReq();
|
||||
refreshScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshScreen() {
|
||||
repaint();
|
||||
// Notify parent to update status bar too
|
||||
Container parent = getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof JFrame) {
|
||||
parent.repaint();
|
||||
break;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFont() {
|
||||
currentFontSize = org.pubvm.j3270.config.Settings.getFontSize();
|
||||
String fontFamily = org.pubvm.j3270.config.Settings.getFontFamily();
|
||||
|
||||
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
|
||||
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
|
||||
// Fallback
|
||||
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
|
||||
}
|
||||
updateCellSize();
|
||||
}
|
||||
|
||||
private void setupColors() {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
hostColors[i] = org.pubvm.j3270.config.Settings.getColorOverride(i, DEFAULT_HOST_COLORS[i]);
|
||||
}
|
||||
monoNormal = org.pubvm.j3270.config.Settings.getMonoColorOverride("NORMAL", DEFAULT_MONO_NORMAL);
|
||||
monoIntensify = org.pubvm.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY);
|
||||
monoProtected = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED);
|
||||
monoProtectedHigh = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH);
|
||||
bgColor = org.pubvm.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR);
|
||||
setBackground(bgColor);
|
||||
}
|
||||
|
||||
public void reloadSettings() {
|
||||
setupColors();
|
||||
setupKeyBindings();
|
||||
setupFont();
|
||||
blockSelectMode = org.pubvm.j3270.config.Settings.getBlockSelectMode();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setFontSize(int size) {
|
||||
currentFontSize = size;
|
||||
org.pubvm.j3270.config.Settings.setFontSize(size);
|
||||
terminalFont = terminalFont.deriveFont((float) size);
|
||||
updateCellSize();
|
||||
resizeGuard = true;
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public int getFontSize() {
|
||||
return currentFontSize;
|
||||
}
|
||||
|
||||
private void updateCellSize() {
|
||||
FontMetrics fm = getFontMetrics(terminalFont);
|
||||
cellWidth = fm.charWidth('M');
|
||||
cellHeight = fm.getHeight();
|
||||
fontAscent = fm.getAscent();
|
||||
fontDescent = fm.getDescent();
|
||||
}
|
||||
|
||||
private void setupCursorBlink() {
|
||||
blinkTimer = new Timer(530, e -> {
|
||||
cursorVisible = !cursorVisible;
|
||||
repaint();
|
||||
});
|
||||
blinkTimer.start();
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
if (client != null) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
// Use the CURRENT screen dimensions (not max) to eliminate extra space.
|
||||
// When the host switches to alternate screen, onScreenSizeChanged fires
|
||||
// and the frame re-packs.
|
||||
int displayCols = sb.getCols();
|
||||
int displayRows = sb.getRows();
|
||||
return new Dimension(displayCols * cellWidth + padding * 2,
|
||||
displayRows * cellHeight + padding * 2);
|
||||
}
|
||||
return new Dimension(80 * cellWidth + padding * 2, 24 * cellHeight + padding * 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
|
||||
if (client == null)
|
||||
return;
|
||||
|
||||
Graphics2D g2 = (Graphics2D) g;
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
||||
|
||||
// Clear entire panel with background color
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||
|
||||
// Compute centered offsets for the grid
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getRows();
|
||||
int cols = sb.getCols();
|
||||
boolean isColorModel = client.getConfig().getModel().isColor();
|
||||
|
||||
// Track current field attribute for monochrome color decisions
|
||||
byte currentFA = 0;
|
||||
ExtendedAttribute currentFieldEa = null;
|
||||
|
||||
for (int row = 0; row < rows; row++) {
|
||||
for (int col = 0; col < cols; col++) {
|
||||
int baddr = row * cols + col;
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
|
||||
int x = ox + col * cellWidth;
|
||||
int y = oy + row * cellHeight;
|
||||
|
||||
// Determine colors and attributes
|
||||
Color fgColor;
|
||||
Color bgColor;
|
||||
boolean bold = false;
|
||||
boolean underline = false;
|
||||
boolean reverse = false;
|
||||
|
||||
if (ea.isFieldAttribute()) {
|
||||
currentFA = ea.fa;
|
||||
currentFieldEa = ea;
|
||||
// Field attributes display as blanks
|
||||
g2.setColor(this.bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
// Selection highlight on field attribute cells too
|
||||
if (isCellSelected(row, col)) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine foreground color
|
||||
if (isColorModel) {
|
||||
fgColor = getColorForAttribute(ea, currentFieldEa, currentFA);
|
||||
bgColor = getBackgroundForAttribute(ea, currentFieldEa);
|
||||
} else {
|
||||
fgColor = getMonoColor(currentFA);
|
||||
bgColor = this.bgColor;
|
||||
}
|
||||
|
||||
// Graphics rendition
|
||||
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
|
||||
if (gr != 0) {
|
||||
if ((gr & GR_INTENSIFY) != 0)
|
||||
bold = true;
|
||||
if ((gr & GR_UNDERLINE) != 0)
|
||||
underline = true;
|
||||
if ((gr & GR_REVERSE) != 0)
|
||||
reverse = true;
|
||||
}
|
||||
|
||||
// Field attribute implicit intensify
|
||||
if (faIsHigh(currentFA & 0xFF))
|
||||
bold = true;
|
||||
|
||||
// Handle invisible fields (zero intensity)
|
||||
if (faIsZero(currentFA & 0xFF)) {
|
||||
g2.setColor(this.bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
if (isCellSelected(row, col)) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply reverse video
|
||||
if (reverse) {
|
||||
Color tmp = fgColor;
|
||||
fgColor = bgColor;
|
||||
bgColor = tmp;
|
||||
}
|
||||
|
||||
// Draw background
|
||||
g2.setColor(bgColor);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
|
||||
// Draw character
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
g2.drawString(String.valueOf(ch), x, y + fontAscent);
|
||||
}
|
||||
|
||||
// Draw underline
|
||||
if (underline) {
|
||||
g2.setColor(fgColor);
|
||||
g2.drawLine(x, y + cellHeight - fontDescent,
|
||||
x + cellWidth - 1, y + cellHeight - fontDescent);
|
||||
}
|
||||
|
||||
// Draw selection highlight
|
||||
if (isCellSelected(row, col)) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
if (cursorVisible && client.getConnectionState().isFullSession()) {
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int curRow = curAddr / cols;
|
||||
int curCol = curAddr % cols;
|
||||
int cx = ox + curCol * cellWidth;
|
||||
int cy = oy + curRow * cellHeight;
|
||||
|
||||
g2.setColor(new Color(255, 255, 255, 180));
|
||||
g2.setXORMode(bgColor);
|
||||
g2.fillRect(cx, cy, cellWidth, cellHeight);
|
||||
g2.setPaintMode();
|
||||
}
|
||||
}
|
||||
|
||||
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
||||
int fg = ea.fg != 0 ? (ea.fg & 0xFF)
|
||||
: (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
|
||||
if (fg >= 0xf0 && fg <= 0xff) {
|
||||
return hostColors[fg - 0xf0];
|
||||
}
|
||||
if (faIsProtected(currentFA & 0xFF)) {
|
||||
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_BLUE];
|
||||
}
|
||||
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_RED] : hostColors[HOST_COLOR_GREEN];
|
||||
}
|
||||
|
||||
private Color getBackgroundForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa) {
|
||||
int bg = ea.bg != 0 ? (ea.bg & 0xFF)
|
||||
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
|
||||
if (bg >= 0xf0 && bg <= 0xff) {
|
||||
int idx = bg - 0xf0;
|
||||
// Neutral black (0xf0) and black (0xf8) should use window bgColor
|
||||
// to avoid visible seams between field bg and window bg
|
||||
if (idx == HOST_COLOR_NEUTRAL_BLACK || idx == HOST_COLOR_BLACK) {
|
||||
return bgColor;
|
||||
}
|
||||
return hostColors[idx];
|
||||
}
|
||||
return bgColor;
|
||||
}
|
||||
|
||||
private Color getMonoColor(byte fa) {
|
||||
if (faIsProtected(fa & 0xFF)) {
|
||||
return faIsHigh(fa & 0xFF) ? monoProtectedHigh : monoProtected;
|
||||
}
|
||||
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (blinkTimer != null)
|
||||
blinkTimer.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class MenuBarShortcutsTest {
|
||||
|
||||
private Map<String, JMenuItem> collectMenuItems(JMenu menu) {
|
||||
Map<String, JMenuItem> map = new HashMap<>();
|
||||
for (int i = 0; i < menu.getItemCount(); i++) {
|
||||
JMenuItem item = menu.getItem(i);
|
||||
if (item != null) {
|
||||
map.put(item.getText(), item);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionAndViewShortcutsUseAlt() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
// In automated/headless environments, JFrame cannot be initialized
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb, "JMenuBar should be present");
|
||||
|
||||
JMenu viewMenu = null;
|
||||
JMenu actionsMenu = null;
|
||||
JMenu fileMenu = null;
|
||||
JMenu editMenu = null;
|
||||
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null) {
|
||||
if ("View".equals(m.getText())) viewMenu = m;
|
||||
else if ("Actions".equals(m.getText())) actionsMenu = m;
|
||||
else if ("File".equals(m.getText())) fileMenu = m;
|
||||
else if ("Edit".equals(m.getText())) editMenu = m;
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(viewMenu, "View menu should exist");
|
||||
assertNotNull(actionsMenu, "Actions menu should exist");
|
||||
assertNotNull(fileMenu, "File menu should exist");
|
||||
assertNotNull(editMenu, "Edit menu should exist");
|
||||
|
||||
// Verify View Menu items use ALT_DOWN_MASK
|
||||
Map<String, JMenuItem> viewItems = collectMenuItems(viewMenu);
|
||||
assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
|
||||
assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
|
||||
assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0);
|
||||
|
||||
// Verify Actions Menu items use ALT_DOWN_MASK
|
||||
Map<String, JMenuItem> actionItems = collectMenuItems(actionsMenu);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Send Enter"), KeyEvent.VK_ENTER);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Clear"), KeyEvent.VK_K);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Reset"), KeyEvent.VK_R);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Erase Input"), KeyEvent.VK_E);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Attention"), KeyEvent.VK_A);
|
||||
assertAcceleratorUsesAlt(actionItems.get("System Request"), KeyEvent.VK_S);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L);
|
||||
assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T);
|
||||
} finally {
|
||||
// Dispose frame
|
||||
app.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void assertAcceleratorUsesAlt(JMenuItem item, int expectedKeyCode) {
|
||||
assertNotNull(item, "Menu item must exist");
|
||||
KeyStroke ks = item.getAccelerator();
|
||||
assertNotNull(ks, "Menu item " + item.getText() + " should have an accelerator");
|
||||
assertEquals(expectedKeyCode, ks.getKeyCode(), "Key code mismatch for " + item.getText());
|
||||
int mods = ks.getModifiers();
|
||||
assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
|
||||
"Menu item " + item.getText() + " accelerator must have ALT modifier");
|
||||
assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
|
||||
"Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ScreenExporterTest {
|
||||
|
||||
private Telnet3270Client client;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_2, false);
|
||||
client = new Telnet3270Client(config);
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainTextExport() throws IOException {
|
||||
client.getPS().setText("TSO/E LOGON SCREEN - WELCOME", 0);
|
||||
|
||||
File tempTxt = File.createTempFile("export_test_", ".txt");
|
||||
tempTxt.deleteOnExit();
|
||||
|
||||
ScreenExporter.exportToText(client, tempTxt);
|
||||
|
||||
assertTrue(tempTxt.exists());
|
||||
assertTrue(tempTxt.length() > 0);
|
||||
|
||||
String content = Files.readString(tempTxt.toPath());
|
||||
assertTrue(content.contains("TSO/E LOGON SCREEN - WELCOME"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHtmlExport() throws IOException {
|
||||
client.getPS().setText("TSO/E LOGON SCREEN - WELCOME", 0);
|
||||
|
||||
File tempHtml = File.createTempFile("export_test_", ".html");
|
||||
tempHtml.deleteOnExit();
|
||||
|
||||
ScreenExporter.exportToHtml(client, tempHtml);
|
||||
|
||||
assertTrue(tempHtml.exists());
|
||||
assertTrue(tempHtml.length() > 0);
|
||||
|
||||
String html = Files.readString(tempHtml.toPath());
|
||||
assertTrue(html.contains("<!DOCTYPE html>"));
|
||||
assertTrue(html.contains("TSO/E LOGON SCREEN - WELCOME"));
|
||||
assertTrue(html.contains("font-family: 'Courier New', Courier, monospace"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class StatusBarTest {
|
||||
|
||||
@Test
|
||||
public void testStatusBarDoesNotContainLightPenButton() {
|
||||
try {
|
||||
StatusBar statusBar = new StatusBar();
|
||||
|
||||
// Ensure no JButton exists in StatusBar components (lightpen button removed)
|
||||
for (Component comp : statusBar.getComponents()) {
|
||||
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
|
||||
}
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusBarUpdatesWithClient() {
|
||||
try {
|
||||
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
|
||||
config.setLuName("TSU001");
|
||||
config.setCodePage("1047");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
|
||||
StatusBar statusBar = new StatusBar();
|
||||
statusBar.setClient(client, null);
|
||||
statusBar.updateStatus();
|
||||
|
||||
// Check labels
|
||||
boolean foundCodePage = false;
|
||||
boolean foundModel = false;
|
||||
for (Component comp : statusBar.getComponents()) {
|
||||
if (comp instanceof JLabel) {
|
||||
JLabel label = (JLabel) comp;
|
||||
if ("CP1047".equals(label.getText())) {
|
||||
foundCodePage = true;
|
||||
}
|
||||
if (label.getText() != null && label.getText().contains("3279-4")) {
|
||||
foundModel = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(foundCodePage, "Should display CP1047");
|
||||
assertTrue(foundModel, "Should display IBM-3279-4 model info");
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ThemeManagerTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUIThemeEnum() {
|
||||
assertEquals("Dark Mode", UITheme.DARK.getDisplayName());
|
||||
assertEquals("Light Mode", UITheme.LIGHT.getDisplayName());
|
||||
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("DARK"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("dark"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("Dark Mode"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("LIGHT"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("light"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("Light Mode"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("invalid_value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testColorTokensContrast() {
|
||||
for (UITheme theme : UITheme.values()) {
|
||||
Color bg = ThemeManager.getBgMain(theme);
|
||||
Color fg = ThemeManager.getFgMain(theme);
|
||||
assertNotNull(bg);
|
||||
assertNotNull(fg);
|
||||
|
||||
// Compute luminance contrast
|
||||
double bgLum = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue());
|
||||
double fgLum = (0.299 * fg.getRed() + 0.587 * fg.getGreen() + 0.114 * fg.getBlue());
|
||||
double lumDiff = Math.abs(bgLum - fgLum);
|
||||
|
||||
// Minimum luminance difference to guarantee readability
|
||||
assertTrue(lumDiff > 120, "Theme " + theme + " text contrast luminance difference " + lumDiff + " must be > 120");
|
||||
|
||||
// Verify Menu Bar colors
|
||||
Color menuBg = ThemeManager.getMenuBarBg(theme);
|
||||
Color menuFg = ThemeManager.getMenuItemFg(theme);
|
||||
double menuBgLum = (0.299 * menuBg.getRed() + 0.587 * menuBg.getGreen() + 0.114 * menuBg.getBlue());
|
||||
double menuFgLum = (0.299 * menuFg.getRed() + 0.587 * menuFg.getGreen() + 0.114 * menuFg.getBlue());
|
||||
assertTrue(Math.abs(menuBgLum - menuFgLum) > 100, "Menu contrast in " + theme + " must be high");
|
||||
|
||||
// Verify Status Bar colors
|
||||
Color sbBg = ThemeManager.getStatusBarBg(theme);
|
||||
Color sbNormal = ThemeManager.getOiaFgNormal(theme);
|
||||
assertNotNull(sbBg);
|
||||
assertNotNull(sbNormal);
|
||||
|
||||
// Verify Button colors
|
||||
Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme);
|
||||
Color btnDefFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.DEFAULT, theme);
|
||||
assertNotNull(btnDefBg);
|
||||
assertNotNull(btnDefFg);
|
||||
|
||||
Color btnPriBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.PRIMARY, theme);
|
||||
Color btnPriFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.PRIMARY, theme);
|
||||
assertNotNull(btnPriBg);
|
||||
assertNotNull(btnPriFg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThemeChangeListeners() {
|
||||
AtomicReference<UITheme> notifiedTheme = new AtomicReference<>(null);
|
||||
ThemeManager.addThemeChangeListener(notifiedTheme::set);
|
||||
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
assertEquals(UITheme.LIGHT, ThemeManager.getTheme());
|
||||
assertEquals(UITheme.LIGHT, notifiedTheme.get());
|
||||
assertTrue(ThemeManager.isLight());
|
||||
assertFalse(ThemeManager.isDark());
|
||||
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
assertEquals(UITheme.DARK, ThemeManager.getTheme());
|
||||
assertEquals(UITheme.DARK, notifiedTheme.get());
|
||||
assertTrue(ThemeManager.isDark());
|
||||
assertFalse(ThemeManager.isLight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComponentStyling() {
|
||||
try {
|
||||
JButton btn = new JButton("Test");
|
||||
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
assertNotNull(btn.getUI());
|
||||
assertTrue(btn.getUI() instanceof ThemeManager.StyledButtonUI);
|
||||
|
||||
JTextField tf = new JTextField("Test");
|
||||
ThemeManager.styleTextField(tf);
|
||||
assertNotNull(tf.getCaretColor());
|
||||
|
||||
JTextArea ta = new JTextArea("Test area");
|
||||
ThemeManager.styleTextArea(ta);
|
||||
assertNotNull(ta.getCaretColor());
|
||||
|
||||
JComboBox<String> cb = new JComboBox<>(new String[]{"A", "B"});
|
||||
ThemeManager.styleComboBox(cb);
|
||||
assertNotNull(cb.getUI());
|
||||
|
||||
JTable table = new JTable(new DefaultTableModel(new Object[]{"Col1"}, 1));
|
||||
ThemeManager.styleTable(table);
|
||||
assertNotNull(table.getSelectionBackground());
|
||||
|
||||
JTabbedPane tp = new JTabbedPane();
|
||||
tp.addTab("Tab1", new JPanel());
|
||||
ThemeManager.styleTabbedPane(tp);
|
||||
assertNotNull(tp.getUI());
|
||||
|
||||
JMenuBar mb = new JMenuBar();
|
||||
ThemeManager.styleMenuBar(mb);
|
||||
|
||||
JMenu menu = new JMenu("File");
|
||||
ThemeManager.styleMenu(menu);
|
||||
|
||||
JMenuItem mi = new JMenuItem("Open");
|
||||
ThemeManager.styleMenuItem(mi);
|
||||
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
ThemeManager.stylePopupMenu(popup);
|
||||
|
||||
JCheckBox chk = new JCheckBox("Check");
|
||||
ThemeManager.styleCheckBox(chk);
|
||||
|
||||
JRadioButton rb = new JRadioButton("Radio");
|
||||
ThemeManager.styleRadioButton(rb);
|
||||
|
||||
Border tb = ThemeManager.createTitledBorder("Title");
|
||||
assertNotNull(tb);
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveApplyTheme() {
|
||||
try {
|
||||
JPanel root = new JPanel(new BorderLayout());
|
||||
JButton b = new JButton("OK");
|
||||
JTextField f = new JTextField("Data");
|
||||
JTabbedPane tp = new JTabbedPane();
|
||||
JPanel tabContent = new JPanel();
|
||||
JLabel lbl = new JLabel("Label");
|
||||
tabContent.add(lbl);
|
||||
tp.addTab("T1", tabContent);
|
||||
|
||||
root.add(b, BorderLayout.NORTH);
|
||||
root.add(f, BorderLayout.CENTER);
|
||||
root.add(tp, BorderLayout.SOUTH);
|
||||
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
ThemeManager.applyTheme(root);
|
||||
|
||||
assertEquals(ThemeManager.getBgPanel(UITheme.LIGHT), root.getBackground());
|
||||
assertEquals(ThemeManager.getFgMain(UITheme.LIGHT), lbl.getForeground());
|
||||
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
ThemeManager.applyTheme(root);
|
||||
|
||||
assertEquals(ThemeManager.getBgPanel(UITheme.DARK), root.getBackground());
|
||||
assertEquals(ThemeManager.getFgMain(UITheme.DARK), lbl.getForeground());
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsPersistence() throws Exception {
|
||||
Settings.setJavaUiTheme(UITheme.LIGHT);
|
||||
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
|
||||
|
||||
File tmpIni = File.createTempFile("j3270_test_config", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
// Write INI file with javaUiTheme = dark
|
||||
try (PrintWriter pw = new PrintWriter(tmpIni)) {
|
||||
pw.println("[j3270]");
|
||||
pw.println("javaUiTheme = dark");
|
||||
pw.println("fontFamily = Monospaced");
|
||||
pw.println("fontSize = 16");
|
||||
}
|
||||
|
||||
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
|
||||
assertEquals(UITheme.DARK, Settings.getJavaUiTheme());
|
||||
|
||||
// Switch to light and export
|
||||
Settings.setJavaUiTheme(UITheme.LIGHT);
|
||||
File exportFile = File.createTempFile("j3270_export_config", ".ini");
|
||||
exportFile.deleteOnExit();
|
||||
|
||||
Settings.exportToIniFile(exportFile.getAbsolutePath());
|
||||
String exportedContent = new String(Files.readAllBytes(exportFile.toPath()));
|
||||
assertTrue(exportedContent.toLowerCase().contains("javauitheme = light"));
|
||||
|
||||
// Reload exported file
|
||||
Settings.loadFromIniFile(exportFile.getAbsolutePath());
|
||||
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
|
||||
} finally {
|
||||
tmpIni.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
jar {
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
/**
|
||||
* Configuration for a 3270 terminal connection.
|
||||
*/
|
||||
public class ConnectionConfig {
|
||||
|
||||
public enum ProxyType {
|
||||
NONE, HTTP, SOCKS4, SOCKS5
|
||||
}
|
||||
|
||||
private String host;
|
||||
private int port = 23;
|
||||
private TerminalModel model = TerminalModel.IBM_3279_4;
|
||||
private String luName = null;
|
||||
private boolean extendedDataStream = true;
|
||||
private boolean useTls = false;
|
||||
private boolean tlsVerifyCert = true;
|
||||
private haus.nightmare.lib3270j.tls.TlsCertificateVerifier certificateVerifier = null;
|
||||
private String sslProtocol = "TLS";
|
||||
private int connectTimeoutMs = 15000;
|
||||
private int nopIntervalSeconds = 0;
|
||||
private String terminalName = null; // override terminal type string
|
||||
private boolean tn3270eEnabled = true;
|
||||
private boolean tcpNoDelay = true;
|
||||
private boolean soKeepAlive = true;
|
||||
private int soTimeoutMs = 0;
|
||||
private java.util.List<String> luNames = new java.util.ArrayList<>();
|
||||
private boolean dynamicModel = false;
|
||||
private int dynamicRows = 24;
|
||||
private int dynamicCols = 80;
|
||||
private haus.nightmare.lib3270j.graphics.GraphicsMode graphicsMode = haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH;
|
||||
private String codePage = "037";
|
||||
private String associatedPrinterLu = null;
|
||||
private boolean nvtLocalEcho = false;
|
||||
|
||||
// Proxy configuration
|
||||
private ProxyType proxyType = ProxyType.NONE;
|
||||
private String proxyHost = null;
|
||||
private int proxyPort = 0;
|
||||
private String proxyUsername = null;
|
||||
private String proxyPassword = null;
|
||||
|
||||
// STARTTLS (Telnet Option 46) dynamic socket elevation
|
||||
private boolean startTlsEnabled = true;
|
||||
|
||||
// RFC 1572 / RFC 2877 Environment variables (Express Logon)
|
||||
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
|
||||
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public ConnectionConfig(String host, int port, TerminalModel model, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.model = model;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
|
||||
public TerminalModel getModel() { return model; }
|
||||
public void setModel(TerminalModel model) { this.model = model; }
|
||||
|
||||
public String getLuName() { return luName; }
|
||||
public void setLuName(String luName) { this.luName = luName; }
|
||||
|
||||
public boolean isExtendedDataStream() { return extendedDataStream; }
|
||||
public void setExtendedDataStream(boolean ext) { this.extendedDataStream = ext; }
|
||||
|
||||
public boolean isUseTls() { return useTls; }
|
||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||
|
||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
|
||||
|
||||
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
|
||||
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
|
||||
|
||||
public haus.nightmare.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||
public void setCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
||||
|
||||
public String getSslProtocol() { return sslProtocol; }
|
||||
public void setSslProtocol(String protocol) { this.sslProtocol = protocol; }
|
||||
|
||||
public int getConnectTimeoutMs() { return connectTimeoutMs; }
|
||||
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
|
||||
|
||||
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
|
||||
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { return graphicsMode; }
|
||||
public void setGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||
this.graphicsMode = (mode != null) ? mode : haus.nightmare.lib3270j.graphics.GraphicsMode.NONE;
|
||||
}
|
||||
|
||||
public String getCodePage() { return codePage; }
|
||||
public void setCodePage(String codePage) {
|
||||
this.codePage = (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037";
|
||||
}
|
||||
|
||||
public boolean isNvtLocalEcho() { return nvtLocalEcho; }
|
||||
public void setNvtLocalEcho(boolean nvtLocalEcho) { this.nvtLocalEcho = nvtLocalEcho; }
|
||||
|
||||
public String getTerminalName() { return terminalName; }
|
||||
public void setTerminalName(String name) { this.terminalName = name; }
|
||||
|
||||
public boolean isTcpNoDelay() { return tcpNoDelay; }
|
||||
public void setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; }
|
||||
|
||||
public boolean isSoKeepAlive() { return soKeepAlive; }
|
||||
public void setSoKeepAlive(boolean soKeepAlive) { this.soKeepAlive = soKeepAlive; }
|
||||
|
||||
public int getSoTimeoutMs() { return soTimeoutMs; }
|
||||
public void setSoTimeoutMs(int soTimeoutMs) { this.soTimeoutMs = soTimeoutMs; }
|
||||
|
||||
public java.util.List<String> getLuNames() { return luNames; }
|
||||
public void setLuNames(java.util.List<String> luNames) {
|
||||
this.luNames = (luNames != null) ? new java.util.ArrayList<>(luNames) : new java.util.ArrayList<>();
|
||||
}
|
||||
public void addLuName(String luName) {
|
||||
if (luName != null && !luName.trim().isEmpty()) {
|
||||
this.luNames.add(luName.trim());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDynamicModel() { return dynamicModel; }
|
||||
public void setDynamicModel(boolean dynamicModel) { this.dynamicModel = dynamicModel; }
|
||||
|
||||
public int getDynamicRows() { return dynamicRows; }
|
||||
public int getDynamicCols() { return dynamicCols; }
|
||||
public void setDynamicDimensions(int rows, int cols) {
|
||||
this.dynamicModel = true;
|
||||
this.dynamicRows = rows;
|
||||
this.dynamicCols = cols;
|
||||
}
|
||||
|
||||
public ProxyType getProxyType() { return proxyType; }
|
||||
public void setProxyType(ProxyType proxyType) { this.proxyType = proxyType != null ? proxyType : ProxyType.NONE; }
|
||||
|
||||
public String getProxyHost() { return proxyHost; }
|
||||
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
|
||||
|
||||
public int getProxyPort() { return proxyPort; }
|
||||
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
|
||||
|
||||
public String getProxyUsername() { return proxyUsername; }
|
||||
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
|
||||
|
||||
public String getProxyPassword() { return proxyPassword; }
|
||||
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
|
||||
|
||||
public void setProxy(ProxyType type, String host, int port, String username, String password) {
|
||||
this.proxyType = type != null ? type : ProxyType.NONE;
|
||||
this.proxyHost = host;
|
||||
this.proxyPort = port;
|
||||
this.proxyUsername = username;
|
||||
this.proxyPassword = password;
|
||||
}
|
||||
|
||||
public boolean isStartTlsEnabled() { return startTlsEnabled; }
|
||||
public void setStartTlsEnabled(boolean enabled) { this.startTlsEnabled = enabled; }
|
||||
|
||||
public java.util.Map<String, String> getEnvironmentVariables() { return environmentVariables; }
|
||||
public void setEnvironmentVariables(java.util.Map<String, String> vars) {
|
||||
this.environmentVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
|
||||
}
|
||||
public void setEnvironmentVariable(String name, String value) {
|
||||
if (name != null) {
|
||||
if (value != null) this.environmentVariables.put(name, value);
|
||||
else this.environmentVariables.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
public java.util.Map<String, String> getUserVariables() { return userVariables; }
|
||||
public void setUserVariables(java.util.Map<String, String> vars) {
|
||||
this.userVariables = (vars != null) ? new java.util.LinkedHashMap<>(vars) : new java.util.LinkedHashMap<>();
|
||||
}
|
||||
public void setUserVariable(String name, String value) {
|
||||
if (name != null) {
|
||||
if (value != null) this.userVariables.put(name, value);
|
||||
else this.userVariables.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
|
||||
* or standard "host:port" formats.
|
||||
*/
|
||||
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
||||
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||
return new ConnectionConfig("localhost", defaultPort, defaultModel);
|
||||
}
|
||||
String s = hostStr.trim();
|
||||
boolean tls = false;
|
||||
boolean tn3270e = true;
|
||||
|
||||
// Parse --proxy=<url> or -proxy=<url> flags
|
||||
ProxyType pType = ProxyType.NONE;
|
||||
String pHost = null;
|
||||
int pPort = 0;
|
||||
String pUser = null;
|
||||
String pPass = null;
|
||||
|
||||
String[] tokens = s.split("\\s+");
|
||||
StringBuilder remaining = new StringBuilder();
|
||||
for (String tok : tokens) {
|
||||
if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
|
||||
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
|
||||
try {
|
||||
java.net.URI uri = new java.net.URI(proxyUrl);
|
||||
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
|
||||
if (scheme.equals("http") || scheme.equals("https")) {
|
||||
pType = ProxyType.HTTP;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
|
||||
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
|
||||
pType = ProxyType.SOCKS4;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
|
||||
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
|
||||
pType = ProxyType.SOCKS5;
|
||||
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
|
||||
}
|
||||
pHost = uri.getHost();
|
||||
String userInfo = uri.getUserInfo();
|
||||
if (userInfo != null) {
|
||||
int colon = userInfo.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
pUser = userInfo.substring(0, colon);
|
||||
pPass = userInfo.substring(colon + 1);
|
||||
} else {
|
||||
pUser = userInfo;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} else {
|
||||
if (remaining.length() > 0) remaining.append(" ");
|
||||
remaining.append(tok);
|
||||
}
|
||||
}
|
||||
s = remaining.toString().trim();
|
||||
|
||||
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
|
||||
boolean prefixFound = true;
|
||||
while (prefixFound) {
|
||||
prefixFound = false;
|
||||
if (s.startsWith("L:") || s.startsWith("l:") || s.startsWith("Y:") || s.startsWith("y:")) {
|
||||
tls = true;
|
||||
s = s.substring(2);
|
||||
prefixFound = true;
|
||||
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
|
||||
tls = true;
|
||||
s = s.substring(4);
|
||||
prefixFound = true;
|
||||
} else if (s.startsWith("N:") || s.startsWith("n:") || s.toLowerCase().startsWith("notls:") || s.toLowerCase().startsWith("nossl:")) {
|
||||
tls = false;
|
||||
int colon = s.indexOf(':');
|
||||
s = s.substring(colon + 1);
|
||||
prefixFound = true;
|
||||
} else if (s.startsWith("P:") || s.startsWith("p:")) {
|
||||
tn3270e = false;
|
||||
s = s.substring(2);
|
||||
prefixFound = true;
|
||||
} else if (s.toLowerCase().startsWith("plain:") || s.toLowerCase().startsWith("non-e:")) {
|
||||
tn3270e = false;
|
||||
int colon = s.indexOf(':');
|
||||
s = s.substring(colon + 1);
|
||||
prefixFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
String host = s;
|
||||
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||
|
||||
// Check for host:port (handle IPv6 [::1]:port)
|
||||
if (s.startsWith("[") && s.contains("]")) {
|
||||
int closeBracket = s.indexOf(']');
|
||||
host = s.substring(1, closeBracket);
|
||||
if (s.length() > closeBracket + 1 && s.charAt(closeBracket + 1) == ':') {
|
||||
try {
|
||||
port = Integer.parseInt(s.substring(closeBracket + 2));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
} else {
|
||||
int colon = s.lastIndexOf(':');
|
||||
if (colon > 0 && colon < s.length() - 1) {
|
||||
try {
|
||||
port = Integer.parseInt(s.substring(colon + 1));
|
||||
host = s.substring(0, colon);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
if (pType != ProxyType.NONE && pHost != null) {
|
||||
config.setProxy(pType, pHost, pPort, pUser, pPass);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective terminal type string to send during negotiation.
|
||||
*/
|
||||
public String getEffectiveTerminalType() {
|
||||
if (terminalName != null) {
|
||||
return terminalName;
|
||||
}
|
||||
if (dynamicModel) {
|
||||
return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC";
|
||||
}
|
||||
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
||||
}
|
||||
|
||||
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
|
||||
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
|
||||
|
||||
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
|
||||
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
|
||||
pcfg.setUseTls(useTls);
|
||||
pcfg.setTlsVerifyCert(tlsVerifyCert);
|
||||
pcfg.setCertificateVerifier(certificateVerifier);
|
||||
pcfg.setSslProtocol(sslProtocol);
|
||||
pcfg.setConnectTimeoutMs(connectTimeoutMs);
|
||||
pcfg.setPrinterLuName(associatedPrinterLu);
|
||||
pcfg.setAssociatedDisplayLuName(luName);
|
||||
pcfg.setCodePage(codePage);
|
||||
return pcfg;
|
||||
}
|
||||
}
|
||||
+29
-1
@@ -1,4 +1,4 @@
|
||||
package org.lib3270j;
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
/**
|
||||
* Connection state machine states.
|
||||
@@ -59,4 +59,32 @@ public enum ConnectionState {
|
||||
public boolean isFullSession() {
|
||||
return isNvt() || is3270();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to IBM Host On-Demand ECL connection state integer codes.
|
||||
* 0 = Disconnected, 1 = Connecting/Resolving, 2 = Connected (NVT/Unbound), 3 = Bound (Full 3270 session).
|
||||
*/
|
||||
public int toHoDStateCode() {
|
||||
switch (this) {
|
||||
case NOT_CONNECTED:
|
||||
return 0;
|
||||
case RECONNECTING:
|
||||
case RESOLVING:
|
||||
case TCP_PENDING:
|
||||
case TLS_PENDING:
|
||||
case PROXY_PENDING:
|
||||
case TELNET_PENDING:
|
||||
return 1;
|
||||
case CONNECTED_NVT:
|
||||
case CONNECTED_NVT_CHAR:
|
||||
case CONNECTED_UNBOUND:
|
||||
case CONNECTED_E_NVT:
|
||||
case CONNECTED_SSCP:
|
||||
return 2;
|
||||
case CONNECTED_3270:
|
||||
case CONNECTED_TN3270E:
|
||||
default:
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
package org.lib3270j;
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import org.lib3270j.screen.ScreenBuffer;
|
||||
import org.lib3270j.screen.ExtendedAttribute;
|
||||
import org.lib3270j.listener.*;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.listener.*;
|
||||
|
||||
import java.util.logging.*;
|
||||
|
||||
@@ -15,7 +15,7 @@ public class DiagnosticClient {
|
||||
static Telnet3270Client client;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Logger rootLogger = Logger.getLogger("org.lib3270j");
|
||||
Logger rootLogger = Logger.getLogger("haus.nightmare.lib3270j");
|
||||
rootLogger.setLevel(Level.ALL);
|
||||
Handler handler = new ConsoleHandler();
|
||||
handler.setLevel(Level.ALL);
|
||||
@@ -26,7 +26,7 @@ public class DiagnosticClient {
|
||||
String host = args.length >= 1 ? args[0] : "192.168.0.30";
|
||||
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270;
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_2);
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4);
|
||||
client = new Telnet3270Client(config);
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@@ -0,0 +1,498 @@
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetConnection;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Main API entry point for lib3270j.
|
||||
*
|
||||
* Provides a high-level interface for connecting to a TN3270 host,
|
||||
* managing the screen buffer, and handling user input.
|
||||
*
|
||||
* Usage:
|
||||
* <pre>
|
||||
* ConnectionConfig config = new ConnectionConfig("hostname", 23, TerminalModel.IBM_3279_4);
|
||||
* Telnet3270Client client = new Telnet3270Client(config);
|
||||
* client.addConnectionListener(myListener);
|
||||
* client.addScreenUpdateListener(myScreenListener);
|
||||
* client.connect();
|
||||
* // ... interact with screen ...
|
||||
* client.disconnect();
|
||||
* </pre>
|
||||
*/
|
||||
public class Telnet3270Client {
|
||||
|
||||
private static final Logger log = Logger.getLogger(Telnet3270Client.class.getName());
|
||||
|
||||
private final ConnectionConfig config;
|
||||
private final EbcdicTranslator translator;
|
||||
private final ScreenBuffer screenBuffer;
|
||||
private final DataStreamProcessor dsProcessor;
|
||||
private final TelnetFSM fsm;
|
||||
private final InputProcessor inputProcessor;
|
||||
private final haus.nightmare.lib3270j.ecl.ECLPS ps;
|
||||
private final haus.nightmare.lib3270j.ecl.ECLOIA oia;
|
||||
private final haus.nightmare.lib3270j.ecl.ECLXfer xfer;
|
||||
private TelnetConnection connection;
|
||||
|
||||
public Telnet3270Client(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
this.translator = new EbcdicTranslator(config.getCodePage());
|
||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||
this.ps = new haus.nightmare.lib3270j.ecl.ECLPS(screenBuffer, inputProcessor, translator);
|
||||
this.oia = new haus.nightmare.lib3270j.ecl.ECLOIA(screenBuffer, inputProcessor, fsm);
|
||||
this.inputProcessor.setOIA(oia);
|
||||
this.xfer = new haus.nightmare.lib3270j.ecl.ECLXfer(screenBuffer, inputProcessor, dsProcessor, translator);
|
||||
|
||||
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
|
||||
dsProcessor.setOutputSender(fsm::send3270Data);
|
||||
dsProcessor.setInputProcessor(inputProcessor);
|
||||
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
|
||||
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
|
||||
|
||||
// Wire screen update to ECLXfer for CUT mode screen tracking and ECLPS for event dispatching
|
||||
addScreenUpdateListener(new haus.nightmare.lib3270j.listener.ScreenUpdateListener() {
|
||||
@Override public void onScreenUpdated() {
|
||||
xfer.onScreenUpdated();
|
||||
ps.notifyPSUpdate(0, 0, screenBuffer.getRows() - 1, screenBuffer.getCols() - 1, true);
|
||||
}
|
||||
@Override public void onCursorMoved(int oldAddress, int newAddress) {
|
||||
ps.notifyCursorMoved(oldAddress, newAddress);
|
||||
}
|
||||
@Override public void onScreenSizeChanged(int rows, int cols) {
|
||||
ps.notifyScreenResized(rows, cols);
|
||||
}
|
||||
@Override public void onSoundAlarm() {
|
||||
ps.notifyAlarm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the configured host.
|
||||
* This method blocks until the TCP connection is established,
|
||||
* then returns while telnet/TN3270E negotiation continues asynchronously.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
|
||||
" model=" + config.getModel());
|
||||
|
||||
connection = new TelnetConnection(config, fsm);
|
||||
fsm.setConnection(connection);
|
||||
|
||||
// Establish TCP connection
|
||||
connection.connect();
|
||||
|
||||
// Notify FSM that TCP is connected — begins telnet negotiation
|
||||
fsm.onConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the configured host synchronously, blocking until the full data session is established
|
||||
* (or timeout expires).
|
||||
* @param timeoutMs maximum time to wait in milliseconds
|
||||
* @return true if successfully connected, false if timed out
|
||||
* @throws IOException if network connection fails
|
||||
*/
|
||||
public boolean connect(long timeoutMs) throws IOException {
|
||||
connect();
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
ConnectionState state = getConnectionState();
|
||||
if (state.isFullSession() || (state.isFullyConnected() && isConnected())) {
|
||||
return true;
|
||||
}
|
||||
if (state == ConnectionState.NOT_CONNECTED && !isConnected()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return getConnectionState().isFullSession() || isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the configured host synchronously and block until the presentation space matches
|
||||
* the specified screen descriptor.
|
||||
* @param timeoutMs maximum time to wait in milliseconds
|
||||
* @param desc ECLScreenDesc descriptor to match against
|
||||
* @return true if successfully connected and screen matched, false otherwise
|
||||
* @throws IOException if network connection fails
|
||||
*/
|
||||
public boolean connect(long timeoutMs, haus.nightmare.lib3270j.ecl.ECLScreenDesc desc) throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
if (!connect(timeoutMs)) {
|
||||
return false;
|
||||
}
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
long remaining = Math.max(1, timeoutMs - elapsed);
|
||||
return ps.waitForScreen(desc, remaining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
fsm.onDisconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from host with synchronous teardown.
|
||||
* @param timeoutMs maximum time to wait for graceful disconnect in milliseconds
|
||||
*/
|
||||
public void disconnect(long timeoutMs) {
|
||||
disconnect();
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (getConnectionState() == ConnectionState.NOT_CONNECTED && !isConnected()) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** HoD StopCommunication compatibility alias. */
|
||||
public void stopCommunication() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
public void stopCommunication(long timeoutMs) {
|
||||
disconnect(timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected (any state past TCP pending).
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return connection != null && connection.isConnected();
|
||||
}
|
||||
|
||||
// ========== Listener management ==========
|
||||
|
||||
public void addConnectionListener(ConnectionListener l) {
|
||||
fsm.addConnectionListener(l);
|
||||
}
|
||||
|
||||
public void removeConnectionListener(ConnectionListener l) {
|
||||
fsm.removeConnectionListener(l);
|
||||
}
|
||||
|
||||
public void addScreenUpdateListener(ScreenUpdateListener l) {
|
||||
fsm.addScreenUpdateListener(l);
|
||||
dsProcessor.addScreenUpdateListener(l);
|
||||
}
|
||||
|
||||
public void removeScreenUpdateListener(ScreenUpdateListener l) {
|
||||
fsm.removeScreenUpdateListener(l);
|
||||
dsProcessor.removeScreenUpdateListener(l);
|
||||
}
|
||||
|
||||
public void addSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
|
||||
fsm.addSCSInboundListener(l);
|
||||
}
|
||||
|
||||
public void removeSCSInboundListener(haus.nightmare.lib3270j.listener.SCSInboundListener l) {
|
||||
fsm.removeSCSInboundListener(l);
|
||||
}
|
||||
|
||||
// ========== Screen access ==========
|
||||
|
||||
/** Get the screen buffer for rendering. */
|
||||
public ScreenBuffer getScreenBuffer() { return screenBuffer; }
|
||||
|
||||
/** Get the EBCDIC translator. */
|
||||
public EbcdicTranslator getTranslator() { return translator; }
|
||||
|
||||
/** Get the active Code Page identifier. */
|
||||
public String getCodePage() { return translator.getCodePageId(); }
|
||||
|
||||
/** Set the active Code Page identifier. */
|
||||
public void setCodePage(String codePageId) { translator.setCodePage(codePageId); }
|
||||
|
||||
/** Get the current connection state. */
|
||||
public ConnectionState getConnectionState() { return fsm.getConnectionState(); }
|
||||
|
||||
/** Get the input processor for keyboard operations. */
|
||||
public InputProcessor getInputProcessor() { return inputProcessor; }
|
||||
|
||||
/** Get the data stream processor. */
|
||||
public DataStreamProcessor getDataStreamProcessor() { return dsProcessor; }
|
||||
|
||||
/** Get the connection config. */
|
||||
public ConnectionConfig getConfig() { return config; }
|
||||
|
||||
/** Get the ECL Presentation Space API. */
|
||||
public haus.nightmare.lib3270j.ecl.ECLPS getPS() { return ps; }
|
||||
|
||||
/** Get the ECL Operator Information Area API. */
|
||||
public haus.nightmare.lib3270j.ecl.ECLOIA getOIA() { return oia; }
|
||||
|
||||
/** Get the ECL File Transfer API. */
|
||||
public haus.nightmare.lib3270j.ecl.ECLXfer getXfer() { return xfer; }
|
||||
|
||||
/** Get the list of all fields currently on screen. */
|
||||
public haus.nightmare.lib3270j.ecl.ECLFieldList getFieldList() { return ps.getFieldList(); }
|
||||
|
||||
/** Send IBM ECL bracketed mnemonic keystrokes (e.g. "USER[tab]PASS[enter]"). */
|
||||
public void sendKeys(String keys) { inputProcessor.sendKeys(keys); }
|
||||
|
||||
/** Set a custom or interactive TLS certificate verifier callback. */
|
||||
public void setTlsCertificateVerifier(haus.nightmare.lib3270j.tls.TlsCertificateVerifier verifier) {
|
||||
config.setCertificateVerifier(verifier);
|
||||
}
|
||||
|
||||
/** Get the active SSLSession if connected over TLS, or null. */
|
||||
public javax.net.ssl.SSLSession getSslSession() {
|
||||
return connection != null ? connection.getSslSession() : null;
|
||||
}
|
||||
|
||||
/** Get the NVT processor for ASCII / ANSI terminal processing. */
|
||||
public haus.nightmare.lib3270j.nvt.NvtProcessor getNvtProcessor() {
|
||||
return fsm.getNvtProcessor();
|
||||
}
|
||||
|
||||
/** Get the Telnet state machine. */
|
||||
public TelnetFSM getTelnetFSM() {
|
||||
return fsm;
|
||||
}
|
||||
|
||||
/** Send an NVT ASCII character in NVT mode. */
|
||||
public void sendNVTChar(char c) throws IOException {
|
||||
fsm.sendNVTChar(c);
|
||||
}
|
||||
|
||||
/** Send an NVT ASCII string in NVT mode. */
|
||||
public void sendNVTString(String s) throws IOException {
|
||||
fsm.sendNVTString(s);
|
||||
}
|
||||
|
||||
/** Send an NVT key event mapped through NVT processor. */
|
||||
public boolean sendNVTKey(int keyCode, char keyChar, boolean shift, boolean ctrl, boolean alt) throws IOException {
|
||||
haus.nightmare.lib3270j.nvt.NvtProcessor nvt = getNvtProcessor();
|
||||
if (nvt != null) {
|
||||
String seq = nvt.mapKey(keyCode, keyChar, shift, ctrl, alt);
|
||||
if (seq != null && !seq.isEmpty()) {
|
||||
fsm.sendNvtData(seq.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addNvtTitleListener(haus.nightmare.lib3270j.nvt.NvtProcessor.NvtTitleListener l) {
|
||||
if (getNvtProcessor() != null) {
|
||||
getNvtProcessor().addTitleListener(l);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeNvtTitleListener(haus.nightmare.lib3270j.nvt.NvtProcessor.NvtTitleListener l) {
|
||||
if (getNvtProcessor() != null) {
|
||||
getNvtProcessor().removeTitleListener(l);
|
||||
}
|
||||
}
|
||||
|
||||
public void setNvtClipboardHandler(haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler h) {
|
||||
if (getNvtProcessor() != null) {
|
||||
getNvtProcessor().setClipboardHandler(h);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Convenience input methods & HoD compatibility ==========
|
||||
|
||||
/** Type a character at the cursor position. */
|
||||
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
|
||||
|
||||
/** Process a character at the current cursor position. */
|
||||
public void processChar(char ch) { inputProcessor.processChar(ch); }
|
||||
|
||||
/** Process a character with explicit position and insert mode flag. */
|
||||
public void processChar(char ch, int pos, boolean insert) { inputProcessor.processChar(ch, pos, insert); }
|
||||
|
||||
/** Type a string at the cursor position. */
|
||||
public void typeString(String s) {
|
||||
for (char ch : s.toCharArray()) {
|
||||
inputProcessor.typeCharacter(ch);
|
||||
}
|
||||
}
|
||||
|
||||
/** Emulate input of a string, pressing Enter on newlines. */
|
||||
public void emulateInput(String s) {
|
||||
inputProcessor.emulateInput(s);
|
||||
}
|
||||
|
||||
/** Send Enter key. */
|
||||
public void sendEnter() {
|
||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
|
||||
}
|
||||
public void processEnter() { inputProcessor.processEnter(); }
|
||||
|
||||
/** Send a PF key (1-24). */
|
||||
public void sendPF(int number) {
|
||||
int aid;
|
||||
switch (number) {
|
||||
case 1: aid = 0xf1; break; case 2: aid = 0xf2; break;
|
||||
case 3: aid = 0xf3; break; case 4: aid = 0xf4; break;
|
||||
case 5: aid = 0xf5; break; case 6: aid = 0xf6; break;
|
||||
case 7: aid = 0xf7; break; case 8: aid = 0xf8; break;
|
||||
case 9: aid = 0xf9; break; case 10: aid = 0x7a; break;
|
||||
case 11: aid = 0x7b; break; case 12: aid = 0x7c; break;
|
||||
case 13: aid = 0xc1; break; case 14: aid = 0xc2; break;
|
||||
case 15: aid = 0xc3; break; case 16: aid = 0xc4; break;
|
||||
case 17: aid = 0xc5; break; case 18: aid = 0xc6; break;
|
||||
case 19: aid = 0xc7; break; case 20: aid = 0xc8; break;
|
||||
case 21: aid = 0xc9; break; case 22: aid = 0x4a; break;
|
||||
case 23: aid = 0x4b; break; case 24: aid = 0x4c; break;
|
||||
default: return;
|
||||
}
|
||||
inputProcessor.sendAid(aid);
|
||||
}
|
||||
public void processPF(int number) { inputProcessor.processPF(number); }
|
||||
|
||||
/** Send a PA key (1-3). */
|
||||
public void sendPA(int number) {
|
||||
int aid;
|
||||
switch (number) {
|
||||
case 1: aid = 0x6c; break;
|
||||
case 2: aid = 0x6e; break;
|
||||
case 3: aid = 0x6b; break;
|
||||
default: return;
|
||||
}
|
||||
inputProcessor.sendAid(aid);
|
||||
}
|
||||
public void processPA(int number) { inputProcessor.processPA(number); }
|
||||
|
||||
/** Send Clear key. */
|
||||
public void sendClear() {
|
||||
inputProcessor.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||
}
|
||||
public void processClear() { inputProcessor.processClear(); }
|
||||
|
||||
/** Move cursor up. */
|
||||
public void cursorUp() { inputProcessor.cursorUp(); }
|
||||
public void processCursorUp() { inputProcessor.processCursorUp(); }
|
||||
|
||||
/** Move cursor down. */
|
||||
public void cursorDown() { inputProcessor.cursorDown(); }
|
||||
public void processCursorDown() { inputProcessor.processCursorDown(); }
|
||||
|
||||
/** Move cursor left. */
|
||||
public void cursorLeft() { inputProcessor.cursorLeft(); }
|
||||
public void processCursorLeft() { inputProcessor.processCursorLeft(); }
|
||||
|
||||
/** Move cursor right. */
|
||||
public void cursorRight() { inputProcessor.cursorRight(); }
|
||||
public void processCursorRight() { inputProcessor.processCursorRight(); }
|
||||
|
||||
/** Move cursor to home position. */
|
||||
public void cursorHome() { inputProcessor.cursorHome(); }
|
||||
public void processHome() { inputProcessor.processHome(); }
|
||||
|
||||
/** Tab to next unprotected field. */
|
||||
public void tab() { inputProcessor.tab(); }
|
||||
public void processTab() { inputProcessor.processTab(); }
|
||||
|
||||
/** Back-tab to previous unprotected field. */
|
||||
public void backTab() { inputProcessor.backTab(); }
|
||||
public void processBackTab() { inputProcessor.processBackTab(); }
|
||||
|
||||
/** Move cursor to next line. */
|
||||
public void newline() { inputProcessor.newline(); }
|
||||
public void processNewline() { inputProcessor.processNewline(); }
|
||||
|
||||
/** Delete character under cursor. */
|
||||
public void deleteChar() { inputProcessor.deleteChar(); }
|
||||
public void processDelete() { inputProcessor.processDelete(); }
|
||||
|
||||
/** Backspace character before cursor. */
|
||||
public void backspace() { inputProcessor.backspace(); }
|
||||
public void processBackspace() { inputProcessor.processBackspace(); }
|
||||
|
||||
/** Erase to end of field. */
|
||||
public void eraseEof() { inputProcessor.eraseEof(); }
|
||||
public void processEraseEOF() { inputProcessor.processEraseEOF(); }
|
||||
|
||||
/** Erase all unprotected fields. */
|
||||
public void eraseInput() { inputProcessor.eraseInput(); }
|
||||
public void processEraseInput() { inputProcessor.processEraseInput(); }
|
||||
|
||||
/** Insert Duplicate order. */
|
||||
public void dup() { inputProcessor.dup(); }
|
||||
public void processDup() { inputProcessor.processDup(); }
|
||||
|
||||
/** Insert Field Mark order. */
|
||||
public void fieldMark() { inputProcessor.fieldMark(); }
|
||||
public void processFieldMark() { inputProcessor.processFieldMark(); }
|
||||
|
||||
/** Toggle Insert Mode. */
|
||||
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
|
||||
public void processToggleInsert() { inputProcessor.processToggleInsert(); }
|
||||
|
||||
/** Move word left. */
|
||||
public void processWordLeft() { inputProcessor.processWordLeft(); }
|
||||
|
||||
/** Move word right. */
|
||||
public void processWordRight() { inputProcessor.processWordRight(); }
|
||||
|
||||
/** Move to field end. */
|
||||
public void processFieldEnd() { inputProcessor.processFieldEnd(); }
|
||||
|
||||
/** Attention key. */
|
||||
public void attn() { inputProcessor.attn(); }
|
||||
public void processAttn() { inputProcessor.processAttn(); }
|
||||
|
||||
/** SysReq key. */
|
||||
public void sysReq() { inputProcessor.sysReq(); }
|
||||
public void processSysReq() { inputProcessor.processSysReq(); }
|
||||
|
||||
/** Reset (unlock keyboard, reset OIA). */
|
||||
public void reset() { inputProcessor.reset(); }
|
||||
public void processReset() { inputProcessor.processReset(); }
|
||||
|
||||
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
|
||||
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
|
||||
public boolean processCurSel() { return inputProcessor.processCurSel(); }
|
||||
public boolean processCursorSelect() { return inputProcessor.processCursorSelect(); }
|
||||
|
||||
/** Trigger Light Pen selection at the specified screen address or cursor position. */
|
||||
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
|
||||
public boolean processLightPen() { return inputProcessor.processLightPen(); }
|
||||
public boolean processLightPen(int addr) { return inputProcessor.processLightPen(addr); }
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
|
||||
return dsProcessor.getProgramSymbolManager();
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GraphicsPlane getGraphicsPlane() {
|
||||
return dsProcessor.getGraphicsPlane();
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return dsProcessor.getGocaDecoder();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package org.lib3270j;
|
||||
package haus.nightmare.lib3270j;
|
||||
|
||||
import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Terminal model definitions for IBM 3278 and 3279 terminals.
|
||||
@@ -0,0 +1,158 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base implementation for Single-Byte Character Set (SBCS) EBCDIC Code Pages.
|
||||
*/
|
||||
public abstract class AbstractCodePage implements CodePage {
|
||||
|
||||
protected final String id;
|
||||
protected final String description;
|
||||
protected final int cpgid;
|
||||
protected final int cgcsgid;
|
||||
|
||||
protected final int[] toUnicode = new int[256];
|
||||
protected final int[] toEbcdic = new int[256];
|
||||
protected final Map<Character, Integer> extendedToEbcdic = new HashMap<>();
|
||||
|
||||
public AbstractCodePage(String id, String description, int cpgid, int cgcsgid, int[] unicodeMapping) {
|
||||
this(id, description, cpgid, cgcsgid, null, unicodeMapping);
|
||||
}
|
||||
|
||||
public AbstractCodePage(String id, String description, int cpgid, int cgcsgid, String javaCharsetName, int[] unicodeMapping) {
|
||||
this.id = id;
|
||||
this.description = description;
|
||||
this.cpgid = cpgid;
|
||||
this.cgcsgid = cgcsgid;
|
||||
|
||||
int[] map = loadMapping(javaCharsetName, unicodeMapping);
|
||||
System.arraycopy(map, 0, this.toUnicode, 0, 256);
|
||||
initReverseMapping();
|
||||
}
|
||||
|
||||
public static int[] loadMapping(String charsetName, int[] fallback) {
|
||||
if (charsetName != null) {
|
||||
String[] candidates = {
|
||||
charsetName,
|
||||
"IBM" + charsetName,
|
||||
"Cp" + charsetName,
|
||||
"IBM-" + charsetName,
|
||||
"x-IBM" + charsetName
|
||||
};
|
||||
for (String name : candidates) {
|
||||
try {
|
||||
if (Charset.isSupported(name)) {
|
||||
Charset cs = Charset.forName(name);
|
||||
int[] map = new int[256];
|
||||
for (int i = 0; i < 256; i++) {
|
||||
byte[] b = new byte[]{(byte) i};
|
||||
String s = new String(b, cs);
|
||||
map[i] = (!s.isEmpty() && s.charAt(0) != '\uFFFD') ? s.charAt(0) : (fallback != null ? fallback[i] : 0);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
return fallback != null ? fallback : new int[256];
|
||||
}
|
||||
|
||||
protected void initReverseMapping() {
|
||||
Arrays.fill(toEbcdic, -1);
|
||||
for (int i = 0; i < 256; i++) {
|
||||
int uc = toUnicode[i];
|
||||
if (uc >= 0 && uc < 256) {
|
||||
if (toEbcdic[uc] == -1) {
|
||||
toEbcdic[uc] = i;
|
||||
}
|
||||
} else if (uc >= 256) {
|
||||
extendedToEbcdic.putIfAbsent((char) uc, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCodePageId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCpgid() {
|
||||
return cpgid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCgcsgid() {
|
||||
return cgcsgid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDBCS() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char ebcdicToUnicode(int ebc) {
|
||||
return (char) toUnicode[ebc & 0xFF];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int unicodeToEbcdic(char unicode) {
|
||||
int u = unicode;
|
||||
if (u >= 0 && u < 256) {
|
||||
return toEbcdic[u];
|
||||
}
|
||||
Integer val = extendedToEbcdic.get(unicode);
|
||||
return val != null ? val : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte unicodeToEbcdicSafe(char unicode) {
|
||||
int ebc = unicodeToEbcdic(unicode);
|
||||
return (byte) (ebc >= 0 ? ebc : 0x40);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char dbcsToUnicode(int b1, int b2) {
|
||||
return '\uFFFD';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int unicodeToDbcs(char unicode) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
|
||||
if (ebcdic == null || length <= 0) return "";
|
||||
char[] chars = new char[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
chars[i] = ebcdicToUnicode(ebcdic[offset + i]);
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] stringToEbcdic(String s) {
|
||||
if (s == null || s.isEmpty()) return new byte[0];
|
||||
byte[] bytes = new byte[s.length()];
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
bytes[i] = unicodeToEbcdicSafe(s.charAt(i));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return id + " (" + description + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base implementation for Mixed Double-Byte Character Set (DBCS) EBCDIC Code Pages
|
||||
* (e.g. Japanese Katakana 930, Japanese Latin 939, Chinese 935/937/1388, Korean 933).
|
||||
*/
|
||||
public abstract class AbstractDBCSCodePage extends AbstractCodePage {
|
||||
|
||||
public static final int SO = 0x0E; // Shift Out (enter DBCS mode)
|
||||
public static final int SI = 0x0F; // Shift In (enter SBCS mode)
|
||||
|
||||
protected final Map<Integer, Character> dbcsToUnicodeMap = new HashMap<>();
|
||||
protected final Map<Character, Integer> unicodeToDbcsMap = new HashMap<>();
|
||||
protected Charset nioCharset;
|
||||
|
||||
public AbstractDBCSCodePage(String id, String description, int cpgid, int cgcsgid,
|
||||
int[] sbcsMapping, String nioCharsetName) {
|
||||
super(id, description, cpgid, cgcsgid, sbcsMapping);
|
||||
if (nioCharsetName != null) {
|
||||
try {
|
||||
if (Charset.isSupported(nioCharsetName)) {
|
||||
this.nioCharset = Charset.forName(nioCharsetName);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
// Initialize ideographic space (0x4040 <-> \u3000)
|
||||
registerDbcsPair(0x4040, '\u3000');
|
||||
}
|
||||
|
||||
public void registerDbcsPair(int ebcdicDBCS, char unicodeChar) {
|
||||
dbcsToUnicodeMap.put(ebcdicDBCS, unicodeChar);
|
||||
unicodeToDbcsMap.put(unicodeChar, ebcdicDBCS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDBCS() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char dbcsToUnicode(int b1, int b2) {
|
||||
int key = ((b1 & 0xFF) << 8) | (b2 & 0xFF);
|
||||
Character c = dbcsToUnicodeMap.get(key);
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
|
||||
if (nioCharset != null) {
|
||||
try {
|
||||
byte[] raw = new byte[] { (byte) SO, (byte) b1, (byte) b2, (byte) SI };
|
||||
CharBuffer cb = nioCharset.decode(ByteBuffer.wrap(raw));
|
||||
if (cb.hasRemaining()) {
|
||||
char decoded = cb.get();
|
||||
if (decoded != '\uFFFD' && decoded != 0) {
|
||||
registerDbcsPair(key, decoded);
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
return '?';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int unicodeToDbcs(char unicode) {
|
||||
Integer val = unicodeToDbcsMap.get(unicode);
|
||||
if (val != null) {
|
||||
return val;
|
||||
}
|
||||
|
||||
if (nioCharset != null) {
|
||||
try {
|
||||
CharsetEncoder encoder = nioCharset.newEncoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
ByteBuffer bb = encoder.encode(CharBuffer.wrap(new char[]{unicode}));
|
||||
byte[] bytes = new byte[bb.remaining()];
|
||||
bb.get(bytes);
|
||||
|
||||
// Check if encoded as SO + b1 + b2 + SI or b1 + b2
|
||||
if (bytes.length >= 4 && (bytes[0] & 0xFF) == SO) {
|
||||
int key = ((bytes[1] & 0xFF) << 8) | (bytes[2] & 0xFF);
|
||||
registerDbcsPair(key, unicode);
|
||||
return key;
|
||||
} else if (bytes.length == 2) {
|
||||
int key = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
|
||||
registerDbcsPair(key, unicode);
|
||||
return key;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
|
||||
return ebcdicToString(ebcdic, offset, length, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate an EBCDIC byte array slice to a Unicode String with optional SO/SI escape preservation.
|
||||
*
|
||||
* @param ebcdic source byte array
|
||||
* @param offset start offset
|
||||
* @param length number of bytes
|
||||
* @param preserveSOSI if true, preserves Shift-Out (\u000E) and Shift-In (\u000F) control characters
|
||||
* @return translated Unicode String
|
||||
*/
|
||||
public String ebcdicToString(byte[] ebcdic, int offset, int length, boolean preserveSOSI) {
|
||||
if (ebcdic == null || length <= 0) return "";
|
||||
|
||||
StringBuilder sb = new StringBuilder(length);
|
||||
boolean inDBCS = false;
|
||||
int end = Math.min(ebcdic.length, offset + length);
|
||||
int i = offset;
|
||||
|
||||
while (i < end) {
|
||||
int b = ebcdic[i] & 0xFF;
|
||||
if (b == SO) {
|
||||
inDBCS = true;
|
||||
if (preserveSOSI) {
|
||||
sb.append((char) SO);
|
||||
}
|
||||
i++;
|
||||
} else if (b == SI) {
|
||||
inDBCS = false;
|
||||
if (preserveSOSI) {
|
||||
sb.append((char) SI);
|
||||
}
|
||||
i++;
|
||||
} else if (inDBCS) {
|
||||
if (i + 1 < end) {
|
||||
int b2 = ebcdic[i + 1] & 0xFF;
|
||||
if (b2 == SI) {
|
||||
// Orphaned single byte before SI
|
||||
inDBCS = false;
|
||||
if (preserveSOSI) {
|
||||
sb.append(ebcdicToUnicode(b));
|
||||
sb.append((char) SI);
|
||||
}
|
||||
i += 2;
|
||||
} else if (b2 == SO) {
|
||||
// Unmatched byte followed by another SO
|
||||
sb.append(ebcdicToUnicode(b));
|
||||
i++;
|
||||
} else {
|
||||
sb.append(dbcsToUnicode(b, b2));
|
||||
i += 2;
|
||||
}
|
||||
} else {
|
||||
// Trailing single byte inside unclosed DBCS shift
|
||||
sb.append(ebcdicToUnicode(b));
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
sb.append(ebcdicToUnicode(b));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] stringToEbcdic(String s) {
|
||||
return stringToEbcdic(s, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a Unicode String to EBCDIC byte array with optional parsing of embedded SO/SI markers.
|
||||
*
|
||||
* @param s input Unicode string
|
||||
* @param parseSOSIMarkers whether to interpret embedded \u000E (SO) and \u000F (SI) characters
|
||||
* @return EBCDIC byte array with balanced SO/SI framing
|
||||
*/
|
||||
public byte[] stringToEbcdic(String s, boolean parseSOSIMarkers) {
|
||||
if (s == null || s.isEmpty()) return new byte[0];
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(s.length() * 2);
|
||||
boolean inDBCS = false;
|
||||
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
|
||||
if (parseSOSIMarkers && c == (char) SO) {
|
||||
if (!inDBCS) {
|
||||
out.write(SO);
|
||||
inDBCS = true;
|
||||
}
|
||||
continue;
|
||||
} else if (parseSOSIMarkers && c == (char) SI) {
|
||||
if (inDBCS) {
|
||||
out.write(SI);
|
||||
inDBCS = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int dbcsCode = unicodeToDbcs(c);
|
||||
|
||||
if (dbcsCode >= 0) {
|
||||
// Character is DBCS
|
||||
if (!inDBCS) {
|
||||
out.write(SO);
|
||||
inDBCS = true;
|
||||
}
|
||||
out.write((dbcsCode >> 8) & 0xFF);
|
||||
out.write(dbcsCode & 0xFF);
|
||||
} else {
|
||||
// Character is SBCS
|
||||
if (inDBCS) {
|
||||
out.write(SI);
|
||||
inDBCS = false;
|
||||
}
|
||||
out.write(unicodeToEbcdicSafe(c));
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-close unclosed DBCS shift sequence
|
||||
if (inDBCS) {
|
||||
out.write(SI);
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to decode an EBCDIC byte array to UTF-8 String preserving SO/SI control codes.
|
||||
*/
|
||||
public String ebcdicToUtf8WithSOSI(byte[] ebcdic) {
|
||||
if (ebcdic == null) return "";
|
||||
return ebcdicToString(ebcdic, 0, ebcdic.length, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to encode a UTF-8/Unicode String with embedded SO/SI markers to EBCDIC.
|
||||
*/
|
||||
public byte[] utf8WithSOSIToEbcdic(String s) {
|
||||
return stringToEbcdic(s, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* Interface defining character translation and metadata for EBCDIC code pages
|
||||
* conforming to IBM Host On-Demand (HoD v14) converters specification.
|
||||
*/
|
||||
public interface CodePage {
|
||||
|
||||
/**
|
||||
* Get the primary identifier for this code page (e.g. "037", "1047", "500", "273").
|
||||
*/
|
||||
String getCodePageId();
|
||||
|
||||
/**
|
||||
* Get a human-readable description (e.g. "US / Canada - EBCDIC").
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Get the Code Page Global ID (CPGID / CCSID) for 3270 query replies.
|
||||
*/
|
||||
int getCpgid();
|
||||
|
||||
/**
|
||||
* Get the Graphic Character Set Global ID (GCSGID) for 3270 query replies.
|
||||
*/
|
||||
int getCgcsgid();
|
||||
|
||||
/**
|
||||
* Translate an EBCDIC byte (0x00-0xFF) to a Unicode character.
|
||||
*/
|
||||
char ebcdicToUnicode(int ebc);
|
||||
|
||||
/**
|
||||
* Translate a Unicode character to an EBCDIC byte value.
|
||||
* Returns -1 if unmappable.
|
||||
*/
|
||||
int unicodeToEbcdic(char unicode);
|
||||
|
||||
/**
|
||||
* Translate a Unicode character to an EBCDIC byte value, returning a safe
|
||||
* fallback (default EBCDIC space 0x40) if unmappable.
|
||||
*/
|
||||
byte unicodeToEbcdicSafe(char unicode);
|
||||
|
||||
/**
|
||||
* Translate an EBCDIC byte array slice to a Unicode String.
|
||||
*/
|
||||
String ebcdicToString(byte[] ebcdic, int offset, int length);
|
||||
|
||||
/**
|
||||
* Translate a Unicode String to an EBCDIC byte array.
|
||||
*/
|
||||
byte[] stringToEbcdic(String s);
|
||||
|
||||
/**
|
||||
* Check if this code page is a double-byte (DBCS) / mixed SBCS+DBCS code page.
|
||||
*/
|
||||
boolean isDBCS();
|
||||
|
||||
/**
|
||||
* Translate a double-byte EBCDIC pair (b1, b2) to a Unicode character.
|
||||
* Returns '\uFFFD' or '?' if unmappable.
|
||||
*/
|
||||
char dbcsToUnicode(int b1, int b2);
|
||||
|
||||
/**
|
||||
* Translate a Unicode character to a double-byte EBCDIC code ((b1 << 8) | b2).
|
||||
* Returns -1 if unmappable.
|
||||
*/
|
||||
int unicodeToDbcs(char unicode);
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Registry and factory for 3270 EBCDIC Code Pages.
|
||||
* Manages built-in SBCS/DBCS codepages, alias normalization, and dynamic JVM Charset resolution.
|
||||
*/
|
||||
public class CodePageRegistry {
|
||||
|
||||
private static final Logger log = Logger.getLogger(CodePageRegistry.class.getName());
|
||||
|
||||
private static final Map<String, CodePage> CODE_PAGES = new LinkedHashMap<>();
|
||||
private static final Map<String, String> ALIASES = new HashMap<>();
|
||||
|
||||
static {
|
||||
// Register core SBCS codepages
|
||||
register(new Cp037());
|
||||
register(new Cp1047());
|
||||
register(new Cp500());
|
||||
register(new Cp273());
|
||||
register(new Cp277());
|
||||
register(new Cp278());
|
||||
register(new Cp280());
|
||||
register(new Cp284());
|
||||
register(new Cp285());
|
||||
register(new Cp297());
|
||||
register(new Cp870());
|
||||
register(new Cp871());
|
||||
register(new Cp875());
|
||||
register(new Cp1026());
|
||||
|
||||
// Register Euro variants (1140-1149)
|
||||
register(new CpEuroVariants.Cp1140());
|
||||
register(new CpEuroVariants.Cp1141());
|
||||
register(new CpEuroVariants.Cp1142());
|
||||
register(new CpEuroVariants.Cp1143());
|
||||
register(new CpEuroVariants.Cp1144());
|
||||
register(new CpEuroVariants.Cp1145());
|
||||
register(new CpEuroVariants.Cp1146());
|
||||
register(new CpEuroVariants.Cp1147());
|
||||
register(new CpEuroVariants.Cp1148());
|
||||
register(new CpEuroVariants.Cp1149());
|
||||
|
||||
// Register DBCS mixed codepages
|
||||
register(new Cp930());
|
||||
register(new Cp939());
|
||||
register(new Cp935());
|
||||
register(new Cp935.Cp1388());
|
||||
register(new Cp937());
|
||||
register(new Cp937.Cp1371());
|
||||
register(new Cp933());
|
||||
|
||||
// Register Phase 8 Extended EBCDIC Codepages (Arabic, Hebrew, Thai, Cyrillic, Turkish, Extended DBCS)
|
||||
registerExtendedCodepages();
|
||||
|
||||
// Setup common aliases
|
||||
addAlias("us", "037");
|
||||
addAlias("usa", "037");
|
||||
addAlias("ebcdic-cp-us", "037");
|
||||
addAlias("ebcdic-cp-ca", "037");
|
||||
addAlias("ebcdic-cp-nl", "037");
|
||||
|
||||
addAlias("posix", "1047");
|
||||
addAlias("unix", "1047");
|
||||
addAlias("open-systems", "1047");
|
||||
addAlias("zos-unix", "1047");
|
||||
|
||||
addAlias("intl", "500");
|
||||
addAlias("international", "500");
|
||||
addAlias("ebcdic-cp-ch", "500");
|
||||
|
||||
addAlias("de", "273");
|
||||
addAlias("germany", "273");
|
||||
addAlias("austria", "273");
|
||||
addAlias("ebcdic-cp-de", "273");
|
||||
|
||||
addAlias("dk", "277");
|
||||
addAlias("no", "277");
|
||||
addAlias("denmark", "277");
|
||||
addAlias("norway", "277");
|
||||
addAlias("ebcdic-cp-dk", "277");
|
||||
addAlias("ebcdic-cp-no", "277");
|
||||
|
||||
addAlias("se", "278");
|
||||
addAlias("fi", "278");
|
||||
addAlias("sweden", "278");
|
||||
addAlias("finland", "278");
|
||||
addAlias("ebcdic-cp-se", "278");
|
||||
addAlias("ebcdic-cp-fi", "278");
|
||||
|
||||
addAlias("it", "280");
|
||||
addAlias("italy", "280");
|
||||
addAlias("ebcdic-cp-it", "280");
|
||||
|
||||
addAlias("es", "284");
|
||||
addAlias("spain", "284");
|
||||
addAlias("latin-america", "284");
|
||||
addAlias("ebcdic-cp-es", "284");
|
||||
|
||||
addAlias("uk", "285");
|
||||
addAlias("gb", "285");
|
||||
addAlias("great-britain", "285");
|
||||
addAlias("ebcdic-cp-gb", "285");
|
||||
|
||||
addAlias("fr", "297");
|
||||
addAlias("france", "297");
|
||||
addAlias("ebcdic-cp-fr", "297");
|
||||
|
||||
addAlias("latin2", "870");
|
||||
addAlias("pl", "870");
|
||||
addAlias("cz", "870");
|
||||
addAlias("hu", "870");
|
||||
addAlias("ebcdic-cp-roece", "870");
|
||||
|
||||
addAlias("is", "871");
|
||||
addAlias("iceland", "871");
|
||||
addAlias("ebcdic-cp-is", "871");
|
||||
|
||||
addAlias("gr", "875");
|
||||
addAlias("greece", "875");
|
||||
addAlias("greek", "875");
|
||||
addAlias("ebcdic-cp-gr", "875");
|
||||
|
||||
addAlias("tr", "1026");
|
||||
addAlias("turkey", "1026");
|
||||
addAlias("turkish", "1026");
|
||||
addAlias("ebcdic-cp-tr", "1026");
|
||||
|
||||
addAlias("ja", "930");
|
||||
addAlias("japanese", "930");
|
||||
addAlias("katakana", "930");
|
||||
addAlias("ja-latin", "939");
|
||||
|
||||
addAlias("zh", "935");
|
||||
addAlias("chinese-simplified", "935");
|
||||
addAlias("zh-tw", "937");
|
||||
addAlias("chinese-traditional", "937");
|
||||
|
||||
addAlias("ko", "933");
|
||||
addAlias("korean", "933");
|
||||
}
|
||||
|
||||
public static void register(CodePage cp) {
|
||||
if (cp != null) {
|
||||
CODE_PAGES.put(cp.getCodePageId(), cp);
|
||||
}
|
||||
}
|
||||
|
||||
public static void addAlias(String alias, String targetId) {
|
||||
if (alias != null && targetId != null) {
|
||||
ALIASES.put(normalizeKey(alias), targetId);
|
||||
}
|
||||
}
|
||||
|
||||
public static String normalizeKey(String name) {
|
||||
if (name == null) return "";
|
||||
String s = name.trim().toLowerCase();
|
||||
s = s.replace("_", "").replace("-", "");
|
||||
if (s.startsWith("ebcdiccp")) {
|
||||
s = s.substring(8);
|
||||
} else if (s.startsWith("ebcdic")) {
|
||||
s = s.substring(6);
|
||||
} else if (s.startsWith("ibm")) {
|
||||
s = s.substring(3);
|
||||
} else if (s.startsWith("cp")) {
|
||||
s = s.substring(2);
|
||||
} else if (s.startsWith("ccsid")) {
|
||||
s = s.substring(5);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a code page by ID or alias.
|
||||
* If not found in built-ins, attempts to load via java.nio.charset.Charset.
|
||||
* Falls back to CP037 if completely unresolvable.
|
||||
*/
|
||||
public static CodePage getCodePage(String name) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return CODE_PAGES.get("037");
|
||||
}
|
||||
|
||||
String raw = name.trim();
|
||||
// Direct match
|
||||
CodePage cp = CODE_PAGES.get(raw);
|
||||
if (cp != null) return cp;
|
||||
|
||||
// Normalized key lookup
|
||||
String norm = normalizeKey(raw);
|
||||
cp = CODE_PAGES.get(norm);
|
||||
if (cp != null) return cp;
|
||||
|
||||
// Alias lookup
|
||||
String targetId = ALIASES.get(norm);
|
||||
if (targetId != null) {
|
||||
cp = CODE_PAGES.get(targetId);
|
||||
if (cp != null) return cp;
|
||||
}
|
||||
|
||||
// Try standard NIO Charset dynamic adapter
|
||||
try {
|
||||
if (Charset.isSupported(raw)) {
|
||||
return new NioCodePageAdapter(raw);
|
||||
}
|
||||
String ibmName = "IBM" + norm;
|
||||
if (Charset.isSupported(ibmName)) {
|
||||
return new NioCodePageAdapter(ibmName);
|
||||
}
|
||||
String cpName = "Cp" + norm;
|
||||
if (Charset.isSupported(cpName)) {
|
||||
return new NioCodePageAdapter(cpName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.fine("Dynamic charset loading failed for " + name + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
log.warning("CodePage not recognized: '" + name + "'; falling back to CP037");
|
||||
return CODE_PAGES.get("037");
|
||||
}
|
||||
|
||||
/**
|
||||
* Register extended EBCDIC codepages from HoD v14 converters:
|
||||
* - Arabic: Cp420, Cp424
|
||||
* - Hebrew: Cp424, Cp803
|
||||
* - Thai: Cp838, Cp1160
|
||||
* - Cyrillic: Cp1025, Cp1123, Cp1154, Cp880
|
||||
* - Turkish: Cp1155, Cp905
|
||||
* - Extended Chinese DBCS: Cp1388, Cp1371
|
||||
*/
|
||||
public static void registerExtendedCodepages() {
|
||||
// Arabic & Hebrew
|
||||
register(new Cp420());
|
||||
register(new Cp424());
|
||||
register(new Cp803());
|
||||
|
||||
// Thai
|
||||
register(new Cp838());
|
||||
register(new Cp1160());
|
||||
|
||||
// Cyrillic / Russian / Ukrainian
|
||||
register(new Cp1025());
|
||||
register(new Cp1123());
|
||||
register(new Cp1154());
|
||||
register(new Cp880());
|
||||
|
||||
// Turkish
|
||||
register(new Cp1155());
|
||||
register(new Cp905());
|
||||
|
||||
// Extended Chinese DBCS
|
||||
register(new Cp1388());
|
||||
register(new Cp1371());
|
||||
|
||||
// Setup Extended Aliases
|
||||
addAlias("ar", "420");
|
||||
addAlias("arabic", "420");
|
||||
addAlias("ebcdic-cp-ar", "420");
|
||||
|
||||
addAlias("he", "424");
|
||||
addAlias("hebrew", "424");
|
||||
addAlias("hebrew-lowercase", "424");
|
||||
addAlias("ebcdic-cp-he", "424");
|
||||
|
||||
addAlias("hebrew-old", "803");
|
||||
addAlias("israel", "803");
|
||||
addAlias("iw", "803");
|
||||
addAlias("ebcdic-cp-he-old", "803");
|
||||
|
||||
addAlias("th", "838");
|
||||
addAlias("thai", "838");
|
||||
addAlias("ebcdic-cp-th", "838");
|
||||
addAlias("thai-euro", "1160");
|
||||
|
||||
addAlias("ru", "1025");
|
||||
addAlias("russian", "1025");
|
||||
addAlias("cyrillic", "1025");
|
||||
addAlias("ebcdic-cp-ru", "1025");
|
||||
|
||||
addAlias("ukraine", "1123");
|
||||
addAlias("ukrainian", "1123");
|
||||
addAlias("ebcdic-cp-ua", "1123");
|
||||
|
||||
addAlias("cyrillic-euro", "1154");
|
||||
addAlias("ru-euro", "1154");
|
||||
addAlias("russian-euro", "1154");
|
||||
|
||||
addAlias("cyrillic-russian", "880");
|
||||
addAlias("ru-old", "880");
|
||||
|
||||
addAlias("turkish-euro", "1155");
|
||||
addAlias("tr-euro", "1155");
|
||||
addAlias("turkish-latin3", "905");
|
||||
addAlias("tr-latin3", "905");
|
||||
|
||||
addAlias("chinese-ext-simplified", "1388");
|
||||
addAlias("zh-simplified-ext", "1388");
|
||||
addAlias("zh-ext", "1388");
|
||||
addAlias("chinese-ext-traditional", "1371");
|
||||
addAlias("zh-traditional-ext", "1371");
|
||||
addAlias("zh-tw-ext", "1371");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a code page is registered.
|
||||
*/
|
||||
public static boolean hasCodePage(String name) {
|
||||
if (name == null || name.trim().isEmpty()) return false;
|
||||
String raw = name.trim();
|
||||
if (CODE_PAGES.containsKey(raw)) return true;
|
||||
String norm = normalizeKey(raw);
|
||||
if (CODE_PAGES.containsKey(norm)) return true;
|
||||
return ALIASES.containsKey(norm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an unmodifiable list of all registered built-in CodePages.
|
||||
*/
|
||||
public static List<CodePage> getAvailableCodePages() {
|
||||
return Collections.unmodifiableList(new ArrayList<>(CODE_PAGES.values()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of all registered built-in CodePage IDs.
|
||||
*/
|
||||
public static String[] getAvailableCodePageIds() {
|
||||
return CODE_PAGES.keySet().toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic fallback adapter wrapping any JVM java.nio.charset.Charset.
|
||||
*/
|
||||
public static class NioCodePageAdapter extends AbstractCodePage {
|
||||
private final Charset charset;
|
||||
|
||||
public NioCodePageAdapter(String charsetName) {
|
||||
this(Charset.forName(charsetName));
|
||||
}
|
||||
|
||||
public NioCodePageAdapter(Charset charset) {
|
||||
super(charset.name(), "JVM Charset: " + charset.displayName(), parseCpgid(charset.name()), 697, buildMappingFromCharset(charset));
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
private static int parseCpgid(String name) {
|
||||
try {
|
||||
String num = name.replaceAll("\\D+", "");
|
||||
if (!num.isEmpty()) {
|
||||
return Integer.parseInt(num);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return 37;
|
||||
}
|
||||
|
||||
private static int[] buildMappingFromCharset(Charset cs) {
|
||||
int[] map = new int[256];
|
||||
for (int i = 0; i < 256; i++) {
|
||||
byte[] b = new byte[]{(byte) i};
|
||||
String s = new String(b, cs);
|
||||
map[i] = !s.isEmpty() ? s.charAt(0) : (char) i;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-72
@@ -1,16 +1,17 @@
|
||||
package org.lib3270j.charset;
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* EBCDIC ↔ Unicode translator.
|
||||
* Default: Code Page 037 (US/Canada EBCDIC).
|
||||
* IBM Code Page 037 (US / Canada / Netherlands / Portugal / Brazil).
|
||||
* CCSID / CPGID: 37, GCSGID: 697.
|
||||
*/
|
||||
public class EbcdicTranslator {
|
||||
public class Cp037 extends AbstractCodePage {
|
||||
|
||||
/**
|
||||
* EBCDIC Code Page 037 to Unicode mapping.
|
||||
* Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint.
|
||||
*/
|
||||
private static final int[] CP037_TO_UNICODE = {
|
||||
public static final String ID = "037";
|
||||
public static final String DESCRIPTION = "US / Canada / Brazil - EBCDIC";
|
||||
public static final int CPGID = 37;
|
||||
public static final int GCSGID = 697;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
@@ -61,68 +62,7 @@ public class EbcdicTranslator {
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
/**
|
||||
* Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1).
|
||||
* Index is the Unicode codepoint (0x00-0xFF), value is the EBCDIC byte (-1 if unmappable).
|
||||
*/
|
||||
private static final int[] UNICODE_TO_CP037 = new int[256];
|
||||
|
||||
static {
|
||||
// Build reverse mapping
|
||||
java.util.Arrays.fill(UNICODE_TO_CP037, -1);
|
||||
for (int i = 0; i < 256; i++) {
|
||||
int unicode = CP037_TO_UNICODE[i];
|
||||
if (unicode < 256) {
|
||||
UNICODE_TO_CP037[unicode] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate EBCDIC byte to Unicode character.
|
||||
*/
|
||||
public char ebcdicToUnicode(int ebc) {
|
||||
return (char) CP037_TO_UNICODE[ebc & 0xFF];
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate Unicode character to EBCDIC byte.
|
||||
* Returns -1 if the character cannot be mapped.
|
||||
*/
|
||||
public int unicodeToEbcdic(char unicode) {
|
||||
if (unicode < 256) {
|
||||
return UNICODE_TO_CP037[unicode];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
|
||||
*/
|
||||
public byte unicodeToEbcdicSafe(char unicode) {
|
||||
int ebc = unicodeToEbcdic(unicode);
|
||||
return (byte) (ebc >= 0 ? ebc : 0x40);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a byte array from EBCDIC to a Unicode string.
|
||||
*/
|
||||
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
|
||||
StringBuilder sb = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(ebcdicToUnicode(ebcdic[offset + i] & 0xFF));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a Unicode string to EBCDIC byte array.
|
||||
*/
|
||||
public byte[] stringToEbcdic(String s) {
|
||||
byte[] result = new byte[s.length()];
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
result[i] = unicodeToEbcdicSafe(s.charAt(i));
|
||||
}
|
||||
return result;
|
||||
public Cp037() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "037", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1025 (Cyrillic Multilingual EBCDIC - Russian, Bulgarian, Belarusian, Serbian, Macedonian).
|
||||
* CCSID / CPGID: 1025, GCSGID: 1150.
|
||||
*/
|
||||
public class Cp1025 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "1025";
|
||||
public static final String DESCRIPTION = "Cyrillic Multilingual EBCDIC";
|
||||
public static final int CPGID = 1025;
|
||||
public static final int GCSGID = 1150;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A, 0x0449,
|
||||
0x0448, 0x0447, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441, 0x0440,
|
||||
0x043F, 0x043E, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439, 0x0438,
|
||||
0x0437, 0x0436, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451, 0x0453,
|
||||
0x0452, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x0454, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x0455, 0x0456, 0x0457, 0x0458, 0x0459, 0x045A,
|
||||
// 90-9F
|
||||
0x045B, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x045C, 0x045E, 0x045F, 0x0401, 0x0402, 0x0403,
|
||||
// A0-AF
|
||||
0x0404, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, 0x040A,
|
||||
// B0-BF
|
||||
0x040B, 0x00A3, 0x040C, 0x00B7, 0x00A9, 0x040E, 0x040F, 0x0410,
|
||||
0x0411, 0x0412, 0x00AC, 0x007C, 0x0413, 0x0414, 0x0415, 0x0416,
|
||||
// C0-CF
|
||||
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C,
|
||||
// D0-DF
|
||||
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422,
|
||||
// E0-EF
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
|
||||
};
|
||||
|
||||
public Cp1025() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "1025", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1026 (Turkey - Turkish Latin-5 EBCDIC).
|
||||
* CCSID / CPGID: 1026, GCSGID: 1152.
|
||||
*/
|
||||
public class Cp1026 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "1026";
|
||||
public static final String DESCRIPTION = "Turkey - Turkish Latin-5 EBCDIC";
|
||||
public static final int CPGID = 1026;
|
||||
public static final int GCSGID = 1152;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x015E, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x00D6, 0x00C7, 0x00DC, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x0130, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x00F6, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x011E, 0x00F4, 0x0131, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x00E7, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x011F, 0x00FB, 0x015F, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x00FC, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00A6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp1026() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "1026", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1047 (IBM Open Systems / z/OS Unix System Services Latin-1).
|
||||
* CCSID / CPGID: 1047, GCSGID: 103.
|
||||
*/
|
||||
public class Cp1047 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "1047";
|
||||
public static final String DESCRIPTION = "IBM Open Systems / z/OS Unix Latin-1";
|
||||
public static final int CPGID = 1047;
|
||||
public static final int GCSGID = 103;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x000A, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x005B, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x00AC, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00DD, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp1047() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "1047", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1123 (Cyrillic Ukraine EBCDIC).
|
||||
* CCSID / CPGID: 1123, GCSGID: 1399.
|
||||
*/
|
||||
public class Cp1123 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "1123";
|
||||
public static final String DESCRIPTION = "Cyrillic Ukraine EBCDIC";
|
||||
public static final int CPGID = 1123;
|
||||
public static final int GCSGID = 1399;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x044F, 0x044E, 0x044D, 0x044C, 0x044B, 0x044A, 0x0449,
|
||||
0x0448, 0x0447, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x0446, 0x0445, 0x0444, 0x0443, 0x0442, 0x0441, 0x0440,
|
||||
0x043F, 0x043E, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x043D, 0x043C, 0x043B, 0x043A, 0x0439, 0x0438,
|
||||
0x0437, 0x0436, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x0435, 0x0434, 0x0433, 0x0432, 0x0431, 0x0430, 0x0451, 0x0491,
|
||||
0x0454, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x0456, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x0457, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C,
|
||||
// D0-DF
|
||||
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422,
|
||||
// E0-EF
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
|
||||
};
|
||||
|
||||
public Cp1123() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "1123", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1154 (Cyrillic Multilingual with Euro \u20AC).
|
||||
* CCSID / CPGID: 1154, GCSGID: 1305.
|
||||
*/
|
||||
public class Cp1154 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "1154";
|
||||
public static final String DESCRIPTION = "Cyrillic Multilingual EBCDIC (with Euro \u20AC)";
|
||||
public static final int CPGID = 1154;
|
||||
public static final int GCSGID = 1305;
|
||||
|
||||
public static final int[] MAPPING = new int[256];
|
||||
|
||||
static {
|
||||
System.arraycopy(Cp1025.MAPPING, 0, MAPPING, 0, 256);
|
||||
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
|
||||
}
|
||||
|
||||
public Cp1154() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "1154", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1155 (Turkey - Turkish Latin-5 with Euro \u20AC).
|
||||
* CCSID / CPGID: 1155, GCSGID: 1306.
|
||||
*/
|
||||
public class Cp1155 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "1155";
|
||||
public static final String DESCRIPTION = "Turkey - Turkish Latin-5 (with Euro \u20AC)";
|
||||
public static final int CPGID = 1155;
|
||||
public static final int GCSGID = 1306;
|
||||
|
||||
public static final int[] MAPPING = new int[256];
|
||||
|
||||
static {
|
||||
System.arraycopy(Cp1026.MAPPING, 0, MAPPING, 0, 256);
|
||||
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
|
||||
}
|
||||
|
||||
public Cp1155() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "1155", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1160 (Thai EBCDIC with Euro \u20AC).
|
||||
* CCSID / CPGID: 1160, GCSGID: 1176.
|
||||
*/
|
||||
public class Cp1160 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "1160";
|
||||
public static final String DESCRIPTION = "Thai EBCDIC (with Euro \u20AC)";
|
||||
public static final int CPGID = 1160;
|
||||
public static final int GCSGID = 1176;
|
||||
|
||||
public static final int[] MAPPING = new int[256];
|
||||
|
||||
static {
|
||||
System.arraycopy(Cp838.MAPPING, 0, MAPPING, 0, 256);
|
||||
MAPPING[0x9F] = 0x20AC; // Euro character \u20AC
|
||||
}
|
||||
|
||||
public Cp1160() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "1160", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1371 (Traditional Chinese Extended Mixed DBCS).
|
||||
* CCSID / CPGID: 1371, GCSGID: 1174.
|
||||
*/
|
||||
public class Cp1371 extends AbstractDBCSCodePage {
|
||||
|
||||
public static final String ID = "1371";
|
||||
public static final String DESCRIPTION = "Traditional Chinese Extended Mixed DBCS";
|
||||
public static final int CPGID = 1371;
|
||||
public static final int GCSGID = 1174;
|
||||
|
||||
public Cp1371() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1371");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 1388 (Simplified Chinese Extended Mixed DBCS).
|
||||
* CCSID / CPGID: 1388, GCSGID: 1175.
|
||||
*/
|
||||
public class Cp1388 extends AbstractDBCSCodePage {
|
||||
|
||||
public static final String ID = "1388";
|
||||
public static final String DESCRIPTION = "Simplified Chinese Extended Mixed DBCS";
|
||||
public static final int CPGID = 1388;
|
||||
public static final int GCSGID = 1175;
|
||||
|
||||
public Cp1388() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM1388");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 273 (Germany / Austria).
|
||||
* CCSID / CPGID: 273, GCSGID: 697.
|
||||
*/
|
||||
public class Cp273 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "273";
|
||||
public static final String DESCRIPTION = "Germany / Austria - EBCDIC";
|
||||
public static final int CPGID = 273;
|
||||
public static final int GCSGID = 697;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x00E4, 0x00F6, 0x00FC, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x00E4, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x00FC, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00DC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp273() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "273", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 277 (Denmark / Norway).
|
||||
* CCSID / CPGID: 277, GCSGID: 697.
|
||||
*/
|
||||
public class Cp277 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "277";
|
||||
public static final String DESCRIPTION = "Denmark / Norway - EBCDIC";
|
||||
public static final int CPGID = 277;
|
||||
public static final int GCSGID = 697;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x00A4, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x00E6, 0x00F8, 0x00E5, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x00C6, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x00D8, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x00C5, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp277() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "277", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 278 (Sweden / Finland).
|
||||
* CCSID / CPGID: 278, GCSGID: 697.
|
||||
*/
|
||||
public class Cp278 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "278";
|
||||
public static final String DESCRIPTION = "Sweden / Finland - EBCDIC";
|
||||
public static final int CPGID = 278;
|
||||
public static final int GCSGID = 697;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x00A4, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x00E4, 0x00F6, 0x00E5, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x00C4, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x00D6, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x00C5, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp278() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "278", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 280 (Italy).
|
||||
* CCSID / CPGID: 280, GCSGID: 697.
|
||||
*/
|
||||
public class Cp280 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "280";
|
||||
public static final String DESCRIPTION = "Italy - EBCDIC";
|
||||
public static final int CPGID = 280;
|
||||
public static final int GCSGID = 697;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00B0, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x00A3, 0x00A7, 0x00E9, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x00F2, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x00F9, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x00E0, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp280() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "280", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 284 (Spain / Latin America).
|
||||
* CCSID / CPGID: 284, GCSGID: 697.
|
||||
*/
|
||||
public class Cp284 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "284";
|
||||
public static final String DESCRIPTION = "Spain / Latin America - EBCDIC";
|
||||
public static final int CPGID = 284;
|
||||
public static final int GCSGID = 697;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x00F1, 0x00D1, 0x00E7, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp284() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "284", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 285 (United Kingdom).
|
||||
* CCSID / CPGID: 285, GCSGID: 697.
|
||||
*/
|
||||
public class Cp285 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "285";
|
||||
public static final String DESCRIPTION = "United Kingdom - EBCDIC";
|
||||
public static final int CPGID = 285;
|
||||
public static final int GCSGID = 697;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x00A3, 0x0040, 0x0027, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp285() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "285", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 297 (France).
|
||||
* CCSID / CPGID: 297, GCSGID: 697.
|
||||
*/
|
||||
public class Cp297 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "297";
|
||||
public static final String DESCRIPTION = "France - EBCDIC";
|
||||
public static final int CPGID = 297;
|
||||
public static final int GCSGID = 697;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x00A0, 0x00E2, 0x007B, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
|
||||
0x00E7, 0x00F1, 0x00B0, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
|
||||
0x00EC, 0x00DF, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x00C2, 0x005B, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
|
||||
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
|
||||
0x00CC, 0x0060, 0x003A, 0x00E0, 0x00E9, 0x00E8, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x00E9, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x00E8, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp297() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "297", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 420 (Arabic Bilingual EBCDIC).
|
||||
* CCSID / CPGID: 420, GCSGID: 235.
|
||||
*/
|
||||
public class Cp420 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "420";
|
||||
public static final String DESCRIPTION = "Arabic Bilingual EBCDIC";
|
||||
public static final int CPGID = 420;
|
||||
public static final int GCSGID = 235;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x0640, 0xFE83, 0xFE87, 0xFE85, 0xFE81, 0xFE80, 0x0621,
|
||||
0xFE8D, 0xFE8B, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0xFE8E, 0xFE8F, 0xFE91, 0xFE93, 0xFE95, 0xFE97, 0xFE99,
|
||||
0xFE9B, 0xFE9D, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0xFE9F, 0xFEA1, 0xFEA3, 0xFEA5, 0xFEA7, 0xFEA9,
|
||||
0xFEAB, 0xFEAD, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0xFEAF, 0xFEB1, 0xFEB3, 0xFEB5, 0xFEB7, 0xFEB9, 0xFEBB, 0xFEBD,
|
||||
0xFEBF, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0xFEC1, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0xFEC3, 0xFEC5, 0xFEC7, 0xFEC9, 0xFECB, 0xFECD,
|
||||
// 90-9F
|
||||
0xFECF, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0xFED1, 0xFED3, 0xFED5, 0xFED7, 0xFED9, 0xFEDB,
|
||||
// A0-AF
|
||||
0xFEDD, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0xFEDF, 0xFEE1, 0xFEE3, 0xFEE5, 0xFEE7, 0xFEE9,
|
||||
// B0-BF
|
||||
0xFEEB, 0xFEED, 0xFEEF, 0xFEF1, 0xFEF3, 0xFEF5, 0xFEF7, 0xFEF9,
|
||||
0xFEFB, 0x00AE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x060C, 0x061B, 0x061F, 0x0628, 0x062A, 0x062B,
|
||||
// D0-DF
|
||||
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x062C, 0x062D, 0x062E, 0x062F, 0x0630, 0x0631,
|
||||
// E0-EF
|
||||
0x005C, 0x0632, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637, 0x0638,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x0639, 0x063A, 0x0641, 0x0642, 0x0643, 0x009F,
|
||||
};
|
||||
|
||||
public Cp420() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "420", MAPPING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package haus.nightmare.lib3270j.charset;
|
||||
|
||||
/**
|
||||
* IBM Code Page 424 (Hebrew with Lowercase EBCDIC).
|
||||
* CCSID / CPGID: 424, GCSGID: 941.
|
||||
*/
|
||||
public class Cp424 extends AbstractCodePage {
|
||||
|
||||
public static final String ID = "424";
|
||||
public static final String DESCRIPTION = "Hebrew (with Lowercase) EBCDIC";
|
||||
public static final int CPGID = 424;
|
||||
public static final int GCSGID = 941;
|
||||
|
||||
public static final int[] MAPPING = {
|
||||
// 00-0F
|
||||
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
|
||||
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
||||
// 10-1F
|
||||
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
|
||||
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
|
||||
// 20-2F
|
||||
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
|
||||
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
|
||||
// 30-3F
|
||||
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
|
||||
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
|
||||
// 40-4F
|
||||
0x0020, 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6,
|
||||
0x05D7, 0x05D8, 0x005B, 0x002E, 0x003C, 0x0028, 0x002B, 0x0021,
|
||||
// 50-5F
|
||||
0x0026, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF,
|
||||
0x05E0, 0x05E1, 0x005D, 0x0024, 0x002A, 0x0029, 0x003B, 0x005E,
|
||||
// 60-6F
|
||||
0x002D, 0x002F, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7,
|
||||
0x05E8, 0x05E9, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
|
||||
// 70-7F
|
||||
0x05EA, 0x00A0, 0x2017, 0x00B8, 0x00A8, 0x00B4, 0x00AA, 0x00BA,
|
||||
0x00DF, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
|
||||
// 80-8F
|
||||
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
||||
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
|
||||
// 90-9F
|
||||
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
|
||||
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
|
||||
// A0-AF
|
||||
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
|
||||
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
|
||||
// B0-BF
|
||||
0x00A2, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
|
||||
0x00BD, 0x00BE, 0x00AC, 0x007C, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
|
||||
// C0-CF
|
||||
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
||||
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
|
||||
// D0-DF
|
||||
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
|
||||
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
|
||||
// E0-EF
|
||||
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
|
||||
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
|
||||
// F0-FF
|
||||
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
||||
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
|
||||
};
|
||||
|
||||
public Cp424() {
|
||||
super(ID, DESCRIPTION, CPGID, GCSGID, "424", MAPPING);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user