23 Commits

Author SHA1 Message Date
rudi aa724fbcef Hotspot detection and screen buffer
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m28s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
2026-09-18 18:18:53 +00:00
rudi 8a30efe9c0 Add bugfix for ATTN
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 35s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s
2026-09-18 17:33:58 +00:00
rudi 5049d8df74 Add facade classes for 3rd party programs
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m18s
2026-09-18 16:48:49 +00:00
rudi badd026055 Cleanup datastream and Graphics
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s
2026-09-18 00:55:54 +00:00
rudi ef70ea0f5a Bugfixes from zOS testing
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 37s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m16s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m52s
2026-09-17 23:09:43 +00:00
rudi 30b1a77b61 Datastream bug fixes w/ not recognizing bytes
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m9s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m10s
Release j3270 / Build & Publish Release (push) Successful in 1m11s
2026-09-16 21:08:22 +00:00
rudi 9bd1676ec1 Fix setting storage, and add proxy options like a3270
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m11s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s
2026-09-16 14:43:30 +00:00
rudi cfb32bdca5 Build Gitea release against Java 11 (lowest supported Java)
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m13s
Release j3270 / Build & Publish Release (push) Successful in 1m12s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m37s
2026-09-15 23:52:21 +00:00
rudi 754b1de7cb Rearrange settings, update sample config
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m8s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m38s
Release j3270 / Build & Publish Release (push) Successful in 1m13s
2026-09-15 17:59:26 +00:00
rudi 7892229a2c Add icon
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m12s
2026-09-14 13:01:41 +00:00
rudi 14bf7ba4b1 Add bugfixes from testing
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 37s
Release j3270 / Build & Publish Release (push) Successful in 43s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m14s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m12s
2026-09-12 17:21:21 -04:00
rudi bdfe6eec2a IMON GRAPHIC!
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 31s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 42s
2026-09-12 15:33:12 +00:00
rudi c28c097e25 Fix bugs reported in Discord
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s
2026-09-12 14:58:48 +00:00
rudi 46a022d86b Add bugfixes from discord
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
2026-09-10 09:24:39 -04:00
rudi 59925c2787 Add headless server and setup script
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m13s
Release j3270 / Build & Publish Release (push) Successful in 1m8s
2026-09-08 11:38:38 -04:00
rudi f8fc0e4b22 Fix the build
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 25s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 59s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 23s
2026-09-07 20:31:18 -04:00
rudi dbcb0aa1b6 Clear cruft
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 26s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Failing after 1m1s
2026-09-07 20:16:00 -04:00
rudi 50a4279446 Update README
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 30s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 24s
2026-09-06 11:31:35 -04:00
rudi 6331e0108b Update tests and Status/UI
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 26s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m5s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m10s
2026-09-06 09:53:43 -04:00
rudi 79d493c24d Update builder
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m12s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m12s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 4m22s
2026-09-05 14:11:08 -04:00
rudi 74e30d1ead IBM-DYNAMIC tweaking
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m25s
2026-09-04 20:58:19 -04:00
rudi cbb310b889 GOCA fixing
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m22s
2026-09-04 14:34:01 -04:00
rudi 6296cf4341 IBM-DYNAMIC
Release j3270 / Build & Publish Release (push) Successful in 1m23s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m28s
2026-09-03 16:43:22 -04:00
404 changed files with 51631 additions and 1919 deletions
+18 -10
View File
@@ -9,8 +9,12 @@ on:
jobs: jobs:
build: build:
name: Build JAR & Run Tests name: Build JAR & Run Tests (Java ${{ matrix.java }})
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java: [ '11', '17', '21' ]
steps: steps:
- name: Configure Dynamic DNS Resolvers - name: Configure Dynamic DNS Resolvers
@@ -23,21 +27,21 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Java JDK (Temurin 17) - name: Set up Java JDK (Temurin ${{ matrix.java }})
uses: actions/setup-java@v3 uses: actions/setup-java@v3
continue-on-error: true continue-on-error: true
with: with:
java-version: '17' java-version: ${{ matrix.java }}
distribution: 'temurin' distribution: 'temurin'
- name: Ensure Java Available - name: Ensure Java Available
run: | run: |
if ! command -v javac >/dev/null 2>&1; then if ! command -v javac >/dev/null 2>&1; then
echo "Installing OpenJDK 17 via package manager..." echo "Installing OpenJDK ${{ matrix.java }} via package manager..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y openjdk-17-jdk-headless || true apt-get update && apt-get install -y openjdk-${{ matrix.java }}-jdk-headless || true
elif command -v apk >/dev/null 2>&1; then elif command -v apk >/dev/null 2>&1; then
apk add --no-cache openjdk17 || true apk add --no-cache openjdk${{ matrix.java }} || true
fi fi
fi fi
echo "Java compiler:" echo "Java compiler:"
@@ -53,23 +57,27 @@ jobs:
run: | run: |
sh ./test_all.sh sh ./test_all.sh
- name: Prepare Versioned JAR
run: |
cp build/j3270.jar "build/j3270-java${{ matrix.java }}.jar"
- name: Upload j3270 Executable JAR Artifact - name: Upload j3270 Executable JAR Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
continue-on-error: true continue-on-error: true
with: with:
name: j3270-executable-jar name: j3270-java${{ matrix.java }}-jar
path: build/j3270.jar path: build/j3270-java${{ matrix.java }}.jar
- name: Upload Test Reports - name: Upload Test Reports
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
continue-on-error: true continue-on-error: true
with: with:
name: test-reports name: test-reports-java${{ matrix.java }}
path: build/reports/tests/ path: build/reports/tests/
- name: Report Build Status - name: Report Build Status
if: failure() if: failure()
run: | run: |
echo "### ❌ Build or Test Failed" >> $GITHUB_STEP_SUMMARY echo "### ❌ Build or Test Failed for Java ${{ matrix.java }}" >> $GITHUB_STEP_SUMMARY
echo "Please check the runner logs above for details." >> $GITHUB_STEP_SUMMARY echo "Please check the runner logs above for details." >> $GITHUB_STEP_SUMMARY
+5 -5
View File
@@ -20,21 +20,21 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Java JDK (Temurin 17) - name: Set up Java JDK (Temurin 11)
uses: actions/setup-java@v3 uses: actions/setup-java@v3
continue-on-error: true continue-on-error: true
with: with:
java-version: '17' java-version: '11'
distribution: 'temurin' distribution: 'temurin'
- name: Ensure Java Available - name: Ensure Java Available
run: | run: |
if ! command -v javac >/dev/null 2>&1; then if ! command -v javac >/dev/null 2>&1; then
echo "Installing OpenJDK 17 via package manager..." echo "Installing OpenJDK 11 via package manager..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y openjdk-17-jdk-headless || true apt-get update && apt-get install -y openjdk-11-jdk-headless || true
elif command -v apk >/dev/null 2>&1; then elif command -v apk >/dev/null 2>&1; then
apk add --no-cache openjdk17 || true apk add --no-cache openjdk11 || true
fi fi
fi fi
echo "Java compiler:" echo "Java compiler:"
+18 -10
View File
@@ -9,8 +9,12 @@ on:
jobs: jobs:
build: build:
name: Build JAR & Run Tests name: Build JAR & Run Tests (Java ${{ matrix.java }})
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java: [ '11', '17', '21' ]
steps: steps:
- name: Configure Dynamic DNS Resolvers - name: Configure Dynamic DNS Resolvers
@@ -23,21 +27,21 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Java JDK (Temurin 17) - name: Set up Java JDK (Temurin ${{ matrix.java }})
uses: actions/setup-java@v3 uses: actions/setup-java@v3
continue-on-error: true continue-on-error: true
with: with:
java-version: '17' java-version: ${{ matrix.java }}
distribution: 'temurin' distribution: 'temurin'
- name: Ensure Java Available - name: Ensure Java Available
run: | run: |
if ! command -v javac >/dev/null 2>&1; then if ! command -v javac >/dev/null 2>&1; then
echo "Installing OpenJDK 17 via package manager..." echo "Installing OpenJDK ${{ matrix.java }} via package manager..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y openjdk-17-jdk-headless || true apt-get update && apt-get install -y openjdk-${{ matrix.java }}-jdk-headless || true
elif command -v apk >/dev/null 2>&1; then elif command -v apk >/dev/null 2>&1; then
apk add --no-cache openjdk17 || true apk add --no-cache openjdk${{ matrix.java }} || true
fi fi
fi fi
echo "Java compiler:" echo "Java compiler:"
@@ -53,23 +57,27 @@ jobs:
run: | run: |
sh ./test_all.sh sh ./test_all.sh
- name: Prepare Versioned JAR
run: |
cp build/j3270.jar "build/j3270-java${{ matrix.java }}.jar"
- name: Upload j3270 Executable JAR Artifact - name: Upload j3270 Executable JAR Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
continue-on-error: true continue-on-error: true
with: with:
name: j3270-executable-jar name: j3270-java${{ matrix.java }}-jar
path: build/j3270.jar path: build/j3270-java${{ matrix.java }}.jar
- name: Upload Test Reports - name: Upload Test Reports
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
continue-on-error: true continue-on-error: true
with: with:
name: test-reports name: test-reports-java${{ matrix.java }}
path: build/reports/tests/ path: build/reports/tests/
- name: Report Build Status - name: Report Build Status
if: failure() if: failure()
run: | run: |
echo "### ❌ Build or Test Failed" >> $GITHUB_STEP_SUMMARY echo "### ❌ Build or Test Failed for Java ${{ matrix.java }}" >> $GITHUB_STEP_SUMMARY
echo "Please check the runner logs above for details." >> $GITHUB_STEP_SUMMARY echo "Please check the runner logs above for details." >> $GITHUB_STEP_SUMMARY
+2
View File
@@ -1,4 +1,5 @@
j3270.log.* j3270.log.*
*.bin
*.txt *.txt
*.py *.py
*.pcap *.pcap
@@ -8,3 +9,4 @@ build/*
*.jar *.jar
build build
.gradle .gradle
*.txt
+123 -30
View File
@@ -2,29 +2,43 @@
[![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions) [![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions)
[![Releases](https://img.shields.io/badge/Releases-Gitea-blue.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases) [![Releases](https://img.shields.io/badge/Releases-Gitea-blue.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases)
[![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021-blue.svg)](https://adoptium.net) [![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021%20(LTS)-blue.svg)](https://adoptium.net)
[![Tests](https://img.shields.io/badge/Tests-539%20passed-brightgreen.svg)]()
[![License](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE) [![License](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE)
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E. An **x3270-aligned and IBM Host On-Demand (HoD) compatible IBM 3270 mainframe terminal emulator & protocol library** written in pure Java with zero third-party runtime dependencies. Designed for high fidelity and compatibility with IBM z/OS, z/VM, and 3270-based systems over TN3270 and TN3270E.
--- ---
## 🚀 Quick Download & Run ## 🚀 Quick Download & Run
### Latest Standalone JAR ### Latest Standalone JARs
You can download the ready-to-run executable JAR directly from the releases or CI builds: Ready-to-run executable JARs are built and tested across all three Java LTS baselines (**Java 11, Java 17, and Java 21**) by Gitea Actions:
- **[Download Latest Tagged Release (`j3270.jar`)](https://git.hugfreevikings.wtf/rudi/j3270/releases)** - **[Download Latest Tagged Release (`j3270.jar`)](https://git.hugfreevikings.wtf/rudi/j3270/releases)**
- **[Latest Nightly CI Artifacts](https://git.hugfreevikings.wtf/rudi/j3270/actions)** (Download under the latest successful workflow run) - **[Latest Nightly CI Artifacts](https://git.hugfreevikings.wtf/rudi/j3270/actions)**: Download standalone JARs tailored for each LTS runtime under the latest workflow run:
- `j3270-java11-jar` (Java 11 LTS)
- `j3270-java17-jar` (Java 17 LTS — primary Gitea runner baseline)
- `j3270-java21-jar` (Java 21 LTS)
### Java Runtime Requirements
Requires **Java 11 or higher** (LTS versions: **Java 11, 17, 21**; also compatible with Java 24).
The project is tested and verified against:
- **Eclipse Temurin** (Adoptium OpenJDK HotSpot) — used in Gitea Actions CI workflows.
- **IBM Semeru Runtimes** (OpenJ9) — e.g. `11.0.32-sem`, `21.0.6-sem`, and `24.0.2-sem` via SDKMAN.
### Running `j3270` ### Running `j3270`
Requires **Java 11 or higher** (Java 11, 17, 21+):
```bash ```bash
# Launch interactive GUI # Launch interactive Swing GUI
java -jar j3270.jar java -jar j3270.jar
# Connect to TLS/SSL mainframe with custom port # Connect to mainframe with host, port, and terminal model (Models 2-5 or 'dynamic')
java -jar j3270.jar mainframe.example.com 23 4
java -jar j3270.jar mainframe.example.com 23 dynamic
# Connect to TLS/SSL mainframe on custom or standard (992) port
java -jar j3270.jar -s mainframe.example.com 992 java -jar j3270.jar -s mainframe.example.com 992
# Or using standard x3270 L: prefix # Or using standard x3270 L: prefix
java -jar j3270.jar L:mainframe.example.com:992 java -jar j3270.jar L:mainframe.example.com:992
@@ -35,12 +49,19 @@ java -jar j3270.jar -P mainframe.example.com 23
java -jar j3270.jar P:mainframe.example.com:23 java -jar j3270.jar P:mainframe.example.com:23
# Connect with unverified/self-signed certificate verification bypass # Connect with unverified/self-signed certificate verification bypass
java -jar j3270.jar --tls --insecure mainframe.example.com 992 java -jar j3270.jar -s --insecure mainframe.example.com 992
# Launch with custom configuration file # Enable GDDM/GOCA host graphics explicitly (or disable via --no-graphics)
java -jar j3270.jar -g GOCA mainframe.example.com
java -jar j3270.jar --no-graphics mainframe.example.com
# Launch with custom INI configuration profile
java -jar j3270.jar -c config.ini java -jar j3270.jar -c config.ini
# Launch directly from source runner # Enable protocol and diagnostic debug logging to j3270.log
java -jar j3270.jar -d mainframe.example.com
# Launch directly from source runner script
./run.sh ./run.sh
``` ```
@@ -48,39 +69,108 @@ java -jar j3270.jar -c config.ini
## ✨ Features ## ✨ Features
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, plain TN3270 fallback, and SSL/TLS encryption. ### 🌐 Protocol & Telnet Engine
- **SSL/TLS Security**: - **TN3270 & TN3270E (RFC 2355)**: Full compliant finite-state machine, Device-Type negotiation, Functions negotiation (`BIND-IMAGE`, `RESPONSES`, `SYSREQ`), plain TN3270 fallback, and NVT (Network Virtual Terminal) mode.
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports. - **SSCP-LU & Unformatted Line Mode**: Native z/VM line-mode support (`CP TERM CONMODE 3270`).
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection. - **Terminal Models & Dynamic Geometry**:
- 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.
- **Terminal Models**:
- Model 2 (24x80) - Model 2 (24x80)
- Model 3 (32x80) - Model 3 (32x80)
- Model 4 (43x80) - Model 4 (43x80)
- Model 5 (27x132) - Model 5 (27x132)
- **Zero-Dependency Core**: Modular architecture separated into: - `IBM-DYNAMIC` (Model 0 dynamic geometry auto-negotiation via Usable Area Query Reply).
- `lib3270j`: Reusable Java 3270 protocol, data stream parser, charset translation, and telnet state machine. - **Enterprise Proxy Support**: SOCKS5 and HTTP CONNECT proxy tunneling with optional authentication.
- `j3270`: Desktop Swing user interface and session management.
### 🔒 Security & Encryption
- **Modern TLS 1.2 / 1.3**: Encrypted TN3270 over TLS on standard port `992` or custom ports via JSSE.
- **Interactive Certificate Verification**: Security dialog for untrusted or self-signed certificates displaying SHA-256 fingerprints, subject, issuer, and validity dates with user approval prompt.
- **Headless Insecure Mode**: `--insecure` / `-k` option for scripted environments and test mainframes.
### 🎨 GDDM / GOCA Host Vector Graphics (IBM 3179G)
- **Vector Drawing Orders**: Polylines, relative lines, circular & elliptical arcs, fillets, and multi-polygon area fills with boundary detection and alternate/winding rules.
- **1:1 Color Calibration**: 16-color GOCA palette and base 4-color mapping calibrated 1:1 against IBM Host On-Demand (HoD).
- **Shading, Patterns & Overlays**: Patterned area fills, geometric markers, and uncompressed / run-length bit-image and wallpaper overlays.
- **Vector Symbol Fonts (VSS)**: Stroked vector typography with baseline rotation angle (`GSCA`), shear (`GSCR`), and micro-scale anti-aliased sub-pixel rendering.
- **Programmed Symbols (PSS / PSA)**: Support for single-plane and triple-plane multi-color composite symbols.
- **Interactive Graphics Input**: Mouse click pick correlation mapping display coordinates to host graphics space, 56-byte Graphic Input structured field generation, and immediate/deferred light pen selection.
### 🖨️ IBM 3287/3286 Host Printing Emulation
- **Autonomous Printer Sessions**: TN3270E host print sessions supporting SCS (SNA Character String / LU-Type 1) and DSC/DSE (Data Stream Compatibility / LU-Type 3).
- **Double-Byte DBCS Printing**: Pitch adjustments, character spacing, and grid line formatting (`PrintSCS3270DB`, `PrintPS3270DB`).
- **Flexible Print Output**: Printer Definition Table (PDT) integration, print spooling to local files, or dispatch directly to system print queues.
### 📁 IND$FILE File Transfer & Host Catalog
- **Full Protocol Coverage**: Support for both **CUT** and high-speed **DFT (DDM)** structured field transfers for TSO, VM/CMS, and CICS.
- **Translation Modes**: Automatic ASCII/EBCDIC text translation, CRLF conversions, and transparent binary transfers.
- **Dataset Formatting**: Full control over Record Format (`RECFM` F/V/U), Logical Record Length (`LRECL`), Block Size (`BLKSIZE`), and Space allocation.
- **Host Catalog Browser**: Interactive directory dialog to query, parse, and browse CMS and TSO host dataset catalogs.
### 🌍 Internationalization & Character Sets
- **22+ Single-Byte EBCDIC Codepages**: US/Canada (037), Open Systems / z/OS Unix (1047), International (500, 1148 Euro), UK (285), Germany/Austria (273, 1141 Euro), France (297), Italy (280), Spain (284), Scandinavia (277, 278), Greece (875), Turkey (1026, 1155, 905), Eastern Europe (870), Iceland (871), Cyrillic (1025, 1123, 1154, 880), Hebrew (424, 803), Arabic (420), Thai (838, 1160).
- **Mixed DBCS Support**: Japanese Katakana (930), Japanese Latin (939), Simplified Chinese (935, 1388), Traditional Chinese (937, 1371), and Korean (933) with Shift-In (`0x0E`) / Shift-Out (`0x0F`) state handling.
- **APL & Special Graphics**: APL keyboard mode, box-drawing characters, and Graphic Escape (`GE`) symbol translations.
### 🏛️ IBM Host On-Demand ECL API Alignment
- Reusable public API compliant with IBM Host On-Demand (HoD) v14 specifications under `com.ibm.eNetwork.ECL.*` and `haus.nightmare.lib3270j.ecl.*`.
- Drop-in implementations for `ECLSession`, `ECLPS` (Presentation Space), `ECLOIA` (Operator Information Area), `ECLConnection`, `ECLField`, `ECLFieldList`, `ECLScreenDesc`, `ECLScreenReco`, and `ECLXfer`.
### 🖥️ Desktop User Interface (j3270)
- **Themes**: Modern Dark and Light UI themes with instant switching.
- **Dynamic Font Scaling**: Real-time font size adjustments (`Cmd/Ctrl +`, `Cmd/Ctrl -`, `Cmd/Ctrl 0`) with smooth scaling and guarded window repacking.
- **Screen Search**: Full-screen text search dialog (`Cmd/Ctrl + F`) with forward/backward navigation and case-sensitivity controls.
- **Screen Exporter**: Save screen images and content as plain text, HTML, or PNG.
- **Field Inspector**: Interactive inspection tool displaying 3270 field boundaries, buffer addresses, protection, highlighting, and color attributes.
- **Scripting & Keystroke Macros**: Interactive mnemonic keystroke playback dialog supporting standard x3270 action strings.
- **Word Processing Modes**: Document Mode (DOC), Word Wrap, audible margin bell, and tab stop navigation.
- **Visual Aids**: Crosshair ruler and configurable cursor styles (block cursor, underline cursor with alpha blending).
- **Persistent Profiles**: Configuration profiles and session preferences saved in INI files (`j3270.ini`).
### 📦 Modular Zero-Dependency Core
- **`lib3270j`**: Standalone Java protocol and terminal engine with **zero third-party dependencies** (`java.base`, `java.desktop` only).
- **`j3270`**: Standalone desktop Swing terminal application.
--- ---
## 🛠️ Building from Source ## 🛠️ Building from Source
### Standalone Build (No Gradle Required) ### Quick Setup & Build for End Users
The project includes self-contained, portable build scripts:
Self-contained setup scripts automatically locate an installed Java 11+ JDK (searching `JAVA_HOME`, system `PATH`, macOS `java_home`, Homebrew, Linux JVM paths, Windows Registry, standard installation directories, and package managers), compile `lib3270j` and `j3270`, and package the standalone executable JAR (`build/j3270.jar`):
**Linux / macOS (POSIX Shell):**
```bash ```bash
# Build executable JAR in 2 seconds: # Setup and build standalone executable JAR:
sh ./build_all.sh ./setup.sh
# Run all 57 automated unit tests in ~500ms: # Build and immediately launch:
sh ./test_all.sh ./setup.sh --run
# Clean build and connect to host:
./setup.sh --clean --run -- mainframe.example.com 23 4
``` ```
The resulting standalone JAR is created at `build/j3270.jar`. **Windows (PowerShell):**
```powershell
# Setup and build standalone executable JAR:
.\setup.ps1
# Build and immediately launch:
.\setup.ps1 -Run
# Clean build and connect to host:
.\setup.ps1 -Clean -Run -- mainframe.example.com 23 4
```
*(Also available as `.\setup.ps`)*
### Standalone Build (Advanced / CI)
The project also provides low-level scripts and optional Gradle support:
```bash
# Direct compile and package without diagnostics:
sh ./build_all.sh
# Run all 539+ automated unit tests (~7s):
sh ./test_all.sh
```
### Gradle Build (Optional) ### Gradle Build (Optional)
```bash ```bash
@@ -88,6 +178,9 @@ The resulting standalone JAR is created at `build/j3270.jar`.
./gradlew test ./gradlew test
``` ```
### Continuous Integration
All pushes and pull requests are verified across **Java 11, 17, and 21 LTS** runners via Gitea Actions (`.gitea/workflows/build.yaml`).
--- ---
## 📜 Acknowledgements ## 📜 Acknowledgements
+146
View File
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="124 738 320 320"
width="320"
height="320"
xml:space="preserve"
id="svg_j3270_icon"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="124" y="738" width="320" height="320" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+146
View File
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="144 814 280 169"
width="280"
height="169"
xml:space="preserve"
id="svg_lib3270j_cropped"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="144" y="814" width="280" height="169" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+2
View File
@@ -17,6 +17,7 @@ subprojects {
test { test {
useJUnitPlatform() useJUnitPlatform()
jvmArgs '-Djava.awt.headless=true'
testLogging { testLogging {
events "passed", "skipped", "failed" events "passed", "skipped", "failed"
} }
@@ -24,5 +25,6 @@ subprojects {
dependencies { dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }
} }
+24 -10
View File
@@ -14,16 +14,32 @@ if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/javac" ] && [ -x "$JAVA_HOME/bin/
JAVAC_BIN="$JAVA_HOME/bin/javac" JAVAC_BIN="$JAVA_HOME/bin/javac"
JAR_BIN="$JAVA_HOME/bin/jar" JAR_BIN="$JAVA_HOME/bin/jar"
elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
JAVAC_BIN="$(command -v javac)" if javac -version >/dev/null 2>&1; then
JAR_BIN="$(command -v jar)" JAVAC_BIN="$(command -v javac)"
else JAR_BIN="$(command -v jar)"
# Check common SDKMAN / macOS / Linux JDK locations fi
for h in "$HOME/.sdkman/candidates/java/current" \ fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
if [ -x /usr/libexec/java_home ]; then
MAC_JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null || true)"
if [ -n "$MAC_JAVA_HOME" ] && [ -x "$MAC_JAVA_HOME/bin/javac" ] && [ -x "$MAC_JAVA_HOME/bin/jar" ]; then
export JAVA_HOME="$MAC_JAVA_HOME"
JAVAC_BIN="$JAVA_HOME/bin/javac"
JAR_BIN="$JAVA_HOME/bin/jar"
fi
fi
fi
if [ -z "$JAVAC_BIN" ] || [ -z "$JAR_BIN" ]; then
# Check standard system JDK installation locations
for h in /usr/lib/jvm/default-java \
/usr/lib/jvm/java-21-openjdk* \ /usr/lib/jvm/java-21-openjdk* \
/usr/lib/jvm/java-17-openjdk* \ /usr/lib/jvm/java-17-openjdk* \
/usr/lib/jvm/java-11-openjdk* \ /usr/lib/jvm/java-11-openjdk* \
/usr/lib/jvm/default-java \ /Library/Java/JavaVirtualMachines/*/Contents/Home \
/Library/Java/JavaVirtualMachines/*/Contents/Home; do /opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home \
/opt/homebrew/opt/openjdk; do
if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/jar" ]; then if [ -d "$h" ] && [ -x "$h/bin/javac" ] && [ -x "$h/bin/jar" ]; then
export JAVA_HOME="$h" export JAVA_HOME="$h"
JAVAC_BIN="$JAVA_HOME/bin/javac" JAVAC_BIN="$JAVA_HOME/bin/javac"
@@ -131,9 +147,7 @@ public class TestRunner {
summary.printTo(new PrintWriter(System.out)); summary.printTo(new PrintWriter(System.out));
summary.printFailuresTo(new PrintWriter(System.err)); summary.printFailuresTo(new PrintWriter(System.err));
if (summary.getTotalFailureCount() > 0) { System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0);
System.exit(1);
}
} }
} }
EOF EOF
+5 -5
View File
@@ -1,10 +1,10 @@
# Gitea Actions Runner Configuration for j3270 # Gitea Actions Runner Configuration for j3270
This repository includes Gitea Actions workflows in `.gitea/workflows/build.yaml` to automatically: This repository includes Gitea Actions workflows in `.gitea/workflows/build.yaml` to automatically:
1. Compile and run the JUnit 5 test suite via `test_all.sh`. 1. Compile and run the JUnit 5 test suite across the 3 LTS Java versions (Java 11, 17, and 21) via `test_all.sh`.
2. Build the standalone `j3270.jar` executable via `build_all.sh`. 2. Build standalone executables for each LTS version via `build_all.sh`.
3. Upload `j3270.jar` as a downloadable artifact. 3. Upload `j3270-java11.jar`, `j3270-java17.jar`, and `j3270-java21.jar` as downloadable artifacts.
4. Report test results and diagnostics. 4. Report test results and diagnostics per runtime.
--- ---
@@ -98,7 +98,7 @@ This means Docker's bridge network inside the runner container cannot resolve th
You can verify the entire build and test suite locally at any time without external tools: You can verify the entire build and test suite locally at any time without external tools:
```bash ```bash
# Compile and run all 25 unit tests (130ms): # Compile and run all 539 unit tests (~7s):
sh ./test_all.sh sh ./test_all.sh
# Build executable standalone JAR: # Build executable standalone JAR:
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+248 -1
View File
@@ -1,2 +1,249 @@
#!/bin/sh #!/bin/sh
exec "$(dirname "$0")/build_all.sh" "$@"
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+92
View File
@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+6
View File
@@ -3,6 +3,11 @@
; or: ./run.sh -c j3270.ini ; or: ./run.sh -c j3270.ini
[appearance] [appearance]
; UI font settings (menus, dialogs, status bar)
; uiFontFamily = SansSerif
; uiFontSize = 13
;
; Terminal font settings (3270 screen display)
; fontFamily = IBM 3270 ; fontFamily = IBM 3270
; fontSize = 18 ; fontSize = 18
@@ -10,6 +15,7 @@
; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING ; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING
; autoConnectHost = mainframe.example.com ; autoConnectHost = mainframe.example.com
; autoConnectPort = 23 ; autoConnectPort = 23
; blockPaste = true
[colors] [colors]
; Host colors 0-15 (hex RGB) ; Host colors 0-15 (hex RGB)
@@ -42,13 +42,27 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// Dialog instances // Dialog instances
private FindDialog findDialog; private FindDialog findDialog;
private ScriptDialog scriptDialog; private ScriptDialog scriptDialog;
private MacroDialog macroDialog;
private FieldInspectorDialog fieldInspectorDialog; private FieldInspectorDialog fieldInspectorDialog;
private PrinterSessionDialog printerSessionDialog; private PrinterSessionDialog printerSessionDialog;
private ScreenHistoryDialog screenHistoryDialog;
// Modes menu items
private JCheckBoxMenuItem docModeItem;
private JCheckBoxMenuItem wordWrapItem;
private JCheckBoxMenuItem aplModeItem;
private JCheckBoxMenuItem insertModeItem;
private JCheckBoxMenuItem fourColorItem;
private JCheckBoxMenuItem numLockItem;
private JCheckBoxMenuItem autoSkipItem;
private JCheckBoxMenuItem insertOffAidItem;
private JCheckBoxMenuItem statusBarItem;
public J3270App() { public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator"); super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBackground(Color.BLACK); setBackground(Color.BLACK);
AppIcon.applyTo(this);
buildUI(); buildUI();
buildMenuBar(); buildMenuBar();
@@ -59,12 +73,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
setLocationRelativeTo(null); setLocationRelativeTo(null);
setMinimumSize(new Dimension(640, 400)); setMinimumSize(new Dimension(640, 400));
// Status refresh timer — also ensures focus stays on terminal // Status refresh timer — also ensures focus stays on terminal (guarding against focus stealing)
refreshTimer = new Timer(100, e -> { refreshTimer = new Timer(100, e -> {
if (client != null) { if (client != null) {
statusBar.updateStatus(); statusBar.updateStatus();
if (isActive() && !terminalPanel.hasFocus()) { syncModeMenuItems();
terminalPanel.requestFocusInWindow(); if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
if (!terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
} }
} }
}); });
@@ -79,19 +96,71 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (printerSessionDialog != null) { if (printerSessionDialog != null) {
printerSessionDialog.dispose(); printerSessionDialog.dispose();
} }
if (screenHistoryDialog != null) {
screenHistoryDialog.dispose();
}
terminalPanel.dispose(); terminalPanel.dispose();
} }
@Override @Override
public void windowActivated(WindowEvent e) { public void windowActivated(WindowEvent e) {
terminalPanel.requestFocusInWindow(); if (!isAnyChildDialogActive()) {
terminalPanel.requestFocusInWindow();
}
} }
}); });
// Ensure clicking anywhere inside the frame brings focus to window and terminalPanel
Toolkit.getDefaultToolkit().addAWTEventListener(event -> {
if (event instanceof MouseEvent && event.getID() == MouseEvent.MOUSE_PRESSED) {
MouseEvent me = (MouseEvent) event;
Object src = me.getSource();
if (src instanceof Component) {
Window win = SwingUtilities.getWindowAncestor((Component) src);
if (win == J3270App.this && !isAnyChildDialogActive()) {
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
J3270App.this.toFront();
J3270App.this.requestFocus();
}
boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu)
|| SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null
|| SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null;
if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
}
}
}
}, AWTEvent.MOUSE_EVENT_MASK);
}
private boolean isAnyChildDialogActive() {
Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
if (focusedWindow != null && focusedWindow != this) {
return true;
}
if (findDialog != null && findDialog.isVisible()) return true;
if (scriptDialog != null && scriptDialog.isVisible()) return true;
if (macroDialog != null && macroDialog.isVisible()) return true;
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
if (screenHistoryDialog != null && screenHistoryDialog.isVisible()) return true;
for (Window owned : getOwnedWindows()) {
if (owned != null && owned.isVisible()) {
return true;
}
}
return false;
} }
private void buildUI() { private void buildUI() {
terminalPanel = new TerminalPanel(); terminalPanel = new TerminalPanel();
statusBar = new StatusBar(); statusBar = new StatusBar();
terminalPanel.setStatusBar(statusBar);
terminalPanel.setStatusBarToggleCallback(this::toggleStatusBar);
terminalPanel.addModeChangeListener(this::syncModeMenuItems);
statusBar.setVisible(haus.nightmare.j3270.config.Settings.getStatusBarVisible());
getContentPane().setLayout(new BorderLayout()); getContentPane().setLayout(new BorderLayout());
getContentPane().setBackground(Color.BLACK); getContentPane().setBackground(Color.BLACK);
@@ -99,6 +168,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
getContentPane().add(statusBar, BorderLayout.SOUTH); getContentPane().add(statusBar, BorderLayout.SOUTH);
} }
private void syncModeMenuItems() {
if (docModeItem != null) docModeItem.setSelected(terminalPanel.isDocMode());
if (wordWrapItem != null) wordWrapItem.setSelected(terminalPanel.isWordWrap());
if (aplModeItem != null) aplModeItem.setSelected(terminalPanel.isAplMode());
if (insertModeItem != null) insertModeItem.setSelected(terminalPanel.isInsertMode());
if (fourColorItem != null) fourColorItem.setSelected(haus.nightmare.j3270.config.Settings.getFourColorOverride());
if (numLockItem != null) numLockItem.setSelected(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
if (autoSkipItem != null) autoSkipItem.setSelected(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
if (insertOffAidItem != null) insertOffAidItem.setSelected(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
}
private void onThemeChanged(UITheme theme) { private void onThemeChanged(UITheme theme) {
buildMenuBar(); buildMenuBar();
statusBar.applyTheme(theme); statusBar.applyTheme(theme);
@@ -131,6 +211,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// 2. Edit menu // 2. Edit menu
JMenu editMenu = createMenu("Edit"); JMenu editMenu = createMenu("Edit");
editMenu.add(createMenuItem("Cut", KeyEvent.VK_X, () -> terminalPanel.cutSelection()));
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection())); editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard())); editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog)); editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
@@ -178,6 +259,44 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
viewMenu.add(themeMenu); viewMenu.add(themeMenu);
viewMenu.addSeparator(); viewMenu.addSeparator();
statusBarItem = new JCheckBoxMenuItem("Status Bar", statusBar != null ? statusBar.isVisible() : haus.nightmare.j3270.config.Settings.getStatusBarVisible());
ThemeManager.styleMenuItem(statusBarItem);
statusBarItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.ALT_DOWN_MASK));
statusBarItem.addActionListener(e -> setStatusBarVisible(statusBarItem.isSelected()));
viewMenu.add(statusBarItem);
JCheckBoxMenuItem rulerItem = new JCheckBoxMenuItem("Crosshair Ruler", terminalPanel.isCrosshairRulerEnabled());
ThemeManager.styleMenuItem(rulerItem);
rulerItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
rulerItem.addActionListener(e -> {
boolean en = rulerItem.isSelected();
terminalPanel.setCrosshairRulerEnabled(en);
haus.nightmare.j3270.config.Settings.setCrosshairRuler(en);
});
viewMenu.add(rulerItem);
JMenu cursorMenu = createMenu("Cursor Style");
ButtonGroup cursorGroup = new ButtonGroup();
JRadioButtonMenuItem blockCursorItem = new JRadioButtonMenuItem("Block Cursor", terminalPanel.getCursorStyle() == TerminalPanel.CursorStyle.BLOCK);
ThemeManager.styleMenuItem(blockCursorItem);
blockCursorItem.addActionListener(e -> {
terminalPanel.setCursorStyle(TerminalPanel.CursorStyle.BLOCK);
haus.nightmare.j3270.config.Settings.setCursorStyle("BLOCK");
});
cursorGroup.add(blockCursorItem);
cursorMenu.add(blockCursorItem);
JRadioButtonMenuItem underlineCursorItem = new JRadioButtonMenuItem("Underline Cursor", terminalPanel.getCursorStyle() == TerminalPanel.CursorStyle.UNDERLINE);
ThemeManager.styleMenuItem(underlineCursorItem);
underlineCursorItem.addActionListener(e -> {
terminalPanel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE);
haus.nightmare.j3270.config.Settings.setCursorStyle("UNDERLINE");
});
cursorGroup.add(underlineCursorItem);
cursorMenu.add(underlineCursorItem);
viewMenu.add(cursorMenu);
viewMenu.addSeparator();
// CodePage Submenu // CodePage Submenu
JMenu cpMenu = createMenu("Code Page"); JMenu cpMenu = createMenu("Code Page");
String[] codePages = { String[] codePages = {
@@ -224,10 +343,81 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
viewMenu.add(gfxMenu); viewMenu.add(gfxMenu);
viewMenu.addSeparator(); viewMenu.addSeparator();
viewMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog)); viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
menuBar.add(viewMenu); menuBar.add(viewMenu);
// 4. Actions menu // 4. Modes menu
JMenu modesMenu = createMenu("Modes");
docModeItem = new JCheckBoxMenuItem("Document Mode (DOC)", terminalPanel.isDocMode());
ThemeManager.styleMenuItem(docModeItem);
docModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.ALT_DOWN_MASK));
docModeItem.addActionListener(e -> terminalPanel.toggleDocMode());
modesMenu.add(docModeItem);
wordWrapItem = new JCheckBoxMenuItem("Word Wrap Mode", terminalPanel.isWordWrap());
ThemeManager.styleMenuItem(wordWrapItem);
wordWrapItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.ALT_DOWN_MASK));
wordWrapItem.addActionListener(e -> terminalPanel.toggleWordWrap());
modesMenu.add(wordWrapItem);
aplModeItem = new JCheckBoxMenuItem("APL Keyboard Mode", terminalPanel.isAplMode());
ThemeManager.styleMenuItem(aplModeItem);
aplModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.ALT_DOWN_MASK));
aplModeItem.addActionListener(e -> terminalPanel.toggleAplMode());
modesMenu.add(aplModeItem);
insertModeItem = new JCheckBoxMenuItem("Insert Mode", terminalPanel.isInsertMode());
ThemeManager.styleMenuItem(insertModeItem);
insertModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0));
insertModeItem.addActionListener(e -> terminalPanel.toggleInsertMode());
modesMenu.add(insertModeItem);
modesMenu.addSeparator();
fourColorItem = new JCheckBoxMenuItem("Base 4-Color Override", haus.nightmare.j3270.config.Settings.getFourColorOverride());
ThemeManager.styleMenuItem(fourColorItem);
fourColorItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setFourColorOverride(fourColorItem.isSelected());
terminalPanel.repaint();
});
modesMenu.add(fourColorItem);
numLockItem = new JCheckBoxMenuItem("Numeric Field Lock", haus.nightmare.j3270.config.Settings.getNumericFieldLock());
ThemeManager.styleMenuItem(numLockItem);
numLockItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setNumericFieldLock(numLockItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(numLockItem);
autoSkipItem = new JCheckBoxMenuItem("Auto-Skip Across Fields", haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
ThemeManager.styleMenuItem(autoSkipItem);
autoSkipItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setAutoSkipEnabled(autoSkipItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(autoSkipItem);
insertOffAidItem = new JCheckBoxMenuItem("Reset Insert on AID Key", haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
ThemeManager.styleMenuItem(insertOffAidItem);
insertOffAidItem.addActionListener(e -> {
haus.nightmare.j3270.config.Settings.setInsertOffOnAid(insertOffAidItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(insertOffAidItem);
modesMenu.addMenuListener(new javax.swing.event.MenuListener() {
@Override
public void menuSelected(javax.swing.event.MenuEvent e) {
syncModeMenuItems();
}
@Override public void menuDeselected(javax.swing.event.MenuEvent e) {}
@Override public void menuCanceled(javax.swing.event.MenuEvent e) {}
});
menuBar.add(modesMenu);
// 5. Actions menu
JMenu actionsMenu = createMenu("Actions"); JMenu actionsMenu = createMenu("Actions");
actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> { actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
@@ -272,6 +462,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}, true)); }, true));
actionsMenu.addSeparator(); actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
actionsMenu.add(createMenuItem("Macro Runtime (<HASCRIPT>)...", KeyEvent.VK_M, this::showMacroDialog, true));
actionsMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true)); actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
menuBar.add(actionsMenu); menuBar.add(actionsMenu);
@@ -378,6 +570,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
scriptDialog.setVisible(true); scriptDialog.setVisible(true);
} }
private void showMacroDialog() {
if (macroDialog == null) {
macroDialog = new MacroDialog(this, client, terminalPanel);
}
macroDialog.setVisible(true);
}
private void showFieldInspectorDialog() { private void showFieldInspectorDialog() {
if (client == null) { if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
@@ -401,6 +600,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
printerSessionDialog.setVisible(true); printerSessionDialog.setVisible(true);
} }
private void showScreenHistoryDialog() {
if (screenHistoryDialog == null) {
screenHistoryDialog = new ScreenHistoryDialog(this, client);
} else {
if (client != null && client.getScreenHistoryBuffer() != null) {
screenHistoryDialog.refreshHistoryView(client.getScreenHistoryBuffer().size() - 1);
}
}
screenHistoryDialog.setVisible(true);
screenHistoryDialog.toFront();
}
private void showPasteLineWrapDialog() { private void showPasteLineWrapDialog() {
if (client == null || !client.getConnectionState().isFullSession()) { if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
@@ -536,6 +747,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls()); dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert()); dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage()); dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
dialog.setVisible(true); dialog.setVisible(true);
if (dialog.isConfirmed()) { if (dialog.isConfirmed()) {
@@ -555,6 +767,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return terminalPanel; return terminalPanel;
} }
public Telnet3270Client getClient() {
return client;
}
public StatusBar getStatusBar() {
return statusBar;
}
void connect(ConnectionConfig config) { void connect(ConnectionConfig config) {
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
@@ -564,6 +784,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls()); haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert()); haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage()); haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
if (config.getGraphicsMode() != null) {
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
}
disconnect(); disconnect();
@@ -607,12 +830,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
fileTransfer.cancel(); fileTransfer.cancel();
fileTransfer = null; fileTransfer = null;
} }
client.disconnect(); if (macroDialog != null) {
macroDialog.stopMacro();
}
Telnet3270Client c = client;
client = null; client = null;
terminalPanel.setClient(null); new Thread(() -> {
statusBar.setClient(null, terminalPanel); try {
terminalPanel.repaint(); c.disconnect();
setTitle("j3270 — Java TN3270 Terminal Emulator"); } catch (Exception ignored) {}
}, "Disconnect-Thread").start();
Runnable uiReset = () -> {
terminalPanel.setClient(null);
statusBar.setClient(null, terminalPanel);
syncModeMenuItems();
terminalPanel.repaint();
setTitle("j3270 — Java TN3270 Terminal Emulator");
};
if (SwingUtilities.isEventDispatchThread()) {
uiReset.run();
} else {
SwingUtilities.invokeLater(uiReset);
}
} }
} }
@@ -623,6 +863,27 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
terminalPanel.guardedPack(); terminalPanel.guardedPack();
} }
public void toggleStatusBar() {
if (statusBar != null) {
setStatusBarVisible(!statusBar.isVisible());
}
}
public void setStatusBarVisible(boolean visible) {
if (statusBar != null) {
statusBar.setVisible(visible);
}
haus.nightmare.j3270.config.Settings.setStatusBarVisible(visible);
if (statusBarItem != null) {
statusBarItem.setSelected(visible);
}
getContentPane().revalidate();
getContentPane().repaint();
if ((getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) {
terminalPanel.guardedPack();
}
}
// ========== ConnectionListener ========== // ========== ConnectionListener ==========
@Override @Override
@@ -632,7 +893,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
statusBar.updateStatus(); statusBar.updateStatus();
terminalPanel.repaint(); terminalPanel.repaint();
if (newState.isFullSession() && !oldState.isFullSession()) { if (newState == ConnectionState.RECONNECTING) {
setTitle("j3270 — " + lastHost + ":" + lastPort + " [Reconnecting...]");
} else if (newState == ConnectionState.NOT_CONNECTED) {
setTitle("j3270 — Java TN3270 Terminal Emulator");
} else if (newState.isFullSession() && !oldState.isFullSession()) {
String tlsIndicator = (client != null && client.getConfig().isUseTls()) ?
(client.getConfig().isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ?
" [" + client.getTelnetFSM().getConnectedLu() + "]" : "";
setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev);
terminalPanel.guardedPack(); terminalPanel.guardedPack();
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
} }
@@ -671,6 +941,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
terminalPanel.repaint(); terminalPanel.repaint();
statusBar.updateStatus(); statusBar.updateStatus();
syncModeMenuItems();
}); });
} }
} }
@@ -688,6 +959,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}); });
} }
@Override
public void onCursorMoved(int oldAddress, int newAddress) {
SwingUtilities.invokeLater(() -> {
terminalPanel.repaint();
statusBar.updateStatus();
});
}
@Override
public void onKeyboardUnlocked() {
SwingUtilities.invokeLater(statusBar::updateStatus);
}
// ========== Help dialogs ========== // ========== Help dialogs ==========
private void showKeyMappings() { private void showKeyMappings() {
@@ -703,8 +987,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"End — Erase to end of field\n" + "End — Erase to end of field\n" +
"Delete — Delete character\n" + "Delete — Delete character\n" +
"Backspace — Backspace\n" + "Backspace — Backspace\n" +
"Insert — Toggle insert mode\n" + "Insert / Help / Alt+I — Toggle insert mode\n" +
"Escape — Reset\n" + "Escape / Alt+R — Reset\n" +
"PageUp/Down — PF7/PF8\n" + "PageUp/Down — PF7/PF8\n" +
"Alt+C / Alt+K — Clear\n" + "Alt+C / Alt+K — Clear\n" +
"Alt+E — Erase Input\n" + "Alt+E — Erase Input\n" +
@@ -713,7 +997,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"Alt+Q — Cursor Select\n" + "Alt+Q — Cursor Select\n" +
"Alt+L — Toggle Light Pen\n" + "Alt+L — Toggle Light Pen\n" +
"Alt+T — File Transfer\n" + "Alt+T — File Transfer\n" +
"Alt+Shift+R — Crosshair Ruler\n" +
"Alt+B — Toggle Status Bar\n" +
"Alt+=/-/0 — Font size +/-/reset\n" + "Alt+=/-/0 — Font size +/-/reset\n" +
"Cmd/Ctrl+C / Ctrl+Ins — Copy\n" +
"Cmd/Ctrl+V / Shift+Ins — Paste\n" +
"Cmd/Ctrl+A — Select All\n" +
"Cmd/Ctrl+F — Find on Screen\n" + "Cmd/Ctrl+F — Find on Screen\n" +
"Cmd/Ctrl+G / F3— Find Next\n" + "Cmd/Ctrl+G / F3— Find Next\n" +
"Cmd/Ctrl+D — Disconnect\n" + "Cmd/Ctrl+D — Disconnect\n" +
@@ -721,7 +1010,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(area); ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp); ThemeManager.styleScrollPane(sp);
@@ -754,7 +1043,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(area); ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp); ThemeManager.styleScrollPane(sp);
@@ -778,6 +1067,109 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"About j3270", JOptionPane.INFORMATION_MESSAGE); "About j3270", JOptionPane.INFORMATION_MESSAGE);
} }
/**
* Configure application logging. When debug is false, disables all logging
* to console and file, preventing creation of j3270.log.
*/
public static void configureLogging(boolean debug) {
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
try {
h.close();
} catch (Exception ignored) {
}
}
if (debug) {
Level logLevel = Level.ALL;
globalRoot.setLevel(Level.ALL);
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());
}
} else {
globalRoot.setLevel(Level.OFF);
for (String pkg : new String[]{"haus.nightmare", "haus.nightmare.j3270", "haus.nightmare.lib3270j", "org.pubvm"}) {
Logger l = Logger.getLogger(pkg);
l.setLevel(Level.OFF);
for (java.util.logging.Handler h : l.getHandlers()) {
l.removeHandler(h);
try {
h.close();
} catch (Exception ignored) {
}
}
}
}
}
private static void applyProxyUrl(ConnectionConfig config, String proxyUrl) {
if (config == null || proxyUrl == null || proxyUrl.trim().isEmpty()) return;
try {
java.net.URI uri = new java.net.URI(proxyUrl.trim());
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
ConnectionConfig.ProxyType pType;
int pPort;
if (scheme.equals("http") || scheme.equals("https")) {
pType = ConnectionConfig.ProxyType.HTTP;
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
pType = ConnectionConfig.ProxyType.SOCKS4;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
pType = ConnectionConfig.ProxyType.SOCKS5;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else {
return;
}
String pHost = uri.getHost();
String pUser = null;
String pPass = null;
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;
}
}
if (pHost != null && !pHost.isEmpty()) {
config.setProxy(pType, pHost, pPort, pUser, pPass);
}
} catch (Exception e) {
log.warning("Failed to parse CLI proxy URL: " + proxyUrl);
}
}
// ========== Main ========== // ========== Main ==========
public static void main(String[] args) { public static void main(String[] args) {
@@ -785,8 +1177,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
boolean cliTls = false; boolean cliTls = false;
boolean cliNoVerifyCert = false; boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null; Boolean cliTn3270e = null;
Boolean cliAutoSysUnlock = null;
GraphicsMode cliGraphicsMode = null; GraphicsMode cliGraphicsMode = null;
String configFile = null; String configFile = null;
String cliProxyUrl = null;
String cliProfileName = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>(); java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
@@ -801,6 +1196,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
cliTn3270e = false; cliTn3270e = false;
} else if ("--tn3270e".equals(arg)) { } else if ("--tn3270e".equals(arg)) {
cliTn3270e = true; cliTn3270e = true;
} else if ("--auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = true;
} else if ("--no-auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = false;
} else if (arg.startsWith("--graphics=")) { } else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11)); cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) { } else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
@@ -809,6 +1208,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
cliGraphicsMode = GraphicsMode.NONE; cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) { } else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
configFile = args[++i]; configFile = args[++i];
} else if (arg.startsWith("--proxy=") || arg.startsWith("-proxy=")) {
cliProxyUrl = arg.substring(arg.indexOf('=') + 1).trim();
} else if (("--proxy".equals(arg) || "-proxy".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliProxyUrl = args[++i].trim();
} else if (arg.startsWith("--profile=")) {
cliProfileName = arg.substring(arg.indexOf('=') + 1).trim();
} else if ("--profile".equals(arg) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliProfileName = args[++i].trim();
} else if (arg.startsWith("-")) { } else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg); System.err.println("Unknown option: " + arg);
} else { } else {
@@ -816,42 +1223,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
} }
Level logLevel = debug ? Level.FINE : Level.INFO; configureLogging(debug);
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) { if (configFile != null) {
try { try {
@@ -877,12 +1250,35 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final boolean finalTls = cliTls; final boolean finalTls = cliTls;
final boolean finalNoVerify = cliNoVerifyCert; final boolean finalNoVerify = cliNoVerifyCert;
final Boolean finalTn3270e = cliTn3270e; final Boolean finalTn3270e = cliTn3270e;
final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
final GraphicsMode finalGraphicsMode = cliGraphicsMode; final GraphicsMode finalGraphicsMode = cliGraphicsMode;
final String finalProxyUrl = cliProxyUrl;
final String finalProfileName = cliProfileName;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App(); J3270App app = new J3270App();
app.setVisible(true); app.setVisible(true);
if (finalProfileName != null) {
haus.nightmare.j3270.storage.SavedHost profile = haus.nightmare.j3270.storage.HostStorage.findByName(finalProfileName);
if (profile == null) {
profile = haus.nightmare.j3270.storage.HostStorage.findById(finalProfileName);
}
if (profile != null) {
ConnectionConfig config = profile.toConnectionConfig();
if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
app.connect(config);
return;
} else {
System.err.println("Profile not found: " + finalProfileName);
}
}
if (!remainingArgs.isEmpty()) { if (!remainingArgs.isEmpty()) {
String hostArg = remainingArgs.get(0); String hostArg = remainingArgs.get(0);
int port = finalTls ? 992 : 23; int port = finalTls ? 992 : 23;
@@ -893,10 +1289,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
TerminalModel model = TerminalModel.IBM_3279_4; TerminalModel model = TerminalModel.IBM_3279_4;
if (remainingArgs.size() >= 3) { if (remainingArgs.size() >= 3) {
try { String mArg = remainingArgs.get(2).trim();
int modelNum = Integer.parseInt(remainingArgs.get(2)); if (mArg.equalsIgnoreCase("dynamic") || mArg.equals("0")) {
model = TerminalModel.forModel(modelNum, true); model = TerminalModel.IBM_DYNAMIC;
} catch (Exception ignored) {} } else {
try {
int modelNum = Integer.parseInt(mArg);
model = TerminalModel.forModel(modelNum, true);
} catch (Exception ignored) {}
}
} }
ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model); ConnectionConfig config = ConnectionConfig.parseHostString(hostArg, port, model);
if (finalTls) { if (finalTls) {
@@ -908,11 +1309,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (finalTn3270e != null) { if (finalTn3270e != null) {
config.setTn3270eEnabled(finalTn3270e); config.setTn3270eEnabled(finalTn3270e);
} }
if (finalAutoSysUnlock != null) {
config.setAutoSysUnlock(finalAutoSysUnlock);
} else {
config.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
}
if (finalGraphicsMode != null) { if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode); config.setGraphicsMode(finalGraphicsMode);
} else { } else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
} }
if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl);
}
app.connect(config); app.connect(config);
} else { } else {
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
@@ -922,23 +1331,41 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
SwingUtilities.invokeLater(app::showConnectDialog); SwingUtilities.invokeLater(app::showConnectDialog);
break; break;
case AUTO_CONNECT: case AUTO_CONNECT:
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost(); haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost();
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort(); if (autoHost != null) {
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls(); ConnectionConfig config = autoHost.toConnectionConfig();
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert(); if (finalTls) config.setUseTls(true);
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e(); if (finalNoVerify) config.setTlsVerifyCert(false);
if (host != null && !host.isEmpty()) { if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
config.setTlsVerifyCert(verify); if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e); if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
}
app.connect(config); app.connect(config);
} else { } else {
SwingUtilities.invokeLater(app::showConnectDialog); 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);
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
}
if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl);
}
app.connect(config);
} else {
SwingUtilities.invokeLater(app::showConnectDialog);
}
} }
break; break;
case DO_NOTHING: case DO_NOTHING:
@@ -33,6 +33,7 @@ public class Settings {
public static void setFontFamily(String family) { public static void setFontFamily(String family) {
prefs.put("fontFamily", family); prefs.put("fontFamily", family);
flushPrefs();
} }
public static int getFontSize() { public static int getFontSize() {
@@ -41,6 +42,32 @@ public class Settings {
public static void setFontSize(int size) { public static void setFontSize(int size) {
prefs.putInt("fontSize", size); prefs.putInt("fontSize", size);
flushPrefs();
}
public static String getUiFontFamily() {
return prefs.get("uiFontFamily", "SansSerif");
}
public static void setUiFontFamily(String family) {
prefs.put("uiFontFamily", (family != null && !family.trim().isEmpty()) ? family : "SansSerif");
flushPrefs();
}
public static int getUiFontSize() {
return prefs.getInt("uiFontSize", 13);
}
public static void setUiFontSize(int size) {
prefs.putInt("uiFontSize", Math.max(8, Math.min(size, 48)));
flushPrefs();
}
private static void flushPrefs() {
try {
prefs.flush();
} catch (Exception ignored) {
}
} }
public static StartupBehavior getStartupBehavior() { public static StartupBehavior getStartupBehavior() {
@@ -96,6 +123,85 @@ public class Settings {
prefs.putBoolean("autoConnectTn3270e", tn3270e); prefs.putBoolean("autoConnectTn3270e", tn3270e);
} }
public static boolean getAutoSysUnlock() {
return prefs.getBoolean("autoSysUnlock", true);
}
public static void setAutoSysUnlock(boolean autoSysUnlock) {
prefs.putBoolean("autoSysUnlock", autoSysUnlock);
flushPrefs();
}
public static boolean getAutoConnectKeepAlive() {
return prefs.getBoolean("autoConnectKeepAlive", true);
}
public static void setAutoConnectKeepAlive(boolean keepAlive) {
prefs.putBoolean("autoConnectKeepAlive", keepAlive);
flushPrefs();
}
public static int getAutoConnectKeepAliveInterval() {
return prefs.getInt("autoConnectKeepAliveInterval", 120);
}
public static void setAutoConnectKeepAliveInterval(int interval) {
prefs.putInt("autoConnectKeepAliveInterval", interval);
flushPrefs();
}
public static boolean getAutoConnectAutoReconnect() {
return prefs.getBoolean("autoConnectAutoReconnect", false);
}
public static void setAutoConnectAutoReconnect(boolean autoReconnect) {
prefs.putBoolean("autoConnectAutoReconnect", autoReconnect);
flushPrefs();
}
public static boolean getAutoReconnect() {
return getAutoConnectAutoReconnect();
}
public static void setAutoReconnect(boolean autoReconnect) {
setAutoConnectAutoReconnect(autoReconnect);
}
public static int getAutoConnectReconnectMaxRetries() {
return prefs.getInt("autoConnectReconnectMaxRetries", 5);
}
public static void setAutoConnectReconnectMaxRetries(int retries) {
prefs.putInt("autoConnectReconnectMaxRetries", retries);
flushPrefs();
}
public static boolean getInputMask() {
return prefs.getBoolean("inputMask", true);
}
public static void setInputMask(boolean mask) {
prefs.putBoolean("inputMask", mask);
flushPrefs();
}
public static boolean getInputMaskEnabled() {
return getInputMask();
}
public static void setInputMaskEnabled(boolean enabled) {
setInputMask(enabled);
}
public static String getInputMaskChar() {
return prefs.get("inputMaskChar", "*");
}
public static void setInputMaskChar(String ch) {
prefs.put("inputMaskChar", (ch != null && !ch.trim().isEmpty()) ? ch.trim().substring(0, 1) : "*");
flushPrefs();
}
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name()); String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr); return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
@@ -113,6 +219,22 @@ public class Settings {
prefs.put("codePage", (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037"); prefs.put("codePage", (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037");
} }
public static int getDynamicRows() {
return prefs.getInt("dynamicRows", 62);
}
public static void setDynamicRows(int rows) {
prefs.putInt("dynamicRows", Math.max(1, rows));
}
public static int getDynamicCols() {
return prefs.getInt("dynamicCols", 160);
}
public static void setDynamicCols(int cols) {
prefs.putInt("dynamicCols", Math.max(1, cols));
}
public static Color getColorOverride(int index, Color defaultColor) { public static Color getColorOverride(int index, Color defaultColor) {
String hex = prefs.get("color_" + index, null); String hex = prefs.get("color_" + index, null);
try { try {
@@ -139,8 +261,61 @@ public class Settings {
prefs.put("mono_" + key, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue())); prefs.put("mono_" + key, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
} }
public static boolean isMac() {
String os = System.getProperty("os.name");
return os != null && os.toLowerCase().contains("mac");
}
public static String getDefaultBinding(String action) {
boolean mac = isMac();
switch (action) {
case "COPY":
return mac ? "meta C, ctrl C, ctrl INSERT, ctrl shift C" : "ctrl C, ctrl INSERT, ctrl shift C";
case "PASTE":
return mac ? "meta V, ctrl V, shift INSERT, ctrl shift V" : "ctrl V, shift INSERT, ctrl shift V";
case "SELECTALL":
return mac ? "meta A, ctrl A" : "ctrl A";
case "INSERT":
return mac ? "INSERT, HELP, alt I" : "INSERT, ctrl I";
case "ERASE_INPUT":
return "alt E";
case "NEWLINE":
return "shift ENTER";
case "DUP":
return "alt D";
case "FIELD_MARK":
return "alt M";
case "ATTN":
return "alt A";
case "SYSREQ":
return "alt S";
case "CURSEL":
return "alt Q";
case "CLEAR":
return "alt C";
case "STATUS_BAR":
return "alt B";
default:
return action;
}
}
public static String getKeyBinding(String action, String defaultBinding) { public static String getKeyBinding(String action, String defaultBinding) {
return prefs.get("key_" + action, defaultBinding); String val = prefs.get("key_" + action, null);
if (val == null) {
return defaultBinding;
}
// If stored binding is the legacy single-key "INSERT" default, migrate it to the current platform default
if ("INSERT".equals(action) && "INSERT".equals(val)) {
prefs.put("key_INSERT", defaultBinding);
return defaultBinding;
}
// If stored binding is the legacy or corrupted "alt STE" for PASTE, migrate it to the current platform default
if ("PASTE".equals(action) && ("alt STE".equals(val) || "PASTE".equals(val))) {
prefs.put("key_PASTE", defaultBinding);
return defaultBinding;
}
return val;
} }
public static void setKeyBinding(String action, String binding) { public static void setKeyBinding(String action, String binding) {
@@ -233,17 +408,288 @@ public class Settings {
prefs.putBoolean("blockSelectMode", block); prefs.putBoolean("blockSelectMode", block);
} }
// ========== Block paste ==========
public static boolean getBlockPaste() {
return prefs.getBoolean("blockPaste", true);
}
public static void setBlockPaste(boolean block) {
prefs.putBoolean("blockPaste", block);
flushPrefs();
}
// ========== Clipboard & Tabular Paste ==========
public static boolean getEnablePasteFromExcel() {
return prefs.getBoolean("enablePasteFromExcel", true);
}
public static void setEnablePasteFromExcel(boolean val) {
prefs.putBoolean("enablePasteFromExcel", val);
flushPrefs();
}
public static boolean getPasteStopAtProtectedLine() {
return prefs.getBoolean("pasteStopAtProtectedLine", false);
}
public static void setPasteStopAtProtectedLine(boolean val) {
prefs.putBoolean("pasteStopAtProtectedLine", val);
flushPrefs();
}
public static boolean getTrimRectSizingHandles() {
return prefs.getBoolean("trimRectSizingHandles", false);
}
public static void setTrimRectSizingHandles(boolean val) {
prefs.putBoolean("trimRectSizingHandles", val);
flushPrefs();
}
public static boolean getTrimRectRemainAfterEdit() {
return prefs.getBoolean("trimRectRemainAfterEdit", false);
}
public static void setTrimRectRemainAfterEdit(boolean val) {
prefs.putBoolean("trimRectRemainAfterEdit", val);
flushPrefs();
}
public static boolean getPasteWordBreak() {
return prefs.getBoolean("pasteWordBreak", true);
}
public static void setPasteWordBreak(boolean val) {
prefs.putBoolean("pasteWordBreak", val);
flushPrefs();
}
// ========== Crosshair Ruler ==========
public static boolean getCrosshairRuler() {
return prefs.getBoolean("crosshairRuler", false);
}
public static void setCrosshairRuler(boolean enabled) {
prefs.putBoolean("crosshairRuler", enabled);
}
// ========== Phase 25: Hotspots & Mouse Wheel Navigation ==========
public static boolean getHotspotsEnabled() {
return prefs.getBoolean("hotspotsEnabled", true);
}
public static void setHotspotsEnabled(boolean enabled) {
prefs.putBoolean("hotspotsEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsUrlEnabled() {
return prefs.getBoolean("hotspotsUrlEnabled", true);
}
public static void setHotspotsUrlEnabled(boolean enabled) {
prefs.putBoolean("hotspotsUrlEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsPfEnabled() {
return prefs.getBoolean("hotspotsPfEnabled", true);
}
public static void setHotspotsPfEnabled(boolean enabled) {
prefs.putBoolean("hotspotsPfEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsMenuChoicesEnabled() {
return prefs.getBoolean("hotspotsMenuChoicesEnabled", true);
}
public static void setHotspotsMenuChoicesEnabled(boolean enabled) {
prefs.putBoolean("hotspotsMenuChoicesEnabled", enabled);
flushPrefs();
}
public static boolean getMouseWheelEnabled() {
return prefs.getBoolean("mouseWheelEnabled", true);
}
public static void setMouseWheelEnabled(boolean enabled) {
prefs.putBoolean("mouseWheelEnabled", enabled);
flushPrefs();
}
public static String getMouseWheelUpKey() {
return prefs.get("mouseWheelUpKey", "[pf7]");
}
public static void setMouseWheelUpKey(String key) {
prefs.put("mouseWheelUpKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf7]");
flushPrefs();
}
public static String getMouseWheelDownKey() {
return prefs.get("mouseWheelDownKey", "[pf8]");
}
public static void setMouseWheelDownKey(String key) {
prefs.put("mouseWheelDownKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf8]");
flushPrefs();
}
// ========== Status Bar Visibility ==========
public static boolean getStatusBarVisible() {
return prefs.getBoolean("statusBarVisible", true);
}
public static void setStatusBarVisible(boolean visible) {
prefs.putBoolean("statusBarVisible", visible);
}
// ========== Cursor Style ==========
public static String getCursorStyle() {
return prefs.get("cursorStyle", "BLOCK");
}
public static void setCursorStyle(String style) {
prefs.put("cursorStyle", style != null ? style.toUpperCase() : "BLOCK");
}
// ========== Entry Assist & Modes ==========
public static boolean getEntryAssistDocMode() {
return prefs.getBoolean("entryAssistDocMode", false);
}
public static void setEntryAssistDocMode(boolean docMode) {
prefs.putBoolean("entryAssistDocMode", docMode);
}
public static boolean getEntryAssistWordWrap() {
return prefs.getBoolean("entryAssistWordWrap", false);
}
public static void setEntryAssistWordWrap(boolean wordWrap) {
prefs.putBoolean("entryAssistWordWrap", wordWrap);
}
public static int getEntryAssistStartCol() {
return prefs.getInt("entryAssistStartCol", 1);
}
public static void setEntryAssistStartCol(int startCol) {
prefs.putInt("entryAssistStartCol", startCol);
}
public static int getEntryAssistEndCol() {
return prefs.getInt("entryAssistEndCol", 80);
}
public static void setEntryAssistEndCol(int endCol) {
prefs.putInt("entryAssistEndCol", endCol);
}
public static boolean getEntryAssistBell() {
return prefs.getBoolean("entryAssistBell", false);
}
public static void setEntryAssistBell(boolean bell) {
prefs.putBoolean("entryAssistBell", bell);
}
public static int getEntryAssistBellCol() {
return prefs.getInt("entryAssistBellCol", 75);
}
public static void setEntryAssistBellCol(int col) {
prefs.putInt("entryAssistBellCol", col);
}
public static String getEntryAssistTabStops() {
return prefs.get("entryAssistTabStops", "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75");
}
public static void setEntryAssistTabStops(String stops) {
prefs.put("entryAssistTabStops", stops != null ? stops : "");
}
public static int[] getEntryAssistTabStopsArray() {
String s = getEntryAssistTabStops();
if (s == null || s.trim().isEmpty()) return new int[0];
String[] parts = s.split(",");
java.util.List<Integer> list = new java.util.ArrayList<>();
for (String p : parts) {
try {
int val = Integer.parseInt(p.trim());
if (val > 0) list.add(val - 1);
} catch (NumberFormatException ignored) {}
}
int[] res = new int[list.size()];
for (int i = 0; i < list.size(); i++) res[i] = list.get(i);
return res;
}
public static boolean getInsertOffOnAid() {
return prefs.getBoolean("insertOffOnAid", true);
}
public static void setInsertOffOnAid(boolean val) {
prefs.putBoolean("insertOffOnAid", val);
}
public static boolean getFourColorOverride() {
return prefs.getBoolean("fourColorOverride", false);
}
public static void setFourColorOverride(boolean val) {
prefs.putBoolean("fourColorOverride", val);
}
public static boolean getNumericFieldLock() {
return prefs.getBoolean("numericFieldLock", true);
}
public static void setNumericFieldLock(boolean val) {
prefs.putBoolean("numericFieldLock", val);
}
public static boolean getAutoSkipEnabled() {
return prefs.getBoolean("autoSkipEnabled", true);
}
public static void setAutoSkipEnabled(boolean val) {
prefs.putBoolean("autoSkipEnabled", val);
}
private static void applyConfigEntry(String section, String key, String value) { private static void applyConfigEntry(String section, String key, String value) {
switch (section) { switch (section) {
case "appearance": case "appearance":
switch (key) { switch (key) {
case "fontFamily": setFontFamily(value); break; case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break; case "fontSize": setFontSize(Integer.parseInt(value)); break;
case "uiFontFamily":
case "uifontfamily":
case "uifont":
case "uiFont":
setUiFontFamily(value);
break;
case "uiFontSize":
case "uifontsize":
setUiFontSize(Integer.parseInt(value));
break;
case "javaUiTheme": case "javaUiTheme":
case "theme": case "theme":
case "uiTheme": case "uiTheme":
setJavaUiTheme(UITheme.fromString(value)); setJavaUiTheme(UITheme.fromString(value));
break; break;
case "crosshairRuler":
case "ruler":
setCrosshairRuler(Boolean.parseBoolean(value));
break;
case "statusBarVisible":
case "statusbar":
case "statusBar":
case "showStatusBar":
setStatusBarVisible(Boolean.parseBoolean(value));
break;
case "cursorStyle":
setCursorStyle(value);
break;
default: default:
log.warning("Unknown appearance key: " + key); log.warning("Unknown appearance key: " + key);
} }
@@ -284,12 +730,103 @@ public class Settings {
case "charset": case "charset":
setCodePage(value); setCodePage(value);
break; break;
case "dynamicRows":
case "dynamic_rows":
setDynamicRows(Integer.parseInt(value));
break;
case "dynamicCols":
case "dynamic_cols":
setDynamicCols(Integer.parseInt(value));
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
case "autoReconnect":
case "auto_reconnect":
case "autoConnectAutoReconnect":
setAutoConnectAutoReconnect(Boolean.parseBoolean(value));
break;
case "reconnectMaxRetries":
case "autoConnectReconnectMaxRetries":
setAutoConnectReconnectMaxRetries(Integer.parseInt(value));
break;
case "inputMask":
case "input_mask":
case "inputMaskEnabled":
case "maskInput":
setInputMask(Boolean.parseBoolean(value));
break;
case "inputMaskChar":
case "input_mask_char":
case "maskChar":
setInputMaskChar(value);
break;
case "autoSysUnlock":
case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value));
break;
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel":
case "pasteFromExcel":
case "excelPaste":
setEnablePasteFromExcel(Boolean.parseBoolean(value));
break;
case "pasteStopAtProtectedLine":
case "stopAtProtected":
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
case "trimRectSizingHandles":
case "sizingHandles":
setTrimRectSizingHandles(Boolean.parseBoolean(value));
break;
case "trimRectRemainAfterEdit":
case "remainAfterEdit":
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
break;
case "pasteWordBreak":
case "wordBreak":
setPasteWordBreak(Boolean.parseBoolean(value));
break;
default: default:
log.warning("Unknown behavior/connection key: " + key); log.warning("Unknown behavior/connection key: " + key);
} }
break; break;
case "clipboard":
switch (key) {
case "blockPaste":
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel":
case "pasteFromExcel":
case "excelPaste":
setEnablePasteFromExcel(Boolean.parseBoolean(value));
break;
case "pasteStopAtProtectedLine":
case "stopAtProtected":
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
case "trimRectSizingHandles":
case "sizingHandles":
setTrimRectSizingHandles(Boolean.parseBoolean(value));
break;
case "trimRectRemainAfterEdit":
case "remainAfterEdit":
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
break;
case "pasteWordBreak":
case "wordBreak":
setPasteWordBreak(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown clipboard key: " + key);
}
break;
case "colors": case "colors":
if (key.startsWith("color_")) { if (key.startsWith("color_")) {
int index = Integer.parseInt(key.substring(6)); int index = Integer.parseInt(key.substring(6));
@@ -314,6 +851,55 @@ public class Settings {
} }
break; break;
case "entryassist":
case "modes":
switch (key) {
case "docMode":
case "entryAssistDocMode":
setEntryAssistDocMode(Boolean.parseBoolean(value));
break;
case "wordWrap":
case "entryAssistWordWrap":
setEntryAssistWordWrap(Boolean.parseBoolean(value));
break;
case "startCol":
case "entryAssistStartCol":
setEntryAssistStartCol(Integer.parseInt(value));
break;
case "endCol":
case "entryAssistEndCol":
setEntryAssistEndCol(Integer.parseInt(value));
break;
case "bell":
case "entryAssistBell":
setEntryAssistBell(Boolean.parseBoolean(value));
break;
case "bellCol":
case "entryAssistBellCol":
setEntryAssistBellCol(Integer.parseInt(value));
break;
case "tabStops":
case "entryAssistTabStops":
setEntryAssistTabStops(value);
break;
case "insertOffOnAid":
setInsertOffOnAid(Boolean.parseBoolean(value));
break;
case "fourColorOverride":
setFourColorOverride(Boolean.parseBoolean(value));
break;
case "numericFieldLock":
setNumericFieldLock(Boolean.parseBoolean(value));
break;
case "autoSkip":
case "autoSkipEnabled":
setAutoSkipEnabled(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown entryassist/modes key: " + key);
}
break;
default: default:
// Allow bare keys outside any section — treat as raw prefs // Allow bare keys outside any section — treat as raw prefs
log.fine("Setting raw preference: " + key + " = " + value); log.fine("Setting raw preference: " + key + " = " + value);
@@ -338,8 +924,13 @@ public class Settings {
// [appearance] // [appearance]
w.println("[appearance]"); w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name()); w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("uiFontFamily = " + getUiFontFamily());
w.println("uiFontSize = " + getUiFontSize());
w.println("fontFamily = " + getFontFamily()); w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize()); w.println("fontSize = " + getFontSize());
w.println("crosshairRuler = " + getCrosshairRuler());
w.println("statusBarVisible = " + getStatusBarVisible());
w.println("cursorStyle = " + getCursorStyle());
w.println(); w.println();
// [behavior] // [behavior]
@@ -354,7 +945,39 @@ public class Settings {
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e()); w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
} }
w.println("codePage = " + getCodePage()); w.println("codePage = " + getCodePage());
w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
w.println("blockPaste = " + getBlockPaste());
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
w.println("inputMask = " + getInputMask());
w.println("inputMaskChar = " + getInputMaskChar());
w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
w.println("trimRectSizingHandles = " + getTrimRectSizingHandles());
w.println("trimRectRemainAfterEdit = " + getTrimRectRemainAfterEdit());
w.println("pasteWordBreak = " + getPasteWordBreak());
w.println();
// [entryassist]
w.println("[entryassist]");
w.println("docMode = " + getEntryAssistDocMode());
w.println("wordWrap = " + getEntryAssistWordWrap());
w.println("startCol = " + getEntryAssistStartCol());
w.println("endCol = " + getEntryAssistEndCol());
w.println("bell = " + getEntryAssistBell());
w.println("bellCol = " + getEntryAssistBellCol());
w.println("tabStops = " + getEntryAssistTabStops());
w.println();
// [modes]
w.println("[modes]");
w.println("insertOffOnAid = " + getInsertOffOnAid());
w.println("fourColorOverride = " + getFourColorOverride());
w.println("numericFieldLock = " + getNumericFieldLock());
w.println("autoSkip = " + getAutoSkipEnabled());
w.println(); w.println();
// [graphics] // [graphics]
@@ -383,9 +1006,10 @@ public class Settings {
// [keybindings] // [keybindings]
w.println("[keybindings]"); w.println("[keybindings]");
// Navigation keys // Navigation & clipboard keys
String[] navActions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT", String[] navActions = {"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",
"STATUS_BAR", "COPY", "PASTE", "SELECTALL"};
for (String act : navActions) { for (String act : navActions) {
String val = prefs.get("key_" + act, null); String val = prefs.get("key_" + act, null);
if (val != null) { if (val != null) {
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.ft; package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.AppIcon;
import haus.nightmare.j3270.ui.HostDirectoryDialog; import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager; import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
@@ -12,6 +13,50 @@ import java.io.File;
public class FileTransferDialog extends JDialog { public class FileTransferDialog extends JDialog {
public static class TransferSessionState {
public FTConfig.HostType hostType = FTConfig.HostType.TSO;
public boolean isSend = false;
public String localFile = "";
public String hostFile = "";
public boolean isAscii = true;
public int mtu = FTConstants.DFT_BUF;
public boolean crFlag = true;
public boolean remapFlag = true;
public boolean append = false;
public boolean overwrite = false;
public String recfm = "";
public String lrecl = "";
public String blksize = "";
public String space = "";
public String options = "";
}
private static TransferSessionState lastTransferState = null;
public static TransferSessionState getLastTransferState() {
return lastTransferState;
}
public static void setLastTransferState(TransferSessionState state) {
lastTransferState = state;
}
public static void resetSessionState() {
lastTransferState = null;
}
public static String formatHostFilename(String localPath, FTConfig.HostType hostType) {
if (localPath == null || localPath.trim().isEmpty()) {
return "";
}
File f = new File(localPath.trim());
String name = f.getName();
if (hostType == FTConfig.HostType.CMS) {
name = name.replace('.', ' ');
}
return name;
}
private final FileTransfer coordinator; private final FileTransfer coordinator;
private final Frame owner; private final Frame owner;
@@ -48,6 +93,7 @@ public class FileTransferDialog extends JDialog {
super(owner, "File Transfer (IND$FILE)", false); super(owner, "File Transfer (IND$FILE)", false);
this.owner = owner; this.owner = owner;
this.coordinator = coordinator; this.coordinator = coordinator;
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
@@ -66,6 +112,16 @@ public class FileTransferDialog extends JDialog {
// Host Type // Host Type
hostTypeCombo = new JComboBox<>(FTConfig.HostType.values()); hostTypeCombo = new JComboBox<>(FTConfig.HostType.values());
hostTypeCombo.addActionListener(e -> {
updateOptionStates();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (hostType == FTConfig.HostType.CMS) {
String hostText = hostFileField.getText().trim();
if (hostText.contains(".")) {
hostFileField.setText(hostText.replace('.', ' '));
}
}
});
formPanel.add(createRow("Host Environment:", hostTypeCombo)); formPanel.add(createRow("Host Environment:", hostTypeCombo));
// Direction // Direction
@@ -88,6 +144,15 @@ public class FileTransferDialog extends JDialog {
// Local File // Local File
localFileField = new JTextField(20); localFileField = new JTextField(20);
localFileField.addFocusListener(new java.awt.event.FocusAdapter() {
@Override
public void focusLost(java.awt.event.FocusEvent e) {
if (hostFileField.getText().trim().isEmpty() && !localFileField.getText().trim().isEmpty()) {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
hostFileField.setText(formatHostFilename(localFileField.getText(), hostType));
}
}
});
browseLocalButton = new JButton("Browse..."); browseLocalButton = new JButton("Browse...");
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
browseLocalButton.addActionListener(e -> browseLocalFile()); browseLocalButton.addActionListener(e -> browseLocalFile());
@@ -211,13 +276,47 @@ public class FileTransferDialog extends JDialog {
setContentPane(mainPanel); setContentPane(mainPanel);
prefillFromLastTransfer();
ThemeManager.applyThemeToWindow(this); ThemeManager.applyThemeToWindow(this);
updateOptionStates(); updateOptionStates();
} }
private void prefillFromLastTransfer() {
if (lastTransferState != null) {
if (lastTransferState.hostType != null) {
hostTypeCombo.setSelectedItem(lastTransferState.hostType);
}
sendRadio.setSelected(lastTransferState.isSend);
receiveRadio.setSelected(!lastTransferState.isSend);
if (lastTransferState.localFile != null) {
localFileField.setText(lastTransferState.localFile);
}
if (lastTransferState.hostFile != null) {
hostFileField.setText(lastTransferState.hostFile);
}
asciiRadio.setSelected(lastTransferState.isAscii);
binaryRadio.setSelected(!lastTransferState.isAscii);
if (lastTransferState.mtu > 0) {
mtuCombo.setSelectedItem(lastTransferState.mtu);
}
crCheck.setSelected(lastTransferState.crFlag);
remapCheck.setSelected(lastTransferState.remapFlag);
appendCheck.setSelected(lastTransferState.append);
overwriteCheck.setSelected(lastTransferState.overwrite);
if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm);
if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl);
if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize);
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
}
}
private void updateOptionStates() { private void updateOptionStates() {
boolean isSend = sendRadio.isSelected(); boolean isSend = sendRadio.isSelected();
boolean isAscii = asciiRadio.isSelected(); boolean isAscii = asciiRadio.isSelected();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
boolean isTso = (hostType == FTConfig.HostType.TSO);
boolean isCms = (hostType == FTConfig.HostType.CMS);
appendCheck.setEnabled(!isSend); appendCheck.setEnabled(!isSend);
overwriteCheck.setEnabled(!isSend); overwriteCheck.setEnabled(!isSend);
@@ -225,10 +324,11 @@ public class FileTransferDialog extends JDialog {
crCheck.setEnabled(isAscii); crCheck.setEnabled(isAscii);
remapCheck.setEnabled(isAscii); remapCheck.setEnabled(isAscii);
recfmField.setEnabled(isSend); recfmField.setEnabled(isSend && isTso);
lreclField.setEnabled(isSend); lreclField.setEnabled(isSend && isTso);
blksizeField.setEnabled(isSend); blksizeField.setEnabled(isSend && isTso);
spaceField.setEnabled(isSend); spaceField.setEnabled(isSend && isTso);
optionsField.setEnabled(isCms);
} }
private JPanel createRow(String labelText, Component comp) { private JPanel createRow(String labelText, Component comp) {
@@ -245,9 +345,13 @@ public class FileTransferDialog extends JDialog {
private void browseLocalFile() { private void browseLocalFile() {
JFileChooser chooser = new JFileChooser(); JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
localFileField.setText(chooser.getSelectedFile().getAbsolutePath()); File selected = chooser.getSelectedFile();
if (hostFileField.getText().trim().isEmpty()) { localFileField.setText(selected.getAbsolutePath());
hostFileField.setText(chooser.getSelectedFile().getName()); FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
boolean shouldAutofill = hostFileField.getText().trim().isEmpty() ||
(lastTransferState != null && hostFileField.getText().trim().equals(lastTransferState.hostFile));
if (shouldAutofill) {
hostFileField.setText(formatHostFilename(selected.getName(), hostType));
} }
} }
} }
@@ -260,10 +364,21 @@ public class FileTransferDialog extends JDialog {
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) { if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
hostFileField.setText(dialog.getSelectedHostFile()); hostFileField.setText(dialog.getSelectedHostFile());
if (localFileField.getText().trim().isEmpty()) { boolean shouldAutofill = localFileField.getText().trim().isEmpty() ||
(lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile));
if (shouldAutofill) {
String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", ""); String cleanName = dialog.getSelectedHostFile().replace("'", "").replace("\"", "");
int lastDot = cleanName.lastIndexOf('.'); if (hostType == FTConfig.HostType.CMS) {
if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1); String[] tokens = cleanName.trim().split("\\s+");
if (tokens.length >= 2) {
cleanName = tokens[0].toLowerCase() + "." + tokens[1].toLowerCase();
} else if (tokens.length == 1) {
cleanName = tokens[0].toLowerCase();
}
} else {
int lastDot = cleanName.lastIndexOf('.');
if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1);
}
localFileField.setText(cleanName); localFileField.setText(cleanName);
} }
} }
@@ -298,6 +413,8 @@ public class FileTransferDialog extends JDialog {
config.setSpace(spaceField.getText().trim()); config.setSpace(spaceField.getText().trim());
config.setOptions(optionsField.getText().trim()); config.setOptions(optionsField.getText().trim());
recordSessionTransfer();
String error = coordinator.startTransfer(config); String error = coordinator.startTransfer(config);
if (error != null) { if (error != null) {
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
@@ -306,6 +423,45 @@ public class FileTransferDialog extends JDialog {
} }
} }
public void recordSessionTransfer() {
TransferSessionState state = new TransferSessionState();
state.hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
state.isSend = sendRadio.isSelected();
state.localFile = localFileField.getText().trim();
state.hostFile = hostFileField.getText().trim();
state.isAscii = asciiRadio.isSelected();
Integer mtu = (Integer) mtuCombo.getSelectedItem();
state.mtu = (mtu != null) ? mtu : FTConstants.DFT_BUF;
state.crFlag = crCheck.isSelected();
state.remapFlag = remapCheck.isSelected();
state.append = appendCheck.isSelected();
state.overwrite = overwriteCheck.isSelected();
state.recfm = recfmField.getText().trim();
state.lrecl = lreclField.getText().trim();
state.blksize = blksizeField.getText().trim();
state.space = spaceField.getText().trim();
state.options = optionsField.getText().trim();
lastTransferState = state;
}
public JComboBox<FTConfig.HostType> getHostTypeCombo() { return hostTypeCombo; }
public JRadioButton getSendRadio() { return sendRadio; }
public JRadioButton getReceiveRadio() { return receiveRadio; }
public JTextField getLocalFileField() { return localFileField; }
public JTextField getHostFileField() { return hostFileField; }
public JRadioButton getAsciiRadio() { return asciiRadio; }
public JRadioButton getBinaryRadio() { return binaryRadio; }
public JComboBox<Integer> getMtuCombo() { return mtuCombo; }
public JCheckBox getCrCheck() { return crCheck; }
public JCheckBox getRemapCheck() { return remapCheck; }
public JCheckBox getAppendCheck() { return appendCheck; }
public JCheckBox getOverwriteCheck() { return overwriteCheck; }
public JTextField getRecfmField() { return recfmField; }
public JTextField getLreclField() { return lreclField; }
public JTextField getBlksizeField() { return blksizeField; }
public JTextField getSpaceField() { return spaceField; }
public JTextField getOptionsField() { return optionsField; }
private void applyTheme(Container container) { private void applyTheme(Container container) {
Color fg = new Color(200, 200, 200); Color fg = new Color(200, 200, 200);
Color bg = new Color(40, 40, 40); Color bg = new Color(40, 40, 40);
@@ -0,0 +1,433 @@
package haus.nightmare.j3270.storage;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
/**
* Storage manager for saved host profiles, maintaining 1:1 format compatibility with a3270.
* Persists profiles in Java user preferences with JSON serialization.
*/
public class HostStorage {
private static final Logger log = Logger.getLogger(HostStorage.class.getName());
private static final Preferences prefs = Preferences.userNodeForPackage(HostStorage.class);
public static final String KEY_SAVED_HOSTS = "saved_hosts";
public static synchronized List<SavedHost> getSavedHosts() {
String jsonStr = prefs.get(KEY_SAVED_HOSTS, null);
if (jsonStr == null || jsonStr.trim().isEmpty()) {
return new ArrayList<>();
}
try {
return parseHostsJson(jsonStr);
} catch (Exception e) {
log.log(Level.WARNING, "Failed to parse saved hosts JSON", e);
return new ArrayList<>();
}
}
public static synchronized void saveHost(SavedHost host) {
if (host == null) return;
List<SavedHost> current = new ArrayList<>(getSavedHosts());
int index = -1;
for (int i = 0; i < current.size(); i++) {
if (Objects.equals(current.get(i).getId(), host.getId())) {
index = i;
break;
}
}
// If this host is set to autoConnect, clear autoConnect on all others
SavedHost hostToSave = host.copy();
if (hostToSave.isAutoConnect()) {
for (int i = 0; i < current.size(); i++) {
current.get(i).setAutoConnect(false);
}
}
if (index >= 0) {
current.set(index, hostToSave);
} else {
current.add(hostToSave);
}
saveAll(current);
}
public static synchronized void deleteHost(String hostId) {
if (hostId == null) return;
List<SavedHost> current = new ArrayList<>(getSavedHosts());
current.removeIf(h -> Objects.equals(h.getId(), hostId));
saveAll(current);
}
public static synchronized SavedHost getAutoConnectHost() {
for (SavedHost h : getSavedHosts()) {
if (h.isAutoConnect()) {
return h;
}
}
return null;
}
public static synchronized SavedHost findById(String id) {
if (id == null) return null;
for (SavedHost h : getSavedHosts()) {
if (id.equals(h.getId())) {
return h;
}
}
return null;
}
public static synchronized SavedHost findByName(String name) {
if (name == null) return null;
for (SavedHost h : getSavedHosts()) {
if (name.equalsIgnoreCase(h.getName())) {
return h;
}
}
return null;
}
public static synchronized void saveAll(List<SavedHost> hosts) {
if (hosts == null) return;
String json = serializeHostsJson(hosts);
prefs.put(KEY_SAVED_HOSTS, json);
try {
prefs.flush();
} catch (Exception ignored) {
}
}
public static synchronized void clearAll() {
prefs.remove(KEY_SAVED_HOSTS);
try {
prefs.flush();
} catch (Exception ignored) {
}
}
// ================= JSON Serialization =================
public static String serializeHostsJson(List<SavedHost> hosts) {
StringBuilder sb = new StringBuilder();
sb.append("[\n");
for (int i = 0; i < hosts.size(); i++) {
SavedHost h = hosts.get(i);
sb.append(" ").append(serializeHostJson(h));
if (i < hosts.size() - 1) {
sb.append(",");
}
sb.append("\n");
}
sb.append("]");
return sb.toString();
}
public static String serializeHostJson(SavedHost h) {
StringBuilder sb = new StringBuilder();
sb.append("{");
appendField(sb, "id", h.getId(), true);
appendField(sb, "name", h.getName(), true);
appendField(sb, "host", h.getHost(), true);
appendField(sb, "port", h.getPort(), true);
appendField(sb, "model", h.getModel(), true);
appendField(sb, "dynamicRows", h.getDynamicRows(), true);
appendField(sb, "dynamicCols", h.getDynamicCols(), true);
appendField(sb, "luName", h.getLuName(), true);
appendField(sb, "autoConnect", h.isAutoConnect(), true);
appendField(sb, "hostType", h.getHostType(), true);
appendField(sb, "useTls", h.isUseTls(), true);
appendField(sb, "tlsVerifyCert", h.isTlsVerifyCert(), true);
appendField(sb, "tn3270e", h.isTn3270e(), true);
appendField(sb, "graphicsMode", h.getGraphicsMode(), true);
appendField(sb, "codePage", h.getCodePage(), true);
appendField(sb, "proxyType", h.getProxyType(), true);
appendField(sb, "proxyHost", h.getProxyHost(), true);
appendField(sb, "proxyPort", h.getProxyPort(), true);
appendField(sb, "proxyUsername", h.getProxyUsername(), true);
appendField(sb, "proxyPassword", h.getProxyPassword(), false);
sb.append("}");
return sb.toString();
}
private static void appendField(StringBuilder sb, String key, String val, boolean comma) {
sb.append("\"").append(key).append("\":\"").append(escapeJson(val != null ? val : "")).append("\"");
if (comma) sb.append(",");
}
private static void appendField(StringBuilder sb, String key, int val, boolean comma) {
sb.append("\"").append(key).append("\":").append(val);
if (comma) sb.append(",");
}
private static void appendField(StringBuilder sb, String key, boolean val, boolean comma) {
sb.append("\"").append(key).append("\":").append(val);
if (comma) sb.append(",");
}
private static String escapeJson(String s) {
if (s == null) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 32) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
break;
}
}
return sb.toString();
}
// ================= JSON Deserialization =================
public static List<SavedHost> parseHostsJson(String json) {
List<SavedHost> list = new ArrayList<>();
if (json == null || json.trim().isEmpty()) return list;
SimpleJsonParser parser = new SimpleJsonParser(json);
Object root = parser.parseValue();
if (root instanceof List) {
for (Object item : (List<?>) root) {
if (item instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) item;
list.add(fromMap(map));
}
}
} else if (root instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) root;
list.add(fromMap(map));
}
return list;
}
public static SavedHost fromMap(Map<String, Object> map) {
SavedHost h = new SavedHost();
if (map.containsKey("id")) h.setId(String.valueOf(map.get("id")));
if (map.containsKey("name")) h.setName(String.valueOf(map.get("name")));
if (map.containsKey("host")) h.setHost(String.valueOf(map.get("host")));
if (map.containsKey("port")) h.setPort(parseInt(map.get("port"), 23));
if (map.containsKey("model")) h.setModel(parseInt(map.get("model"), 4));
if (map.containsKey("dynamicRows")) h.setDynamicRows(parseInt(map.get("dynamicRows"), 62));
if (map.containsKey("dynamicCols")) h.setDynamicCols(parseInt(map.get("dynamicCols"), 160));
if (map.containsKey("luName")) h.setLuName(String.valueOf(map.get("luName")));
if (map.containsKey("autoConnect")) h.setAutoConnect(parseBool(map.get("autoConnect"), false));
if (map.containsKey("hostType")) h.setHostType(String.valueOf(map.get("hostType")));
if (map.containsKey("useTls")) h.setUseTls(parseBool(map.get("useTls"), false));
if (map.containsKey("tlsVerifyCert")) h.setTlsVerifyCert(parseBool(map.get("tlsVerifyCert"), true));
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
if (map.containsKey("graphicsMode")) {
String gm = String.valueOf(map.get("graphicsMode"));
h.setGraphicsMode(gm);
}
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
if (map.containsKey("proxyType")) h.setProxyType(String.valueOf(map.get("proxyType")));
if (map.containsKey("proxyHost")) h.setProxyHost(String.valueOf(map.get("proxyHost")));
if (map.containsKey("proxyPort")) h.setProxyPort(parseInt(map.get("proxyPort"), 0));
if (map.containsKey("proxyUsername")) h.setProxyUsername(String.valueOf(map.get("proxyUsername")));
if (map.containsKey("proxyPassword")) h.setProxyPassword(String.valueOf(map.get("proxyPassword")));
return h;
}
private static int parseInt(Object obj, int def) {
if (obj == null) return def;
if (obj instanceof Number) return ((Number) obj).intValue();
try {
return Integer.parseInt(String.valueOf(obj).trim());
} catch (Exception e) {
return def;
}
}
private static boolean parseBool(Object obj, boolean def) {
if (obj == null) return def;
if (obj instanceof Boolean) return (Boolean) obj;
return Boolean.parseBoolean(String.valueOf(obj).trim());
}
/**
* Minimal pure-Java JSON parser for nested Maps, Lists, Strings, Numbers, and Booleans.
*/
static class SimpleJsonParser {
private final String src;
private int pos = 0;
SimpleJsonParser(String src) {
this.src = src;
}
Object parseValue() {
skipWhitespace();
if (pos >= src.length()) return null;
char c = src.charAt(pos);
if (c == '{') return parseObject();
if (c == '[') return parseArray();
if (c == '"') return parseString();
if (c == 't' || c == 'f') return parseBoolean();
if (c == 'n') return parseNull();
if (c == '-' || Character.isDigit(c)) return parseNumber();
throw new IllegalArgumentException("Unexpected char '" + c + "' at position " + pos);
}
private Map<String, Object> parseObject() {
match('{');
Map<String, Object> map = new LinkedHashMap<>();
skipWhitespace();
if (peek() == '}') {
pos++;
return map;
}
while (true) {
skipWhitespace();
String key = parseString();
skipWhitespace();
match(':');
Object val = parseValue();
map.put(key, val);
skipWhitespace();
char c = peek();
if (c == ',') {
pos++;
} else if (c == '}') {
pos++;
break;
} else {
break;
}
}
return map;
}
private List<Object> parseArray() {
match('[');
List<Object> list = new ArrayList<>();
skipWhitespace();
if (peek() == ']') {
pos++;
return list;
}
while (true) {
Object val = parseValue();
list.add(val);
skipWhitespace();
char c = peek();
if (c == ',') {
pos++;
} else if (c == ']') {
pos++;
break;
} else {
break;
}
}
return list;
}
private String parseString() {
match('"');
StringBuilder sb = new StringBuilder();
while (pos < src.length()) {
char c = src.charAt(pos++);
if (c == '"') {
return sb.toString();
} else if (c == '\\') {
if (pos >= src.length()) break;
char esc = src.charAt(pos++);
switch (esc) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case 'u':
if (pos + 4 <= src.length()) {
String hex = src.substring(pos, pos + 4);
pos += 4;
try {
sb.append((char) Integer.parseInt(hex, 16));
} catch (NumberFormatException e) {
sb.append("?");
}
}
break;
default:
sb.append(esc);
break;
}
} else {
sb.append(c);
}
}
return sb.toString();
}
private Boolean parseBoolean() {
if (src.startsWith("true", pos)) {
pos += 4;
return true;
} else if (src.startsWith("false", pos)) {
pos += 5;
return false;
}
throw new IllegalArgumentException("Expected boolean at " + pos);
}
private Object parseNull() {
if (src.startsWith("null", pos)) {
pos += 4;
return null;
}
throw new IllegalArgumentException("Expected null at " + pos);
}
private Number parseNumber() {
int start = pos;
if (src.charAt(pos) == '-') pos++;
while (pos < src.length() && (Character.isDigit(src.charAt(pos)) || src.charAt(pos) == '.' || src.charAt(pos) == 'e' || src.charAt(pos) == 'E' || src.charAt(pos) == '+' || src.charAt(pos) == '-')) {
pos++;
}
String numStr = src.substring(start, pos);
if (numStr.contains(".")) {
return Double.parseDouble(numStr);
}
return Long.parseLong(numStr);
}
private void skipWhitespace() {
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
pos++;
}
}
private char peek() {
return pos < src.length() ? src.charAt(pos) : '\0';
}
private void match(char expected) {
skipWhitespace();
if (pos >= src.length() || src.charAt(pos) != expected) {
throw new IllegalArgumentException("Expected '" + expected + "' at " + pos);
}
pos++;
}
}
}
@@ -0,0 +1,222 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import java.util.Objects;
import java.util.UUID;
/**
* Data model for a saved 3270 connection profile, matching the schema of a3270 SavedHost.
* Includes per-connection host, model, protocol, and proxy configuration.
*/
public class SavedHost {
private String id;
private String name;
private String host;
private int port;
private int model;
private int dynamicRows;
private int dynamicCols;
private String luName;
private boolean autoConnect;
private String hostType;
private boolean useTls;
private boolean tlsVerifyCert;
private boolean tn3270e;
private String graphicsMode;
private String codePage;
// Per-connection proxy configuration
private String proxyType;
private String proxyHost;
private int proxyPort;
private String proxyUsername;
private String proxyPassword;
public SavedHost() {
this.id = UUID.randomUUID().toString();
this.name = "Mainframe";
this.host = "127.0.0.1";
this.port = 23;
this.model = 4;
this.dynamicRows = 62;
this.dynamicCols = 160;
this.luName = "";
this.autoConnect = false;
this.hostType = "TSO";
this.useTls = false;
this.tlsVerifyCert = true;
this.tn3270e = true;
this.graphicsMode = "BOTH";
this.codePage = "037";
this.proxyType = "NONE";
this.proxyHost = "";
this.proxyPort = 0;
this.proxyUsername = "";
this.proxyPassword = "";
}
public SavedHost(String id, String name, String host, int port, int model,
int dynamicRows, int dynamicCols, String luName, boolean autoConnect,
String hostType, boolean useTls, boolean tlsVerifyCert, boolean tn3270e,
String graphicsMode, String codePage, String proxyType, String proxyHost,
int proxyPort, String proxyUsername, String proxyPassword) {
this.id = (id != null && !id.trim().isEmpty()) ? id : UUID.randomUUID().toString();
this.name = name != null ? name : "";
this.host = host != null ? host : "127.0.0.1";
this.port = port > 0 ? port : 23;
this.model = model;
this.dynamicRows = dynamicRows > 0 ? dynamicRows : 62;
this.dynamicCols = dynamicCols > 0 ? dynamicCols : 160;
this.luName = luName != null ? luName : "";
this.autoConnect = autoConnect;
this.hostType = hostType != null ? hostType : "TSO";
this.useTls = useTls;
this.tlsVerifyCert = tlsVerifyCert;
this.tn3270e = tn3270e;
this.graphicsMode = graphicsMode != null ? graphicsMode : "BOTH";
this.codePage = codePage != null ? codePage : "037";
this.proxyType = (proxyType != null && !proxyType.trim().isEmpty()) ? proxyType.toUpperCase() : "NONE";
this.proxyHost = proxyHost != null ? proxyHost : "";
this.proxyPort = proxyPort;
this.proxyUsername = proxyUsername != null ? proxyUsername : "";
this.proxyPassword = proxyPassword != null ? proxyPassword : "";
}
public SavedHost copy() {
return new SavedHost(id, name, host, port, model, dynamicRows, dynamicCols, luName,
autoConnect, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, codePage,
proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword);
}
/**
* Converts this saved host profile into an active ConnectionConfig.
*/
public ConnectionConfig toConnectionConfig() {
TerminalModel tm;
if (model == 0) {
tm = TerminalModel.IBM_DYNAMIC;
} else {
try {
tm = TerminalModel.forModel(model, true);
} catch (Exception e) {
tm = TerminalModel.IBM_3279_4;
}
}
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
if (model == 0 || tm.isDynamic()) {
config.setDynamicDimensions(dynamicRows, dynamicCols);
}
if (luName != null && !luName.trim().isEmpty()) {
config.setLuName(luName.trim());
}
config.setTlsVerifyCert(tlsVerifyCert);
config.setTn3270eEnabled(tn3270e);
if (graphicsMode != null && !graphicsMode.isEmpty()) {
config.setGraphicsMode(GraphicsMode.fromString(graphicsMode));
}
if (codePage != null && !codePage.isEmpty()) {
config.setCodePage(codePage);
}
// Apply per-connection proxy if configured
if (proxyType != null && !proxyType.equalsIgnoreCase("NONE") && proxyHost != null && !proxyHost.trim().isEmpty()) {
try {
ConnectionConfig.ProxyType pt = ConnectionConfig.ProxyType.valueOf(proxyType.toUpperCase());
int pPort = proxyPort > 0 ? proxyPort : (pt == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080);
config.setProxy(pt, proxyHost.trim(), pPort,
(proxyUsername != null && !proxyUsername.trim().isEmpty()) ? proxyUsername.trim() : null,
(proxyPassword != null && !proxyPassword.isEmpty()) ? proxyPassword : null);
} catch (IllegalArgumentException ignored) {
}
}
return config;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
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 int getModel() { return model; }
public void setModel(int model) { this.model = model; }
public int getDynamicRows() { return dynamicRows; }
public void setDynamicRows(int dynamicRows) { this.dynamicRows = dynamicRows; }
public int getDynamicCols() { return dynamicCols; }
public void setDynamicCols(int dynamicCols) { this.dynamicCols = dynamicCols; }
public String getLuName() { return luName; }
public void setLuName(String luName) { this.luName = luName; }
public boolean isAutoConnect() { return autoConnect; }
public void setAutoConnect(boolean autoConnect) { this.autoConnect = autoConnect; }
public String getHostType() { return hostType; }
public void setHostType(String hostType) { this.hostType = hostType; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
public boolean isTn3270e() { return tn3270e; }
public void setTn3270e(boolean tn3270e) { this.tn3270e = tn3270e; }
public String getGraphicsMode() { return graphicsMode; }
public void setGraphicsMode(String graphicsMode) { this.graphicsMode = graphicsMode; }
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) { this.codePage = codePage; }
public String getProxyType() { return proxyType != null ? proxyType : "NONE"; }
public void setProxyType(String proxyType) { this.proxyType = (proxyType != null) ? proxyType.toUpperCase() : "NONE"; }
public String getProxyHost() { return proxyHost != null ? 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 != null ? proxyUsername : ""; }
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
public String getProxyPassword() { return proxyPassword != null ? proxyPassword : ""; }
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
@Override
public String toString() {
if (name != null && !name.trim().isEmpty()) {
return name;
}
return host + (port > 0 ? ":" + port : "");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SavedHost savedHost = (SavedHost) o;
return Objects.equals(id, savedHost.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -0,0 +1,138 @@
package haus.nightmare.j3270.ui;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility for loading and applying application window and taskbar icons.
* Loads multi-resolution lib3270j icons from resources.
*/
public final class AppIcon {
private static final Logger log = Logger.getLogger(AppIcon.class.getName());
private static final int[] ICON_SIZES = {16, 24, 32, 48, 64, 128, 256, 512};
private static List<Image> cachedIcons = null;
private static Image defaultIcon = null;
private AppIcon() {}
/**
* Retrieves the list of available application icon images in various resolutions.
*
* @return an unmodifiable list of Image objects, or empty list if none could be loaded.
*/
public static synchronized List<Image> getAppIcons() {
if (cachedIcons == null) {
loadIcons();
}
return cachedIcons;
}
/**
* Retrieves the default application icon image (256x256 or best available).
*
* @return the default Image, or null if no icon is available.
*/
public static synchronized Image getDefaultIcon() {
if (cachedIcons == null) {
loadIcons();
}
return defaultIcon;
}
/**
* Applies the application icons to a target Window (JFrame, JDialog, etc.)
* and updates the desktop Taskbar icon if supported.
*
* @param window the Window to set icon images for.
*/
public static void applyTo(Window window) {
if (GraphicsEnvironment.isHeadless()) {
return;
}
List<Image> icons = getAppIcons();
if (icons.isEmpty()) {
return;
}
if (window != null) {
window.setIconImages(icons);
}
// Apply to OS Taskbar / Dock if supported (Java 9+)
try {
if (Taskbar.isTaskbarSupported()) {
Taskbar taskbar = Taskbar.getTaskbar();
if (taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) {
Image bestIcon = defaultIcon != null ? defaultIcon : icons.get(icons.size() - 1);
taskbar.setIconImage(bestIcon);
}
}
} catch (Throwable t) {
log.log(Level.FINE, "Taskbar icon could not be set: {0}", t.getMessage());
}
}
private static void loadIcons() {
List<Image> icons = new ArrayList<>();
for (int size : ICON_SIZES) {
String path = "/icons/lib3270j_" + size + ".png";
Image img = loadImage(path);
if (img != null) {
icons.add(img);
}
}
if (icons.isEmpty()) {
Image img = loadImage("/icons/lib3270j.png");
if (img != null) {
icons.add(img);
}
}
if (!icons.isEmpty()) {
for (Image img : icons) {
if (img.getWidth(null) == 256) {
defaultIcon = img;
break;
}
}
if (defaultIcon == null) {
defaultIcon = icons.get(icons.size() - 1);
}
}
cachedIcons = Collections.unmodifiableList(icons);
}
private static Image loadImage(String resourcePath) {
try {
URL url = AppIcon.class.getResource(resourcePath);
if (url != null) {
BufferedImage img = ImageIO.read(url);
if (img != null) {
return img;
}
}
try (InputStream in = AppIcon.class.getResourceAsStream(resourcePath)) {
if (in != null) {
return ImageIO.read(in);
}
}
} catch (Exception e) {
log.log(Level.FINE, "Failed to load icon from " + resourcePath, e);
}
return null;
}
}
@@ -0,0 +1,88 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
import haus.nightmare.lib3270j.graphics.PixelBuffer;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
/**
* Desktop Swing/AWT bridge for converting between lib3270j PixelBuffer and java.awt BufferedImage.
*/
public final class AwtPixelBufferBridge {
private AwtPixelBufferBridge() {}
/**
* Converts a PixelBuffer to a BufferedImage.
* Uses TYPE_INT_ARGB with backed raster for optimal Java2D hardware acceleration.
*/
public static BufferedImage toBufferedImage(PixelBuffer buffer) {
if (buffer == null) return null;
int w = buffer.getWidth();
int h = buffer.getHeight();
if (w <= 0 || h <= 0) {
return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
}
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
copyIntoBufferedImage(buffer, img);
return img;
}
/**
* Copies pixels from a PixelBuffer directly into an existing BufferedImage.
*/
public static void copyIntoBufferedImage(PixelBuffer buffer, BufferedImage target) {
if (buffer == null || target == null) return;
int w = Math.min(buffer.getWidth(), target.getWidth());
int h = Math.min(buffer.getHeight(), target.getHeight());
if (w <= 0 || h <= 0) return;
int[] srcPixels = buffer.getPixels();
if (srcPixels == null) return;
if (target.getType() == BufferedImage.TYPE_INT_ARGB || target.getType() == BufferedImage.TYPE_INT_RGB) {
if (target.getRaster().getDataBuffer() instanceof DataBufferInt) {
int[] dstPixels = ((DataBufferInt) target.getRaster().getDataBuffer()).getData();
int srcW = buffer.getWidth();
int dstW = target.getWidth();
if (srcW == dstW && srcW == w && srcPixels.length >= w * h && dstPixels.length >= w * h) {
System.arraycopy(srcPixels, 0, dstPixels, 0, w * h);
return;
}
for (int y = 0; y < h; y++) {
System.arraycopy(srcPixels, y * srcW, dstPixels, y * dstW, w);
}
return;
}
}
// Fallback for custom or incompatible image formats
target.setRGB(0, 0, w, h, srcPixels, 0, buffer.getWidth());
}
/**
* Wraps or converts a java.awt BufferedImage into a PixelBuffer.
*/
public static PixelBuffer toPixelBuffer(BufferedImage img) {
if (img == null) return null;
int w = img.getWidth();
int h = img.getHeight();
int[] pixels;
if ((img.getType() == BufferedImage.TYPE_INT_ARGB || img.getType() == BufferedImage.TYPE_INT_RGB)
&& img.getRaster().getDataBuffer() instanceof DataBufferInt) {
pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
} else {
pixels = new int[w * h];
img.getRGB(0, 0, w, h, pixels, 0, w);
}
return new DefaultPixelBuffer(w, h, pixels);
}
}
@@ -1,118 +1,261 @@
package haus.nightmare.j3270.ui; package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.lib3270j.ConnectionConfig; import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.TerminalModel;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*; import java.awt.*;
import java.util.List;
/** /**
* Connection dialog for entering host, port, model, and LU name. * Connection dialog for entering host, port, model, LU name, and per-connection proxy configuration.
* Supports loading and saving host profiles matching the a3270 schema.
*/ */
public class ConnectDialog extends JDialog { public class ConnectDialog extends JDialog {
private static final String NEW_PROFILE_LABEL = "<New / Custom Connection>";
// Profile selector
private JComboBox<Object> profileCombo;
private JButton saveProfileBtn;
private JButton deleteProfileBtn;
private String selectedProfileId = null;
// Connection parameters
private JTextField hostField; private JTextField hostField;
private JTextField portField; private JTextField portField;
private JComboBox<TerminalModel> modelCombo; private JComboBox<TerminalModel> modelCombo;
private JLabel dynamicDimLabel;
private JPanel dynamicDimPanel;
private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner;
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo; private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JComboBox<String> codePageCombo; private JComboBox<String> codePageCombo;
private JTextField luField; private JTextField luField;
private JCheckBox tlsCheckBox; private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox; private JCheckBox verifyCertCheckBox;
private JCheckBox tn3270eCheckBox; private JCheckBox tn3270eCheckBox;
private JCheckBox keepAliveCheckBox;
private JCheckBox autoReconnectCheckBox;
// Per-connection proxy configuration
private JComboBox<ConnectionConfig.ProxyType> proxyTypeCombo;
private JTextField proxyHostField;
private JTextField proxyPortField;
private JTextField proxyUserField;
private JPasswordField proxyPassField;
private boolean confirmed; private boolean confirmed;
private ConnectionConfig result; private ConnectionConfig result;
public ConnectDialog(Frame parent) { public ConnectDialog(Frame parent) {
super(parent, "Connect to Host", true); super(parent, "Connect to Host", true);
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
setResizable(false); setResizable(true);
} }
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout()); JPanel contentPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
GridBagConstraints gbc = new GridBagConstraints(); GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 6, 5, 6); gbc.insets = new Insets(4, 5, 4, 5);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Host int gridy = 0;
// 0. Profile Row
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = gridy;
gbc.weightx = 0;
JLabel profileLabel = new JLabel("Profile:");
profileLabel.setFont(labelFont);
contentPanel.add(profileLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
JPanel profileBar = new JPanel(new BorderLayout(6, 0));
profileBar.setOpaque(false);
profileCombo = new JComboBox<>();
profileCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(profileCombo);
refreshProfileCombo();
profileCombo.addActionListener(e -> onProfileSelectionChanged());
profileBar.add(profileCombo, BorderLayout.CENTER);
JPanel profileBtnBar = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
profileBtnBar.setOpaque(false);
saveProfileBtn = new JButton("Save Profile...");
ThemeManager.styleButton(saveProfileBtn, ThemeManager.ButtonVariant.DEFAULT);
saveProfileBtn.setFont(labelFont);
saveProfileBtn.addActionListener(e -> onSaveProfile());
deleteProfileBtn = new JButton("Delete");
ThemeManager.styleButton(deleteProfileBtn, ThemeManager.ButtonVariant.CANCEL);
deleteProfileBtn.setFont(labelFont);
deleteProfileBtn.setEnabled(false);
deleteProfileBtn.addActionListener(e -> onDeleteProfile());
profileBtnBar.add(saveProfileBtn);
profileBtnBar.add(deleteProfileBtn);
profileBar.add(profileBtnBar, BorderLayout.EAST);
contentPanel.add(profileBar, gbc);
gridy++;
// 1. Host
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel hostLabel = new JLabel("Host:"); JLabel hostLabel = new JLabel("Host:");
hostLabel.setFont(labelFont); hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc); contentPanel.add(hostLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
hostField = createField(20); hostField = createField(20);
mainPanel.add(hostField, gbc); contentPanel.add(hostField, gbc);
// Port gridy++;
// 2. Port
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 1; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:"); JLabel portLabel = new JLabel("Port:");
portLabel.setFont(labelFont); portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc); contentPanel.add(portLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
portField = createField(6); portField = createField(6);
portField.setText("23"); portField.setText("23");
mainPanel.add(portField, gbc); contentPanel.add(portField, gbc);
// Model gridy++;
// 3. Model
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 2; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:"); JLabel modelLabel = new JLabel("Model:");
modelLabel.setFont(labelFont); modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc); contentPanel.add(modelLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values()); modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4); modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); modelCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(modelCombo); ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc); contentPanel.add(modelCombo, gbc);
// LU Name gridy++;
// 4. Dynamic Dimensions
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 3; gbc.gridy = gridy;
gbc.weightx = 0;
dynamicDimLabel = new JLabel("Screen Size:");
dynamicDimLabel.setFont(labelFont);
contentPanel.add(dynamicDimLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
dynamicDimPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
dynamicDimPanel.setOpaque(false);
JLabel rowsLabel = new JLabel("Rows:");
rowsLabel.setFont(labelFont);
dynamicRowsSpinner = new JSpinner(new SpinnerNumberModel(haus.nightmare.j3270.config.Settings.getDynamicRows(), 24, 255, 1));
ThemeManager.styleSpinner(dynamicRowsSpinner);
JLabel colsLabel = new JLabel("Cols:");
colsLabel.setFont(labelFont);
dynamicColsSpinner = new JSpinner(new SpinnerNumberModel(haus.nightmare.j3270.config.Settings.getDynamicCols(), 80, 255, 1));
ThemeManager.styleSpinner(dynamicColsSpinner);
dynamicDimPanel.add(rowsLabel);
dynamicDimPanel.add(dynamicRowsSpinner);
dynamicDimPanel.add(colsLabel);
dynamicDimPanel.add(dynamicColsSpinner);
contentPanel.add(dynamicDimPanel, gbc);
Runnable updateDynamicVisibility = () -> {
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
boolean isDyn = (m != null && m.isDynamic());
dynamicDimLabel.setVisible(isDyn);
dynamicDimPanel.setVisible(isDyn);
pack();
};
modelCombo.addActionListener(e -> updateDynamicVisibility.run());
updateDynamicVisibility.run();
gridy++;
// 5. LU Name
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:"); JLabel luLabel = new JLabel("LU Name:");
luLabel.setFont(labelFont); luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc); contentPanel.add(luLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
luField = createField(12); luField = createField(12);
mainPanel.add(luField, gbc); contentPanel.add(luField, gbc);
// Graphics Mode gridy++;
// 6. Graphics Mode
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 4; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:"); JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setFont(labelFont); graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc); contentPanel.add(graphicsLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values()); graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode()); graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); graphicsCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(graphicsCombo); ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc); graphicsCombo.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 (value instanceof haus.nightmare.lib3270j.graphics.GraphicsMode) {
setText(((haus.nightmare.lib3270j.graphics.GraphicsMode) value).getDescription());
}
if (isSelected) {
setBackground(ThemeManager.getSelectionBg());
setForeground(ThemeManager.getSelectionFg());
} else {
setBackground(ThemeManager.getBgComponent());
setForeground(ThemeManager.getFgMain());
}
return this;
}
});
contentPanel.add(graphicsCombo, gbc);
// Code Page gridy++;
// 7. Code Page
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 5; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:"); JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setFont(labelFont); cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc); contentPanel.add(cpLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
String[] commonCodePages = { String[] commonCodePages = {
@@ -133,43 +276,44 @@ public class ConnectDialog extends JDialog {
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)", "1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)", "905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual", "420 - Arabic Bilingual",
"424 - Hebrew (with Lowercase)", "424 - Hebrew (with Latin)",
"803 - Hebrew Old / Standard", "803 - Hebrew Character Set",
"838 - Thai", "838 - Thai Extended",
"1160 - Thai (Euro \u20AC)", "1160 - Thai Euro \u20AC",
"1025 - Cyrillic Multilingual", "1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine", "1123 - Cyrillic Ukraine",
"1154 - Cyrillic Multilingual (Euro \u20AC)", "1154 - Cyrillic Euro \u20AC",
"880 - Cyrillic Russian", "880 - Cyrillic (Russian)",
"1140 - US / Canada (Euro \u20AC)", "1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)", "1141 - Germany / Austria (Euro \u20AC)",
"1142 - Denmark / Norway (Euro \u20AC)",
"1143 - Sweden / Finland (Euro \u20AC)",
"1144 - Italy (Euro \u20AC)",
"1145 - Spain / Latin America (Euro \u20AC)",
"1146 - United Kingdom (Euro \u20AC)",
"1147 - France (Euro \u20AC)",
"1148 - International (Euro \u20AC)", "1148 - International (Euro \u20AC)",
"1149 - Iceland (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS", "930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS", "939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS", "935 - Simplified Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS", "937 - Traditional Chinese Mixed DBCS",
"1371 - Traditional Chinese Extended Mixed DBCS", "1388 - Simplified Chinese Extended DBCS",
"1371 - Traditional Chinese Extended DBCS",
"933 - Korean Mixed DBCS" "933 - Korean Mixed DBCS"
}; };
codePageCombo = new JComboBox<>(commonCodePages); codePageCombo = new JComboBox<>(commonCodePages);
codePageCombo.setEditable(true); codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
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); ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc); setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
contentPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox gridy++;
// 8. TLS Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 6; gbc.gridy = gridy;
gbc.weightx = 1.0; tlsCheckBox = new JCheckBox("Use TLS / SSL Encryption");
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
ThemeManager.styleCheckBox(tlsCheckBox); ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setFont(labelFont); tlsCheckBox.setFont(labelFont);
tlsCheckBox.addActionListener(e -> { tlsCheckBox.addActionListener(e -> {
@@ -182,28 +326,169 @@ public class ConnectDialog extends JDialog {
portField.setText("23"); portField.setText("23");
} }
}); });
mainPanel.add(tlsCheckBox, gbc); contentPanel.add(tlsCheckBox, gbc);
// Verify Certificate Checkbox gridy++;
// 9. Verify Cert Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 7; gbc.gridy = gridy;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
ThemeManager.styleCheckBox(verifyCertCheckBox); ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); verifyCertCheckBox.setFont(ThemeManager.getUiFont());
verifyCertCheckBox.setSelected(true); verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false); verifyCertCheckBox.setEnabled(false);
mainPanel.add(verifyCertCheckBox, gbc); contentPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox gridy++;
// 10. TN3270E Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 8; gbc.gridy = gridy;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
ThemeManager.styleCheckBox(tn3270eCheckBox); ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); tn3270eCheckBox.setFont(ThemeManager.getUiFont());
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e()); tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
mainPanel.add(tn3270eCheckBox, gbc); contentPanel.add(tn3270eCheckBox, gbc);
gridy++;
// 11. Keep-Alive Checkbox
gbc.gridx = 1;
gbc.gridy = gridy;
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
ThemeManager.styleCheckBox(keepAliveCheckBox);
keepAliveCheckBox.setFont(ThemeManager.getUiFont());
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
contentPanel.add(keepAliveCheckBox, gbc);
gridy++;
// 12. Auto-Reconnect Checkbox
gbc.gridx = 1;
gbc.gridy = gridy;
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
ThemeManager.styleCheckBox(autoReconnectCheckBox);
autoReconnectCheckBox.setFont(ThemeManager.getUiFont());
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
contentPanel.add(autoReconnectCheckBox, gbc);
gridy++;
// 13. PER-CONNECTION PROXY CONFIGURATION PANEL
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
JPanel proxyGroup = new JPanel(new GridBagLayout());
proxyGroup.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(),
"Proxy Configuration (This Connection)",
TitledBorder.LEFT,
TitledBorder.TOP,
labelFont
));
GridBagConstraints pgbc = new GridBagConstraints();
pgbc.insets = new Insets(3, 6, 3, 6);
pgbc.fill = GridBagConstraints.HORIZONTAL;
// Proxy Type
pgbc.gridx = 0;
pgbc.gridy = 0;
pgbc.weightx = 0;
JLabel ptLabel = new JLabel("Proxy Type:");
ptLabel.setFont(labelFont);
proxyGroup.add(ptLabel, pgbc);
pgbc.gridx = 1;
pgbc.gridwidth = 3;
pgbc.weightx = 1.0;
proxyTypeCombo = new JComboBox<>(ConnectionConfig.ProxyType.values());
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
proxyTypeCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(proxyTypeCombo);
proxyGroup.add(proxyTypeCombo, pgbc);
// Proxy Host & Port
pgbc.gridx = 0;
pgbc.gridy = 1;
pgbc.gridwidth = 1;
pgbc.weightx = 0;
JLabel phLabel = new JLabel("Proxy Host:");
phLabel.setFont(labelFont);
proxyGroup.add(phLabel, pgbc);
pgbc.gridx = 1;
pgbc.weightx = 1.0;
proxyHostField = createField(14);
proxyGroup.add(proxyHostField, pgbc);
pgbc.gridx = 2;
pgbc.weightx = 0;
JLabel ppLabel = new JLabel("Port:");
ppLabel.setFont(labelFont);
proxyGroup.add(ppLabel, pgbc);
pgbc.gridx = 3;
pgbc.weightx = 0.4;
proxyPortField = createField(5);
proxyGroup.add(proxyPortField, pgbc);
// Proxy User & Password
pgbc.gridx = 0;
pgbc.gridy = 2;
pgbc.gridwidth = 1;
pgbc.weightx = 0;
JLabel puLabel = new JLabel("Username:");
puLabel.setFont(labelFont);
proxyGroup.add(puLabel, pgbc);
pgbc.gridx = 1;
pgbc.weightx = 1.0;
proxyUserField = createField(12);
proxyGroup.add(proxyUserField, pgbc);
pgbc.gridx = 2;
pgbc.weightx = 0;
JLabel pwLabel = new JLabel("Password:");
pwLabel.setFont(labelFont);
proxyGroup.add(pwLabel, pgbc);
pgbc.gridx = 3;
pgbc.weightx = 0.4;
proxyPassField = createPasswordField(10);
proxyGroup.add(proxyPassField, pgbc);
// Dynamic enable/disable based on Proxy Type
Runnable updateProxyFields = () -> {
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
boolean active = (pt != null && pt != ConnectionConfig.ProxyType.NONE);
proxyHostField.setEnabled(active);
proxyPortField.setEnabled(active);
proxyUserField.setEnabled(active);
proxyPassField.setEnabled(active && pt != ConnectionConfig.ProxyType.SOCKS4);
if (active) {
String currentPPort = proxyPortField.getText().trim();
if (currentPPort.isEmpty() || "0".equals(currentPPort)) {
proxyPortField.setText(pt == ConnectionConfig.ProxyType.HTTP ? "8080" : "1080");
}
}
};
proxyTypeCombo.addActionListener(e -> updateProxyFields.run());
updateProxyFields.run();
contentPanel.add(proxyGroup, gbc);
gridy++;
// 14. Action Buttons
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
JButton connectBtn = new JButton("Connect"); JButton connectBtn = new JButton("Connect");
@@ -221,13 +506,14 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(cancelBtn); buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn); buttonPanel.add(connectBtn);
contentPanel.add(buttonPanel, gbc);
gbc.gridx = 0; JScrollPane scrollPane = new JScrollPane(contentPanel);
gbc.gridy = 9; scrollPane.setBorder(BorderFactory.createEmptyBorder());
gbc.gridwidth = 2; scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
mainPanel.add(buttonPanel, gbc); scrollPane.getVerticalScrollBar().setUnitIncrement(14);
setContentPane(scrollPane);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this); ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect // Enter key triggers connect
@@ -236,11 +522,194 @@ public class ConnectDialog extends JDialog {
private JTextField createField(int cols) { private JTextField createField(int cols) {
JTextField field = new JTextField(cols); JTextField field = new JTextField(cols);
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field); ThemeManager.styleTextField(field);
return field; return field;
} }
private JPasswordField createPasswordField(int cols) {
JPasswordField field = new JPasswordField(cols);
field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field);
return field;
}
// ================= Profile Management =================
private void refreshProfileCombo() {
profileCombo.removeAllItems();
profileCombo.addItem(NEW_PROFILE_LABEL);
List<SavedHost> saved = HostStorage.getSavedHosts();
for (SavedHost h : saved) {
profileCombo.addItem(h);
}
if (selectedProfileId != null) {
for (int i = 0; i < profileCombo.getItemCount(); i++) {
Object item = profileCombo.getItemAt(i);
if (item instanceof SavedHost && ((SavedHost) item).getId().equals(selectedProfileId)) {
profileCombo.setSelectedIndex(i);
return;
}
}
}
profileCombo.setSelectedIndex(0);
}
private void onProfileSelectionChanged() {
Object sel = profileCombo.getSelectedItem();
if (sel instanceof SavedHost) {
SavedHost h = (SavedHost) sel;
selectedProfileId = h.getId();
deleteProfileBtn.setEnabled(true);
loadProfileFields(h);
} else {
selectedProfileId = null;
deleteProfileBtn.setEnabled(false);
}
}
public void loadProfile(SavedHost h) {
if (h == null) return;
selectedProfileId = h.getId();
refreshProfileCombo();
loadProfileFields(h);
}
private void loadProfileFields(SavedHost h) {
if (h == null) return;
hostField.setText(h.getHost());
portField.setText(String.valueOf(h.getPort()));
if (h.getModel() == 0) {
modelCombo.setSelectedItem(TerminalModel.IBM_DYNAMIC);
} else {
try {
modelCombo.setSelectedItem(TerminalModel.forModel(h.getModel(), true));
} catch (Exception ignored) {
}
}
dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62);
dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160);
luField.setText(h.getLuName() != null ? h.getLuName() : "");
tlsCheckBox.setSelected(h.isUseTls());
verifyCertCheckBox.setEnabled(h.isUseTls());
verifyCertCheckBox.setSelected(h.isTlsVerifyCert());
tn3270eCheckBox.setSelected(h.isTn3270e());
setInitialCodePage(h.getCodePage());
if (h.getGraphicsMode() != null) {
graphicsCombo.setSelectedItem(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(h.getGraphicsMode()));
}
// Per-connection proxy parameters
String ptStr = h.getProxyType();
try {
ConnectionConfig.ProxyType pt = (ptStr != null && !ptStr.trim().isEmpty())
? ConnectionConfig.ProxyType.valueOf(ptStr.toUpperCase())
: ConnectionConfig.ProxyType.NONE;
proxyTypeCombo.setSelectedItem(pt);
} catch (IllegalArgumentException e) {
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
}
proxyHostField.setText(h.getProxyHost() != null ? h.getProxyHost() : "");
proxyPortField.setText(h.getProxyPort() > 0 ? String.valueOf(h.getProxyPort()) : "");
proxyUserField.setText(h.getProxyUsername() != null ? h.getProxyUsername() : "");
proxyPassField.setText(h.getProxyPassword() != null ? h.getProxyPassword() : "");
}
private void onSaveProfile() {
String hStr = hostField.getText().trim();
if (hStr.isEmpty()) {
hostField.requestFocus();
JOptionPane.showMessageDialog(this, "Please specify a host before saving profile.", "Missing Host", JOptionPane.WARNING_MESSAGE);
return;
}
String defaultName = hStr;
Object currentSel = profileCombo.getSelectedItem();
if (currentSel instanceof SavedHost) {
defaultName = ((SavedHost) currentSel).getName();
}
String profileName = (String) JOptionPane.showInputDialog(
this,
"Enter Host Profile Name:",
"Save Profile",
JOptionPane.PLAIN_MESSAGE,
null,
null,
defaultName
);
if (profileName == null || profileName.trim().isEmpty()) {
return;
}
profileName = profileName.trim();
SavedHost hostToSave = buildCurrentSavedHost(profileName);
HostStorage.saveHost(hostToSave);
selectedProfileId = hostToSave.getId();
refreshProfileCombo();
}
private void onDeleteProfile() {
if (selectedProfileId == null) return;
int opt = JOptionPane.showConfirmDialog(this,
"Are you sure you want to delete this host profile?",
"Delete Profile",
JOptionPane.YES_NO_OPTION);
if (opt == JOptionPane.YES_OPTION) {
HostStorage.deleteHost(selectedProfileId);
selectedProfileId = null;
refreshProfileCombo();
}
}
private SavedHost buildCurrentSavedHost(String profileName) {
String id = selectedProfileId != null ? selectedProfileId : java.util.UUID.randomUUID().toString();
String h = hostField.getText().trim();
int port = 23;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException ignored) {}
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
int mNum = (m != null) ? m.getModelNumber() : 4;
int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue();
String lu = luField.getText().trim();
boolean tls = tlsCheckBox.isSelected();
boolean verify = verifyCertCheckBox.isSelected();
boolean tn3270e = tn3270eCheckBox.isSelected();
String gfx = "BOTH";
if (graphicsCombo.getSelectedItem() != null) {
gfx = ((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()).name();
}
String cp = "037";
Object cpSel = codePageCombo.getSelectedItem();
if (cpSel != null) {
String s = cpSel.toString().trim();
int dash = s.indexOf(" -");
cp = (dash > 0) ? s.substring(0, dash).trim() : s;
}
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
String ptStr = (pt != null) ? pt.name() : "NONE";
String pHost = proxyHostField.getText().trim();
int pPort = 0;
try {
pPort = Integer.parseInt(proxyPortField.getText().trim());
} catch (NumberFormatException ignored) {}
String pUser = proxyUserField.getText().trim();
String pPass = new String(proxyPassField.getPassword());
return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false,
"TSO", tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
}
// ================= Connect Action =================
private void onConnect() { private void onConnect() {
String host = hostField.getText().trim(); String host = hostField.getText().trim();
if (host.isEmpty()) { if (host.isEmpty()) {
@@ -256,12 +725,35 @@ public class ConnectDialog extends JDialog {
return; return;
} }
result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem()); TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
if (selectedModel == null) {
selectedModel = TerminalModel.IBM_3279_4;
}
// Support full host string parsing if the user pasted flags or prefixes into host field
if (host.contains(" ") || host.startsWith("L:") || host.startsWith("P:") || host.startsWith("--proxy=") || host.startsWith("-proxy=")) {
result = ConnectionConfig.parseHostString(host, port, selectedModel);
} else {
result = new ConnectionConfig(host, port, selectedModel);
}
if (selectedModel.isDynamic()) {
int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue();
result.setDynamicDimensions(dRows, dCols);
haus.nightmare.j3270.config.Settings.setDynamicRows(dRows);
haus.nightmare.j3270.config.Settings.setDynamicCols(dCols);
}
String lu = luField.getText().trim(); String lu = luField.getText().trim();
if (!lu.isEmpty()) { if (!lu.isEmpty()) {
result.setLuName(lu); result.setLuName(lu);
} }
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()); if (graphicsCombo.getSelectedItem() != null) {
haus.nightmare.lib3270j.graphics.GraphicsMode gm = (haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem();
result.setGraphicsMode(gm);
haus.nightmare.j3270.config.Settings.setGraphicsMode(gm);
}
Object cpSelection = codePageCombo.getSelectedItem(); Object cpSelection = codePageCombo.getSelectedItem();
if (cpSelection != null) { if (cpSelection != null) {
@@ -277,6 +769,42 @@ public class ConnectDialog extends JDialog {
result.setUseTls(tlsCheckBox.isSelected()); result.setUseTls(tlsCheckBox.isSelected());
result.setTlsVerifyCert(verifyCertCheckBox.isSelected()); result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
result.setTn3270eEnabled(tn3270eCheckBox.isSelected()); result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
result.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
boolean ka = keepAliveCheckBox.isSelected();
result.setKeepAliveEnabled(ka);
result.setKeepAliveIntervalSeconds(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAliveInterval());
haus.nightmare.j3270.config.Settings.setAutoConnectKeepAlive(ka);
boolean ar = autoReconnectCheckBox.isSelected();
result.setAutoReconnect(ar);
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
// Apply per-connection proxy parameters strictly to this connection
ConnectionConfig.ProxyType pType = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
String pHost = proxyHostField.getText().trim();
if (pType != null && pType != ConnectionConfig.ProxyType.NONE && !pHost.isEmpty()) {
int pPort = 0;
try {
pPort = Integer.parseInt(proxyPortField.getText().trim());
} catch (NumberFormatException ignored) {}
if (pPort <= 0) {
pPort = (pType == ConnectionConfig.ProxyType.HTTP) ? 8080 : 1080;
}
String pUser = proxyUserField.getText().trim();
String pPass = new String(proxyPassField.getPassword());
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
}
if (selectedProfileId != null) {
SavedHost existing = HostStorage.findById(selectedProfileId);
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
? existing.getName() : host;
SavedHost updated = buildCurrentSavedHost(pName);
HostStorage.saveHost(updated);
}
confirmed = true; confirmed = true;
dispose(); dispose();
} }
@@ -311,6 +839,12 @@ public class ConnectDialog extends JDialog {
tn3270eCheckBox.setSelected(tn3270e); tn3270eCheckBox.setSelected(tn3270e);
} }
public void setInitialGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
if (mode != null && selectedProfileId == null && graphicsCombo != null) {
graphicsCombo.setSelectedItem(mode);
}
}
public void setInitialCodePage(String cp) { public void setInitialCodePage(String cp) {
if (cp != null && codePageCombo != null) { if (cp != null && codePageCombo != null) {
for (int i = 0; i < codePageCombo.getItemCount(); i++) { for (int i = 0; i < codePageCombo.getItemCount(); i++) {
@@ -323,4 +857,49 @@ public class ConnectDialog extends JDialog {
codePageCombo.setSelectedItem(cp); codePageCombo.setSelectedItem(cp);
} }
} }
/**
* Set initial proxy settings for this connection.
*/
public void setInitialProxy(ConnectionConfig.ProxyType type, String host, int port, String user, String pass) {
if (proxyTypeCombo != null) {
proxyTypeCombo.setSelectedItem(type != null ? type : ConnectionConfig.ProxyType.NONE);
}
if (proxyHostField != null) {
proxyHostField.setText(host != null ? host : "");
}
if (proxyPortField != null) {
proxyPortField.setText(port > 0 ? String.valueOf(port) : "");
}
if (proxyUserField != null) {
proxyUserField.setText(user != null ? user : "");
}
if (proxyPassField != null) {
proxyPassField.setText(pass != null ? pass : "");
}
}
public ConnectionConfig.ProxyType getSelectedProxyType() {
return (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
}
public String getProxyHost() {
return proxyHostField.getText().trim();
}
public int getProxyPort() {
try {
return Integer.parseInt(proxyPortField.getText().trim());
} catch (Exception e) {
return 0;
}
}
public String getProxyUsername() {
return proxyUserField.getText().trim();
}
public String getProxyPassword() {
return new String(proxyPassField.getPassword());
}
} }
@@ -32,6 +32,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
super(parent, "3270 Presentation Space Field Inspector", false); super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
setSize(850, 420); setSize(850, 420);
@@ -60,7 +61,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
JPanel topPanel = new JPanel(new BorderLayout()); JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false); topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen"); countLabel = new JLabel("0 fields detected on screen");
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); countLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
topPanel.add(countLabel, BorderLayout.WEST); topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh"); JButton refreshBtn = new JButton("Refresh");
@@ -83,12 +84,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
table = new JTable(tableModel); table = new JTable(tableModel);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
// Column widths // Column widths
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260}; int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
@@ -32,6 +32,7 @@ public class FindDialog extends JDialog {
super(parent, "Find on Screen", false); super(parent, "Find on Screen", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
@@ -50,7 +51,7 @@ public class FindDialog extends JDialog {
gbc.insets = new Insets(4, 4, 4, 4); gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Search text // Search text
gbc.gridx = 0; gbc.gridx = 0;
@@ -63,7 +64,7 @@ public class FindDialog extends JDialog {
gbc.weightx = 1.0; gbc.weightx = 1.0;
searchField = new JTextField(20); searchField = new JTextField(20);
searchField.setText(lastSearchText); searchField.setText(lastSearchText);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); searchField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(searchField); ThemeManager.styleTextField(searchField);
formPanel.add(searchField, gbc); formPanel.add(searchField, gbc);
@@ -102,7 +103,7 @@ public class FindDialog extends JDialog {
gbc.gridwidth = 2; gbc.gridwidth = 2;
statusLabel = new JLabel(" "); statusLabel = new JLabel(" ");
statusLabel.setForeground(ThemeManager.getOiaFgAlert()); statusLabel.setForeground(ThemeManager.getOiaFgAlert());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(labelFont);
formPanel.add(statusLabel, gbc); formPanel.add(statusLabel, gbc);
mainPanel.add(formPanel, BorderLayout.CENTER); mainPanel.add(formPanel, BorderLayout.CENTER);
@@ -40,6 +40,7 @@ public class HostDirectoryDialog extends JDialog {
super(parent, "Host Catalog & Directory Browser", true); super(parent, "Host Catalog & Directory Browser", true);
this.client = client; this.client = client;
this.initialHostType = hostType; this.initialHostType = hostType;
AppIcon.applyTo(this);
buildUI(initialQuery); buildUI(initialQuery);
setSize(700, 420); setSize(700, 420);
@@ -50,7 +51,7 @@ public class HostDirectoryDialog extends JDialog {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12)); mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font font = ThemeManager.getUiFont();
// Top Query bar // Top Query bar
JPanel topPanel = new JPanel(new GridBagLayout()); JPanel topPanel = new JPanel(new GridBagLayout());
@@ -78,7 +79,7 @@ public class HostDirectoryDialog extends JDialog {
gbc.gridx = 3; gbc.gridx = 3;
gbc.weightx = 1.0; gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); queryField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(queryField); ThemeManager.styleTextField(queryField);
topPanel.add(queryField, gbc); topPanel.add(queryField, gbc);
@@ -96,12 +97,12 @@ public class HostDirectoryDialog extends JDialog {
tableModel = new DefaultTableModel(); tableModel = new DefaultTableModel();
table = new JTable(tableModel); table = new JTable(tableModel);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
table.addMouseListener(new MouseAdapter() { table.addMouseListener(new MouseAdapter() {
@Override @Override
@@ -122,7 +123,7 @@ public class HostDirectoryDialog extends JDialog {
statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(font);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
@@ -0,0 +1,391 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Real-time hotspot and URL auto-detection engine matching IBM Host On-Demand
* ShowURLOptions and ScreenText parity.
*
* Scans screen text using regular expressions for:
* 1. URLs (http://, https://, ftp://, file://, mailto:, www.)
* 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
* 3. Menu choices ("1.", "2.", "===> 3", "=> 4")
*
* Provides hit-testing for mouse hover/clicks and generates keystroke actions.
*/
public class HotspotEngine {
public enum HotspotType {
URL,
PF_KEY,
ENTER,
MENU_CHOICE
}
/**
* Represents an active interactive hotspot region on the terminal screen.
*/
public static class Hotspot {
private final HotspotType type;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final String displayText;
private final String action;
private final String tooltip;
public Hotspot(HotspotType type, int startRow, int startCol, int endRow, int endCol,
String displayText, String action, String tooltip) {
this.type = type;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.displayText = displayText;
this.action = action;
this.tooltip = tooltip;
}
public HotspotType getType() {
return type;
}
public int getStartRow() {
return startRow;
}
public int getStartCol() {
return startCol;
}
public int getEndRow() {
return endRow;
}
public int getEndCol() {
return endCol;
}
public String getDisplayText() {
return displayText;
}
public String getAction() {
return action;
}
public String getTooltip() {
return tooltip;
}
/**
* Test whether the given row/col coordinate falls within this hotspot.
*/
public boolean contains(int row, int col) {
if (row < startRow || row > endRow) {
return false;
}
if (startRow == endRow) {
return col >= startCol && col <= endCol;
}
if (row == startRow) {
return col >= startCol;
}
if (row == endRow) {
return col <= endCol;
}
return true; // Intermediate rows
}
@Override
public String toString() {
return "Hotspot{" + type + " [" + startRow + "," + startCol + "]-[" + endRow + "," + endCol + "] '" + displayText + "'}";
}
}
// Configurable scan enablement
private boolean urlEnabled = true;
private boolean pfKeyEnabled = true;
private boolean menuChoiceEnabled = true;
// Regular expression patterns
// 1. URLs
private static final Pattern URL_PATTERN = Pattern.compile(
"(?:https?|ftp|file)://[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|www\\.[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|mailto:[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+",
Pattern.CASE_INSENSITIVE
);
// 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
private static final Pattern PF_KEY_PATTERN = Pattern.compile(
"(?:(?<=\\W)|^)(PF(?:[1-9]|1[0-9]|2[0-4])|F(?:[1-9]|1[0-9]|2[0-4])|PA[1-3]|\\[ENTER\\]|ENTER)(?:(?=\\W)|$)",
Pattern.CASE_INSENSITIVE
);
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5"
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
);
private static final Pattern MENU_PROMPT_PATTERN = Pattern.compile(
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
);
// Caching state to avoid rescanning unchanged screens
private String cachedScreenText = "";
private int cachedRows = 0;
private int cachedCols = 0;
private List<Hotspot> cachedHotspots = Collections.emptyList();
public HotspotEngine() {
}
public boolean isUrlEnabled() {
return urlEnabled;
}
public void setUrlEnabled(boolean urlEnabled) {
this.urlEnabled = urlEnabled;
invalidateCache();
}
public boolean isPfKeyEnabled() {
return pfKeyEnabled;
}
public void setPfKeyEnabled(boolean pfKeyEnabled) {
this.pfKeyEnabled = pfKeyEnabled;
invalidateCache();
}
public boolean isMenuChoiceEnabled() {
return menuChoiceEnabled;
}
public void setMenuChoiceEnabled(boolean menuChoiceEnabled) {
this.menuChoiceEnabled = menuChoiceEnabled;
invalidateCache();
}
public void invalidateCache() {
cachedScreenText = "";
cachedHotspots = Collections.emptyList();
}
/**
* Scan the given ScreenBuffer for interactive hotspots.
*/
public synchronized List<Hotspot> scan(ScreenBuffer sb) {
if (sb == null) return Collections.emptyList();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
// Build plain text representation
StringBuilder fullText = new StringBuilder(rows * (cols + 1));
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getDisplayCell(baddr);
if (ea == null || ea.isFieldAttribute() || ea.ucs4 <= 0x20 || ea.ucs4 == 0xFF) {
fullText.append(' ');
} else {
fullText.append((char) ea.ucs4);
}
}
if (r < rows - 1) {
fullText.append('\n');
}
}
return scanText(fullText.toString(), rows, cols);
}
/**
* Scan formatted screen text (lines separated by '\n') for hotspots.
*/
public synchronized List<Hotspot> scanText(String screenText, int rows, int cols) {
if (screenText == null || rows <= 0 || cols <= 0) {
return Collections.emptyList();
}
if (screenText.equals(cachedScreenText) && rows == cachedRows && cols == cachedCols) {
return cachedHotspots;
}
List<Hotspot> hotspots = new ArrayList<>();
String[] lines = screenText.split("\n", -1);
int actualRows = Math.min(rows, lines.length);
// Scan line by line for precise row/column tracking
for (int r = 0; r < actualRows; r++) {
String line = lines[r];
boolean[] occupied = new boolean[line.length() + 1];
// 1. Scan URLs
if (urlEnabled) {
Matcher mUrl = URL_PATTERN.matcher(line);
while (mUrl.find()) {
int startCol = mUrl.start();
int endCol = mUrl.end();
String match = mUrl.group();
// Trim trailing punctuation like . , ; : ) ]
while (match.length() > 1) {
char last = match.charAt(match.length() - 1);
if (last == '.' || last == ',' || last == ';' || last == ':' || last == ')' || last == ']' || last == '>') {
match = match.substring(0, match.length() - 1);
endCol--;
} else {
break;
}
}
if (match.isEmpty()) continue;
String actionUrl = match;
if (match.toLowerCase().startsWith("www.")) {
actionUrl = "http://" + match;
}
Hotspot h = new Hotspot(
HotspotType.URL,
r, startCol, r, endCol - 1,
match, actionUrl, "Open URL: " + actionUrl
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
// 2. Scan PF / Function keys
if (pfKeyEnabled) {
Matcher mPf = PF_KEY_PATTERN.matcher(line);
while (mPf.find()) {
int startCol = mPf.start(1);
int endCol = mPf.end(1);
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String keyText = mPf.group(1).toUpperCase();
String action;
String tooltip;
if (keyText.equals("ENTER") || keyText.equals("[ENTER]")) {
action = "[enter]";
tooltip = "Send [ENTER]";
} else if (keyText.startsWith("PF")) {
String numStr = keyText.substring(2);
action = "[pf" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else if (keyText.startsWith("F")) {
String numStr = keyText.substring(1);
action = "[pf" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else if (keyText.startsWith("PA")) {
String numStr = keyText.substring(2);
action = "[pa" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else {
continue;
}
Hotspot h = new Hotspot(
keyText.contains("ENTER") ? HotspotType.ENTER : HotspotType.PF_KEY,
r, startCol, r, endCol - 1,
mPf.group(1), action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
// 3. Scan Menu choices ("1.", "2.", "===> 3", "=> 4")
if (menuChoiceEnabled) {
// Pattern 3a: Number followed by dot (e.g. " 1. ")
Matcher mNum = MENU_NUMBER_PATTERN.matcher(line);
while (mNum.find()) {
int startCol = mNum.start(1);
int endCol = mNum.end();
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String choiceNum = mNum.group(1);
String action = choiceNum + "[enter]";
String tooltip = "Select option " + choiceNum;
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
choiceNum + ".", action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
// Pattern 3b: Prompt followed by number (e.g. "===> 3")
Matcher mPrompt = MENU_PROMPT_PATTERN.matcher(line);
while (mPrompt.find()) {
int startCol = mPrompt.start();
int endCol = mPrompt.end();
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String choiceNum = mPrompt.group(1);
String action = choiceNum + "[enter]";
String tooltip = "Execute choice " + choiceNum;
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
mPrompt.group(), action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
}
this.cachedScreenText = screenText;
this.cachedRows = rows;
this.cachedCols = cols;
this.cachedHotspots = Collections.unmodifiableList(hotspots);
return cachedHotspots;
}
/**
* Hit-test for a hotspot at the specified terminal row and column.
*/
public Hotspot findHotspot(int row, int col) {
for (Hotspot h : cachedHotspots) {
if (h.contains(row, col)) {
return h;
}
}
return null;
}
/**
* Query currently active cached hotspots.
*/
public List<Hotspot> getHotspots() {
return cachedHotspots;
}
private static void markOccupied(boolean[] arr, int start, int end) {
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
arr[i] = true;
}
}
private static boolean isRangeOccupied(boolean[] arr, int start, int end) {
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
if (arr[i]) return true;
}
return false;
}
}
@@ -0,0 +1,484 @@
package haus.nightmare.j3270.ui;
import java.io.*;
import java.util.*;
import java.util.logging.Logger;
/**
* Keyboard mapping profile (.kmp) file importer and keybinding utility.
* Parses keyboard mapping profile files, translates key scan codes and mnemonics
* to j3270 action handlers.
*/
public class KeyBindings {
private static final Logger log = Logger.getLogger(KeyBindings.class.getName());
// IBM scan code to standard Java KeyStroke key name mapping
private static final Map<Integer, String> SCAN_CODE_MAP = new HashMap<>();
// Mnemonic to j3270 action mapping
private static final Map<String, String> MNEMONIC_MAP = new HashMap<>();
static {
// Alphanumeric and symbol keys
SCAN_CODE_MAP.put(1, "BACK_QUOTE");
SCAN_CODE_MAP.put(2, "1");
SCAN_CODE_MAP.put(3, "2");
SCAN_CODE_MAP.put(4, "3");
SCAN_CODE_MAP.put(5, "4");
SCAN_CODE_MAP.put(6, "5");
SCAN_CODE_MAP.put(7, "6");
SCAN_CODE_MAP.put(8, "7");
SCAN_CODE_MAP.put(9, "8");
SCAN_CODE_MAP.put(10, "9");
SCAN_CODE_MAP.put(11, "0");
SCAN_CODE_MAP.put(12, "MINUS");
SCAN_CODE_MAP.put(13, "EQUALS");
SCAN_CODE_MAP.put(14, "BACK_SPACE");
SCAN_CODE_MAP.put(15, "TAB");
SCAN_CODE_MAP.put(16, "Q");
SCAN_CODE_MAP.put(17, "W");
SCAN_CODE_MAP.put(18, "E");
SCAN_CODE_MAP.put(19, "R");
SCAN_CODE_MAP.put(20, "T");
SCAN_CODE_MAP.put(21, "Y");
SCAN_CODE_MAP.put(22, "U");
SCAN_CODE_MAP.put(23, "I");
SCAN_CODE_MAP.put(24, "O");
SCAN_CODE_MAP.put(25, "P");
SCAN_CODE_MAP.put(26, "OPEN_BRACKET");
SCAN_CODE_MAP.put(27, "CLOSE_BRACKET");
SCAN_CODE_MAP.put(28, "BACK_SLASH");
SCAN_CODE_MAP.put(29, "CAPS_LOCK");
SCAN_CODE_MAP.put(30, "A");
SCAN_CODE_MAP.put(31, "S");
SCAN_CODE_MAP.put(32, "D");
SCAN_CODE_MAP.put(33, "F");
SCAN_CODE_MAP.put(34, "G");
SCAN_CODE_MAP.put(35, "H");
SCAN_CODE_MAP.put(36, "J");
SCAN_CODE_MAP.put(37, "K");
SCAN_CODE_MAP.put(38, "L");
SCAN_CODE_MAP.put(39, "SEMICOLON");
SCAN_CODE_MAP.put(40, "QUOTE");
SCAN_CODE_MAP.put(41, "BACK_QUOTE");
SCAN_CODE_MAP.put(42, "ENTER");
SCAN_CODE_MAP.put(43, "ENTER");
SCAN_CODE_MAP.put(45, "Z");
SCAN_CODE_MAP.put(46, "X");
SCAN_CODE_MAP.put(47, "C");
SCAN_CODE_MAP.put(48, "V");
SCAN_CODE_MAP.put(49, "B");
SCAN_CODE_MAP.put(50, "N");
SCAN_CODE_MAP.put(51, "M");
SCAN_CODE_MAP.put(52, "COMMA");
SCAN_CODE_MAP.put(53, "PERIOD");
SCAN_CODE_MAP.put(54, "SLASH");
SCAN_CODE_MAP.put(57, "SPACE");
// Editing & Navigation keys
SCAN_CODE_MAP.put(75, "INSERT");
SCAN_CODE_MAP.put(76, "DELETE");
SCAN_CODE_MAP.put(79, "LEFT");
SCAN_CODE_MAP.put(80, "HOME");
SCAN_CODE_MAP.put(81, "END");
SCAN_CODE_MAP.put(83, "UP");
SCAN_CODE_MAP.put(84, "DOWN");
SCAN_CODE_MAP.put(85, "PAGE_UP");
SCAN_CODE_MAP.put(86, "PAGE_DOWN");
SCAN_CODE_MAP.put(89, "RIGHT");
// Numeric Keypad
SCAN_CODE_MAP.put(90, "NUM_LOCK");
SCAN_CODE_MAP.put(91, "NUMPAD7");
SCAN_CODE_MAP.put(92, "NUMPAD4");
SCAN_CODE_MAP.put(93, "NUMPAD1");
SCAN_CODE_MAP.put(95, "DIVIDE");
SCAN_CODE_MAP.put(96, "NUMPAD8");
SCAN_CODE_MAP.put(97, "NUMPAD5");
SCAN_CODE_MAP.put(98, "NUMPAD2");
SCAN_CODE_MAP.put(99, "NUMPAD0");
SCAN_CODE_MAP.put(100, "MULTIPLY");
SCAN_CODE_MAP.put(101, "NUMPAD9");
SCAN_CODE_MAP.put(102, "NUMPAD6");
SCAN_CODE_MAP.put(103, "NUMPAD3");
SCAN_CODE_MAP.put(104, "DECIMAL");
SCAN_CODE_MAP.put(105, "SUBTRACT");
SCAN_CODE_MAP.put(106, "ADD");
SCAN_CODE_MAP.put(108, "ENTER");
// Function keys & Escape
SCAN_CODE_MAP.put(110, "ESCAPE");
SCAN_CODE_MAP.put(112, "F1");
SCAN_CODE_MAP.put(113, "F2");
SCAN_CODE_MAP.put(114, "F3");
SCAN_CODE_MAP.put(115, "F4");
SCAN_CODE_MAP.put(116, "F5");
SCAN_CODE_MAP.put(117, "F6");
SCAN_CODE_MAP.put(118, "F7");
SCAN_CODE_MAP.put(119, "F8");
SCAN_CODE_MAP.put(120, "F9");
SCAN_CODE_MAP.put(121, "F10");
SCAN_CODE_MAP.put(122, "F11");
SCAN_CODE_MAP.put(123, "F12");
// Mnemonics mapping
MNEMONIC_MAP.put("enter", "ENTER");
MNEMONIC_MAP.put("enterreset", "ENTER");
MNEMONIC_MAP.put("newline", "NEWLINE");
MNEMONIC_MAP.put("tab", "TAB");
MNEMONIC_MAP.put("backtab", "shift TAB");
MNEMONIC_MAP.put("reset", "ESCAPE");
MNEMONIC_MAP.put("clear", "CLEAR");
MNEMONIC_MAP.put("eraseeof", "END");
MNEMONIC_MAP.put("eof", "END");
MNEMONIC_MAP.put("erasefld", "ERASE_INPUT");
MNEMONIC_MAP.put("erinp", "ERASE_INPUT");
MNEMONIC_MAP.put("eraseinput", "ERASE_INPUT");
MNEMONIC_MAP.put("dup", "DUP");
MNEMONIC_MAP.put("fieldmark", "FIELD_MARK");
MNEMONIC_MAP.put("fldmark", "FIELD_MARK");
MNEMONIC_MAP.put("fldext", "NEWLINE");
MNEMONIC_MAP.put("field-exit", "NEWLINE");
MNEMONIC_MAP.put("field+", "TAB");
MNEMONIC_MAP.put("field-", "TAB");
MNEMONIC_MAP.put("attn", "ATTN");
MNEMONIC_MAP.put("sysreq", "SYSREQ");
MNEMONIC_MAP.put("cursel", "CURSEL");
MNEMONIC_MAP.put("copy", "COPY");
MNEMONIC_MAP.put("paste", "PASTE");
MNEMONIC_MAP.put("selectall", "SELECTALL");
MNEMONIC_MAP.put("insert", "INSERT");
MNEMONIC_MAP.put("delete", "DELETE");
MNEMONIC_MAP.put("backspace", "BACK_SPACE");
MNEMONIC_MAP.put("home", "HOME");
MNEMONIC_MAP.put("end", "END");
MNEMONIC_MAP.put("up", "UP");
MNEMONIC_MAP.put("down", "DOWN");
MNEMONIC_MAP.put("left", "LEFT");
MNEMONIC_MAP.put("right", "RIGHT");
MNEMONIC_MAP.put("pageup", "PAGE_UP");
MNEMONIC_MAP.put("pagedn", "PAGE_DOWN");
MNEMONIC_MAP.put("pagedown", "PAGE_DOWN");
MNEMONIC_MAP.put("docmode", "DOCMODE");
MNEMONIC_MAP.put("wordwrap", "WORDWRAP");
MNEMONIC_MAP.put("apl", "APL");
MNEMONIC_MAP.put("rule", "CROSSHAIR_RULER");
MNEMONIC_MAP.put("statusbar", "STATUS_BAR");
for (int i = 1; i <= 24; i++) {
MNEMONIC_MAP.put("pf" + i, "PF" + i);
}
for (int i = 1; i <= 3; i++) {
MNEMONIC_MAP.put("pa" + i, "PA" + i);
}
// Shifted PF keys 1-12 = PF13-PF24
for (int i = 1; i <= 12; i++) {
MNEMONIC_MAP.put("spf" + i, "PF" + (i + 12));
}
}
/**
* Parse a .kmp file from a File object.
*
* @param file the .kmp file to parse
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(File file) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return parseKmp(reader);
}
}
/**
* Parse a .kmp file from an InputStream.
*
* @param in the InputStream to parse
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(InputStream in) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
return parseKmp(reader);
}
}
/**
* Parse a .kmp file from a Reader.
* Supports both IBM Host On-Demand (HoD) .kmp format (hashKey prefixes)
* and IBM Personal Communications (PCOMM) INI-style and bind statements.
*
* @param reader the reader to parse from
* @return Map of j3270 action names to key stroke bindings
* @throws IOException on I/O error
*/
public static Map<String, String> parseKmp(Reader reader) throws IOException {
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
List<String> rawLines = new ArrayList<>();
boolean hasHodKeyRemap = false;
boolean hasHodHashKey = false;
String line;
while ((line = br.readLine()) != null) {
rawLines.add(line);
String trimmed = line.trim();
if (trimmed.equalsIgnoreCase("[KeyRemap]")) {
hasHodKeyRemap = true;
}
int eq = trimmed.indexOf('=');
if (eq > 0) {
String left = trimmed.substring(0, eq).trim();
if (left.matches("^[A-Za-z][0-9]+$")) {
hasHodHashKey = true;
}
}
}
if (hasHodKeyRemap || hasHodHashKey) {
Map<String, String> hodMap = haus.nightmare.lib3270j.input.HodKeyMap.parseHodKmp(
new StringReader(String.join("\n", rawLines))
);
if (hodMap != null && !hodMap.isEmpty()) {
return hodMap;
}
}
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
for (String raw : rawLines) {
line = raw.trim();
// Strip comments
int commentIdx = line.indexOf(';');
if (commentIdx >= 0) {
line = line.substring(0, commentIdx).trim();
}
commentIdx = line.indexOf('#');
if (commentIdx >= 0) {
line = line.substring(0, commentIdx).trim();
}
if (line.isEmpty() || line.startsWith("[")) {
continue; // Skip section headers and blank lines
}
// Syntax 1: bind [mnemonic] KeyCombination OR bind action KeyCombination
if (line.toLowerCase().startsWith("bind ")) {
parseBindStatement(line.substring(5).trim(), actionToBindings);
continue;
}
// Syntax 2: KEY<n>=action OR Modifier-KEY<n>=action OR KeyStroke=[mnemonic]
int eq = line.indexOf('=');
if (eq > 0) {
String left = line.substring(0, eq).trim();
String right = line.substring(eq + 1).trim();
parseKeyAssignment(left, right, actionToBindings);
}
}
// Format into combined comma-separated bindings map
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : actionToBindings.entrySet()) {
String action = entry.getKey();
List<String> bindings = entry.getValue();
if (!bindings.isEmpty()) {
result.put(action, String.join(", ", bindings));
}
}
return result;
}
/**
* Export key bindings to a .kmp file using IBM HoD format.
*/
public static void exportKmp(File file, Map<String, String> keyBindings) throws IOException {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
exportKmp(bw, keyBindings);
}
}
/**
* Export key bindings to a Writer using IBM HoD format.
*/
public static void exportKmp(Writer writer, Map<String, String> keyBindings) throws IOException {
haus.nightmare.lib3270j.input.HodKeyMap.exportHodKmp(writer, keyBindings);
}
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
String[] parts = stmt.split("\\s+", 2);
if (parts.length < 2) return;
String target = parts[0].trim();
String keyStrokeStr = parts[1].trim();
String action = resolveMnemonic(target);
if (action == null) {
// Check if parts[1] was the mnemonic instead (e.g. "bind KeyCombination [mnemonic]")
String altAction = resolveMnemonic(keyStrokeStr);
if (altAction != null) {
action = altAction;
keyStrokeStr = target;
}
}
if (action != null) {
String normKeyStroke = normalizeKeyStroke(keyStrokeStr);
if (normKeyStroke != null && !normKeyStroke.isEmpty()) {
addBinding(actionToBindings, action, normKeyStroke);
}
}
}
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
if (left.matches("^[A-Za-z][0-9]+$")) {
haus.nightmare.lib3270j.input.HodKeyMap.KeyEntry entry =
haus.nightmare.lib3270j.input.HodKeyMap.parseHashKey(left, right);
if (entry != null) {
String stroke = entry.toKeyStrokeString();
String act = haus.nightmare.lib3270j.input.HodKeyMap.resolveMnemonicToAction(right);
if (act != null && stroke != null && !stroke.isEmpty()) {
addBinding(actionToBindings, act, stroke);
return;
}
}
}
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
String action = resolveMnemonic(right);
String keyStroke = null;
if (action != null) {
keyStroke = parseScanCodeEntry(left);
} else {
// Case B: left is mnemonic, right is KeyStroke
action = resolveMnemonic(left);
if (action != null) {
keyStroke = normalizeKeyStroke(right);
} else {
// Case C: left is KeyStroke, right is mnemonic
action = resolveMnemonic(right);
if (action != null) {
keyStroke = normalizeKeyStroke(left);
}
}
}
if (action != null && keyStroke != null && !keyStroke.isEmpty()) {
addBinding(actionToBindings, action, keyStroke);
}
}
/**
* Parse scan code entry like "KEY43", "S-KEY112", "C-KEY43", "CS-KEY85", etc.
*/
public static String parseScanCodeEntry(String entry) {
if (entry == null || entry.isEmpty()) return null;
String upper = entry.toUpperCase().trim();
int keyPos = upper.indexOf("KEY");
if (keyPos < 0) {
return normalizeKeyStroke(entry);
}
String prefix = upper.substring(0, keyPos);
if (prefix.endsWith("-")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
String numPart = upper.substring(keyPos + 3);
int scanCode;
try {
scanCode = Integer.parseInt(numPart.trim());
} catch (NumberFormatException e) {
return null;
}
String baseKey = SCAN_CODE_MAP.get(scanCode);
if (baseKey == null) {
return null;
}
StringBuilder sb = new StringBuilder();
if (prefix.contains("C")) sb.append("ctrl ");
if (prefix.contains("A") || prefix.contains("2")) sb.append("alt ");
if (prefix.contains("S")) sb.append("shift ");
if (prefix.contains("M")) sb.append("meta ");
sb.append(baseKey);
return sb.toString().trim();
}
/**
* Normalize key stroke expression (e.g. "Ctrl+Shift+C" -> "ctrl shift C").
*/
public static String normalizeKeyStroke(String stroke) {
if (stroke == null || stroke.trim().isEmpty()) return null;
String s = stroke.trim();
// Check if it is an IBM scan code
if (s.toUpperCase().contains("KEY")) {
String fromScan = parseScanCodeEntry(s);
if (fromScan != null) return fromScan;
}
// Replace '+' with space
s = s.replace("+", " ");
String[] parts = s.split("\\s+");
StringBuilder mods = new StringBuilder();
String mainKey = null;
for (String p : parts) {
String lp = p.toLowerCase();
if ("ctrl".equals(lp) || "control".equals(lp)) {
mods.append("ctrl ");
} else if ("shift".equals(lp)) {
mods.append("shift ");
} else if ("alt".equals(lp)) {
mods.append("alt ");
} else if ("meta".equals(lp) || "cmd".equals(lp) || "command".equals(lp)) {
mods.append("meta ");
} else {
mainKey = p.toUpperCase();
}
}
if (mainKey == null) return null;
return (mods.toString() + mainKey).trim();
}
/**
* Resolve a mnemonic token (e.g. "[enter]", "enter", "[pf12]", "eraseeof")
* into a j3270 action name.
*/
public static String resolveMnemonic(String token) {
if (token == null) return null;
String clean = token.trim();
if (clean.startsWith("[") && clean.endsWith("]")) {
clean = clean.substring(1, clean.length() - 1);
}
clean = clean.toLowerCase();
String direct = MNEMONIC_MAP.get(clean);
if (direct != null) {
return direct;
}
// Case-insensitive match on existing action names
String upper = clean.toUpperCase();
if (MNEMONIC_MAP.containsValue(upper)) {
return upper;
}
return null;
}
private static void addBinding(Map<String, List<String>> map, String action, String keyStroke) {
List<String> list = map.computeIfAbsent(action, k -> new ArrayList<>());
if (!list.contains(keyStroke)) {
list.add(keyStroke);
}
}
}
@@ -0,0 +1,392 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLSession;
import haus.nightmare.lib3270j.macro.Macro;
import haus.nightmare.lib3270j.macro.MacroScreen;
import haus.nightmare.lib3270j.macro.MacroScreens;
import haus.nightmare.lib3270j.macro.MacroXmlParser;
import haus.nightmare.lib3270j.macro.action.MacroAction;
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/**
* Interactive Swing dialog for loading, editing, running, and debugging
* IBM Host On-Demand XML Macros (<HASCRIPT> / .mac files).
*/
public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextArea xmlArea;
private JTextArea logArea;
private JLabel statusLabel;
private JButton playBtn;
private JButton pauseBtn;
private JButton stopBtn;
private Macro macro;
public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "IBM HoD XML Macro Runtime (<HASCRIPT>)", false);
this.client = client;
this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
this.macro = new Macro(client);
this.macro.addMacroListener(this);
this.macro.setPromptHandler(this);
buildUI();
setSize(780, 580);
setLocationRelativeTo(parent);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
Font labelFont = ThemeManager.getUiFont();
// Top info label
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
topPanel.setOpaque(false);
JLabel descLabel = new JLabel("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code>&lt;HASCRIPT&gt;</code>).<br>"
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Center split: XML editor and Execution Log
xmlArea = new JTextArea();
xmlArea.setFont(ThemeManager.getMonospacedUiFont());
xmlArea.setLineWrap(false);
xmlArea.setText(getDefaultMacroXml());
ThemeManager.styleTextArea(xmlArea);
JScrollPane xmlScroll = new JScrollPane(xmlArea);
ThemeManager.styleScrollPane(xmlScroll);
xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML (<HASCRIPT>)"));
logArea = new JTextArea();
logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1));
logArea.setEditable(false);
logArea.setLineWrap(true);
ThemeManager.styleTextArea(logArea);
JScrollPane logScroll = new JScrollPane(logArea);
ThemeManager.styleScrollPane(logScroll);
logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log"));
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll);
splitPane.setResizeWeight(0.65);
splitPane.setOpaque(false);
mainPanel.add(splitPane, BorderLayout.CENTER);
// Bottom control panel
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
bottomPanel.setOpaque(false);
statusLabel = new JLabel("Status: Stopped");
statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load .mac...");
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadMacroFile());
JButton saveBtn = new JButton("Save .mac...");
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveMacroFile());
playBtn = new JButton("Play");
ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY);
playBtn.setFont(labelFont);
playBtn.addActionListener(e -> playMacro());
pauseBtn = new JButton("Pause");
ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT);
pauseBtn.setFont(labelFont);
pauseBtn.setEnabled(false);
pauseBtn.addActionListener(e -> togglePause());
stopBtn = new JButton("Stop");
ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT);
stopBtn.setFont(labelFont);
stopBtn.setEnabled(false);
stopBtn.addActionListener(e -> stopMacro());
JButton clearLogBtn = new JButton("Clear Log");
ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT);
clearLogBtn.setFont(labelFont);
clearLogBtn.addActionListener(e -> logArea.setText(""));
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(8));
buttonPanel.add(playBtn);
buttonPanel.add(pauseBtn);
buttonPanel.add(stopBtn);
buttonPanel.add(clearLogBtn);
buttonPanel.add(Box.createHorizontalStrut(8));
buttonPanel.add(closeBtn);
bottomPanel.add(buttonPanel, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
}
private void loadMacroFile() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Open IBM HoD Macro");
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
sb.append(line).append("\n");
}
xmlArea.setText(sb.toString());
log("Loaded macro file: " + f.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void saveMacroFile() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Save IBM HoD Macro");
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
if (!f.getName().contains(".")) {
f = new File(f.getParentFile(), f.getName() + ".mac");
}
try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) {
w.write(xmlArea.getText());
log("Saved macro file: " + f.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
public void playMacro() {
String xml = xmlArea.getText();
if (xml == null || xml.trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE);
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
int opt = JOptionPane.showConfirmDialog(this,
"Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?",
"Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (opt != JOptionPane.YES_OPTION) {
return;
}
}
try {
MacroScreens screens = MacroXmlParser.parse(xml);
macro.setScreens(screens);
macro.setClient(client);
log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName()));
macro.play();
} catch (Exception ex) {
log("Parse / Launch Error: " + ex.getMessage());
JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE);
}
}
public void togglePause() {
if (macro.getState() == Macro.STATE_PLAYING) {
macro.pause();
pauseBtn.setText("Resume");
log("Macro paused.");
} else if (macro.getState() == Macro.STATE_PAUSED) {
macro.resume();
pauseBtn.setText("Pause");
log("Macro resumed.");
}
}
public void stopMacro() {
macro.stop();
log("Macro stopped by user.");
}
private void log(String msg) {
SwingUtilities.invokeLater(() -> {
logArea.append(msg + "\n");
logArea.setCaretPosition(logArea.getDocument().getLength());
});
}
// ========== MacroListener Callbacks ==========
@Override
public void macroStateChanged(Macro macro, int oldState, int newState) {
SwingUtilities.invokeLater(() -> {
switch (newState) {
case Macro.STATE_PLAYING:
statusLabel.setText("Status: Playing");
statusLabel.setForeground(ThemeManager.getFgMain());
playBtn.setEnabled(false);
pauseBtn.setEnabled(true);
pauseBtn.setText("Pause");
stopBtn.setEnabled(true);
break;
case Macro.STATE_PAUSED:
statusLabel.setText("Status: Paused");
statusLabel.setForeground(ThemeManager.getFgMuted());
pauseBtn.setText("Resume");
break;
case Macro.STATE_ERROR:
statusLabel.setText("Status: Error");
statusLabel.setForeground(Color.RED);
playBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
break;
case Macro.STATE_STOPPED:
default:
statusLabel.setText("Status: Stopped");
statusLabel.setForeground(ThemeManager.getFgMuted());
playBtn.setEnabled(true);
pauseBtn.setEnabled(false);
pauseBtn.setText("Pause");
stopBtn.setEnabled(false);
break;
}
});
}
@Override
public void macroScreenMatching(MacroScreen screen) {
log("Matched screen: [" + screen.getName() + "]");
if (terminalPanel != null) {
terminalPanel.repaint();
}
}
@Override
public void macroActionExecuting(MacroAction action) {
log(" -> Executing: " + action.getClass().getSimpleName());
if (terminalPanel != null) {
terminalPanel.repaint();
}
}
@Override
public void macroMessage(String title, String message) {
log("Message [" + title + "]: " + message);
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE);
});
}
@Override
public void macroError(String error) {
log("ERROR: " + error);
}
// ========== MacroPromptHandler Callback ==========
@Override
public String prompt(MacroActionPrompt prompt) {
AtomicReference<String> result = new AtomicReference<>(null);
if (SwingUtilities.isEventDispatchThread()) {
result.set(showPromptModal(prompt));
} else {
try {
SwingUtilities.invokeAndWait(() -> {
result.set(showPromptModal(prompt));
});
} catch (Exception e) {
return null;
}
}
return result.get();
}
private String showPromptModal(MacroActionPrompt prompt) {
JPanel panel = new JPanel(new BorderLayout(5, 5));
JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription());
panel.add(msgLabel, BorderLayout.NORTH);
JTextField textField;
if (prompt.isEncrypted()) {
textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
} else {
textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
}
panel.add(textField, BorderLayout.CENTER);
int opt = JOptionPane.showConfirmDialog(this, panel,
prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(),
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
if (opt == JOptionPane.OK_OPTION) {
if (textField instanceof JPasswordField) {
return new String(((JPasswordField) textField).getPassword());
}
return textField.getText();
}
return null;
}
private String getDefaultMacroXml() {
return "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
+ " <vars>\n"
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
+ " </vars>\n\n"
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
+ " </actions>\n"
+ " <nextscreens timeout=\"10000\">\n"
+ " <nextscreen name=\"ReadyScreen\" />\n"
+ " </nextscreens>\n"
+ " </screen>\n\n"
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
+ " </actions>\n"
+ " </screen>\n"
+ "</HASCRIPT>";
}
}
@@ -48,6 +48,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) { public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
super(parent, "IBM 3287 Printer Session Manager", false); super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent; this.parent = parent;
AppIcon.applyTo(this);
config = new PrinterConfig(defaultHost, defaultPort); config = new PrinterConfig(defaultHost, defaultPort);
config.setUseTls(defaultTls); config.setUseTls(defaultTls);
@@ -71,7 +72,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
gbc.insets = new Insets(3, 4, 3, 4); gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); Font labelFont = ThemeManager.getUiFont();
// Host / Port // Host / Port
gbc.gridx = 0; gbc.gridy = 0; gbc.gridx = 0; gbc.gridy = 0;
@@ -205,7 +206,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool text area // Spool text area
spoolArea = new JTextArea(); spoolArea = new JTextArea();
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); spoolArea.setFont(ThemeManager.getMonospacedUiFont());
spoolArea.setEditable(false); spoolArea.setEditable(false);
ThemeManager.styleTextArea(spoolArea); ThemeManager.styleTextArea(spoolArea);
@@ -256,7 +257,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
private JTextField createField(String text, int cols) { private JTextField createField(String text, int cols) {
JTextField tf = new JTextField(text, cols); JTextField tf = new JTextField(text, cols);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); tf.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(tf); ThemeManager.styleTextField(tf);
return tf; return tf;
} }
@@ -98,19 +98,21 @@ public class ScreenExporter {
writer.write("<style>\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("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(".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-blue { color: #7890f0; }\n");
writer.write(".c-red { color: #ff3232; }\n"); writer.write(".c-red { color: #ff0000; }\n");
writer.write(".c-pink { color: #ff82b4; }\n"); writer.write(".c-pink { color: #ff00ff; }\n");
writer.write(".c-green { color: #32cd32; }\n"); writer.write(".c-green { color: #00ff00; }\n");
writer.write(".c-turq { color: #40e0d0; }\n"); writer.write(".c-turq { color: #00ffff; }\n");
writer.write(".c-yellow { color: #ffff50; }\n"); writer.write(".c-yellow { color: #ffff00; }\n");
writer.write(".c-white { color: #ffffff; }\n"); writer.write(".c-white { color: #ffffff; }\n");
writer.write(".c-black { color: #000000; }\n"); writer.write(".c-black { color: #000000; }\n");
writer.write(".c-orange { color: #ffa500; }\n"); writer.write(".c-deepblue { color: #000080; }\n");
writer.write(".c-purple { color: #b482ff; }\n"); writer.write(".c-orange { color: #ffa200; }\n");
writer.write(".c-palegreen { color: #90ee90; }\n"); writer.write(".c-purple { color: #800080; }\n");
writer.write(".c-paleturq { color: #afeeee; }\n"); writer.write(".c-palegreen { color: #008000; }\n");
writer.write(".c-grey { color: #aaaaaa; }\n"); writer.write(".c-paleturq { color: #008080; }\n");
writer.write(".c-mustard { color: #a0a000; }\n");
writer.write(".c-grey { color: #c0c0c0; }\n");
writer.write(".bold { font-weight: bold; }\n"); writer.write(".bold { font-weight: bold; }\n");
writer.write(".underline { text-decoration: underline; }\n"); writer.write(".underline { text-decoration: underline; }\n");
writer.write("</style>\n</head>\n<body>\n<div class=\"screen\"><pre>"); writer.write("</style>\n</head>\n<body>\n<div class=\"screen\"><pre>");
@@ -209,17 +211,17 @@ public class ScreenExporter {
case 6: return "c-yellow"; case 6: return "c-yellow";
case 7: return "c-white"; case 7: return "c-white";
case 8: return "c-black"; case 8: return "c-black";
case 9: return "c-blue"; case 9: return "c-deepblue";
case 10: return "c-orange"; case 10: return "c-orange";
case 11: return "c-purple"; case 11: return "c-purple";
case 12: return "c-palegreen"; case 12: return "c-palegreen";
case 13: return "c-paleturq"; case 13: return "c-paleturq";
case 14: return "c-grey"; case 14: return "c-mustard";
case 15: return "c-white"; case 15: return "c-grey";
} }
} }
if (faIsProtected(currentFA & 0xFF)) { if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-blue"; return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-turq";
} }
return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green"; return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green";
} }
@@ -0,0 +1,323 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenHistoryListener;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenSnapshot;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Screen History viewer dialog matching IBM Host On-Demand ScreenHistory UI parity.
*
* Provides:
* - Interactive JSlider scrubber to review past 64 screen buffer frames
* - Previous / Next navigation buttons
* - Exact monospace terminal frame preview with selection capability
* - Copy Screen, Copy Selection, and Copy as Table actions
* - Live update synchronization as new host screens arrive
*/
public class ScreenHistoryDialog extends JDialog implements ScreenHistoryListener {
private final J3270App parentApp;
private final Telnet3270Client client;
private final ScreenHistoryBuffer historyBuffer;
private JSlider scrubSlider;
private JButton btnPrev;
private JButton btnNext;
private JLabel infoLabel;
private JTextArea previewArea;
private JButton btnCopyScreen;
private JButton btnCopySelection;
private JButton btnCopyTable;
private JButton btnClose;
private int currentIndex = -1; // 0-based index into historyBuffer
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public ScreenHistoryDialog(J3270App parent, Telnet3270Client client) {
super(parent, "Screen History", false);
this.parentApp = parent;
this.client = client;
this.historyBuffer = client != null ? client.getScreenHistoryBuffer() : null;
AppIcon.applyTo(this);
initComponents();
setupKeyNavigation();
if (historyBuffer != null) {
historyBuffer.addListener(this);
refreshHistoryView(historyBuffer.size() - 1);
} else {
updateEmptyState();
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
if (historyBuffer != null) {
historyBuffer.removeListener(ScreenHistoryDialog.this);
}
}
});
setSize(780, 560);
setLocationRelativeTo(parent);
}
private void initComponents() {
setLayout(new BorderLayout(6, 6));
JPanel contentPane = new JPanel(new BorderLayout(8, 8));
contentPane.setBorder(new EmptyBorder(10, 10, 10, 10));
setContentPane(contentPane);
// Top Navigation Bar
JPanel topPanel = new JPanel(new BorderLayout(8, 8));
topPanel.setBorder(BorderFactory.createTitledBorder("History Scrub Navigation"));
JPanel sliderPanel = new JPanel(new BorderLayout(6, 6));
btnPrev = new JButton("◄ Prev");
ThemeManager.styleButton(btnPrev, ThemeManager.ButtonVariant.DEFAULT);
btnPrev.addActionListener(e -> stepHistory(-1));
btnNext = new JButton("Next ►");
ThemeManager.styleButton(btnNext, ThemeManager.ButtonVariant.DEFAULT);
btnNext.addActionListener(e -> stepHistory(1));
scrubSlider = new JSlider(1, 1, 1);
scrubSlider.setMajorTickSpacing(10);
scrubSlider.setMinorTickSpacing(1);
scrubSlider.setPaintTicks(true);
scrubSlider.setSnapToTicks(true);
scrubSlider.addChangeListener(e -> {
if (!scrubSlider.getValueIsAdjusting()) {
int selected = scrubSlider.getValue() - 1;
displayScreen(selected);
}
});
sliderPanel.add(btnPrev, BorderLayout.WEST);
sliderPanel.add(scrubSlider, BorderLayout.CENTER);
sliderPanel.add(btnNext, BorderLayout.EAST);
infoLabel = new JLabel("Screen 0 of 0", SwingConstants.CENTER);
infoLabel.setFont(infoLabel.getFont().deriveFont(Font.BOLD, 13f));
topPanel.add(sliderPanel, BorderLayout.CENTER);
topPanel.add(infoLabel, BorderLayout.SOUTH);
contentPane.add(topPanel, BorderLayout.NORTH);
// Center Terminal Preview Pane
previewArea = new JTextArea();
previewArea.setEditable(false);
previewArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
previewArea.setBackground(Color.BLACK);
previewArea.setForeground(new Color(0, 255, 65)); // 3270 Green Phosphor
previewArea.setCaretColor(Color.WHITE);
previewArea.setLineWrap(false);
previewArea.setMargin(new Insets(6, 6, 6, 6));
JScrollPane scrollPane = new JScrollPane(previewArea);
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
contentPane.add(scrollPane, BorderLayout.CENTER);
// Bottom Action Bar
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
btnCopyScreen = new JButton("Copy Screen");
ThemeManager.styleButton(btnCopyScreen, ThemeManager.ButtonVariant.DEFAULT);
btnCopyScreen.setToolTipText("Copy entire historical screen to clipboard");
btnCopyScreen.addActionListener(e -> copyCurrentScreen());
btnCopySelection = new JButton("Copy Selection");
ThemeManager.styleButton(btnCopySelection, ThemeManager.ButtonVariant.DEFAULT);
btnCopySelection.setToolTipText("Copy selected text to clipboard");
btnCopySelection.addActionListener(e -> copySelectedText());
btnCopyTable = new JButton("Copy as Table");
ThemeManager.styleButton(btnCopyTable, ThemeManager.ButtonVariant.DEFAULT);
btnCopyTable.setToolTipText("Copy screen text with trimmed trailing spaces per row");
btnCopyTable.addActionListener(e -> copyAsTable());
btnClose = new JButton("Close");
ThemeManager.styleButton(btnClose, ThemeManager.ButtonVariant.DEFAULT);
btnClose.addActionListener(e -> setVisible(false));
bottomPanel.add(btnCopyScreen);
bottomPanel.add(btnCopySelection);
bottomPanel.add(btnCopyTable);
bottomPanel.add(Box.createHorizontalStrut(12));
bottomPanel.add(btnClose);
contentPane.add(bottomPanel, BorderLayout.SOUTH);
}
private void setupKeyNavigation() {
// Keyboard shortcuts for rapid scrub navigation
Action prevAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { stepHistory(-1); }
};
Action nextAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { stepHistory(1); }
};
Action homeAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { displayScreen(0); }
};
Action endAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) {
if (historyBuffer != null) displayScreen(historyBuffer.size() - 1);
}
};
Action closeAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { setVisible(false); }
};
JRootPane root = getRootPane();
InputMap im = root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap am = root.getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK), "prevScreen");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "prevScreen");
am.put("prevScreen", prevAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK), "nextScreen");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "nextScreen");
am.put("nextScreen", nextAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.ALT_DOWN_MASK), "homeScreen");
am.put("homeScreen", homeAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.ALT_DOWN_MASK), "endScreen");
am.put("endScreen", endAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeDialog");
am.put("closeDialog", closeAction);
}
private void stepHistory(int delta) {
if (historyBuffer == null || historyBuffer.isEmpty()) return;
int target = currentIndex + delta;
target = Math.max(0, Math.min(target, historyBuffer.size() - 1));
displayScreen(target);
}
private void displayScreen(int index) {
if (historyBuffer == null || historyBuffer.isEmpty()) {
updateEmptyState();
return;
}
int total = historyBuffer.size();
index = Math.max(0, Math.min(index, total - 1));
currentIndex = index;
ScreenSnapshot snap = historyBuffer.get(index);
if (snap != null) {
previewArea.setText(snap.getText());
previewArea.setCaretPosition(0);
String timeStr = dateFormat.format(new Date(snap.getTimestamp()));
infoLabel.setText("Screen " + (index + 1) + " of " + total + " (" + timeStr + ") — "
+ snap.getRows() + "x" + snap.getCols() + ", Cursor: ["
+ (snap.getCursorRow() + 1) + "," + (snap.getCursorCol() + 1) + "]");
scrubSlider.setValue(index + 1);
}
btnPrev.setEnabled(index > 0);
btnNext.setEnabled(index < total - 1);
btnCopyScreen.setEnabled(true);
btnCopySelection.setEnabled(true);
btnCopyTable.setEnabled(true);
}
private void updateEmptyState() {
currentIndex = -1;
previewArea.setText("No screen history captured yet.\n\nScreens are automatically recorded as the host session receives updates or unlocks the keyboard.");
infoLabel.setText("Screen 0 of 0 — Empty History");
scrubSlider.setEnabled(false);
btnPrev.setEnabled(false);
btnNext.setEnabled(false);
btnCopyScreen.setEnabled(false);
btnCopySelection.setEnabled(false);
btnCopyTable.setEnabled(false);
}
/**
* Refresh slider bounds and select target index.
*/
public synchronized void refreshHistoryView(int targetIndex) {
if (historyBuffer == null || historyBuffer.isEmpty()) {
updateEmptyState();
return;
}
int total = historyBuffer.size();
scrubSlider.setEnabled(true);
scrubSlider.setMinimum(1);
scrubSlider.setMaximum(total);
int select = targetIndex >= 0 ? Math.min(targetIndex, total - 1) : total - 1;
displayScreen(select);
}
@Override
public void onHistoryUpdated(ScreenHistoryBuffer buffer, ScreenSnapshot newSnapshot) {
SwingUtilities.invokeLater(() -> {
boolean wasAtLatest = (currentIndex >= 0 && currentIndex == buffer.size() - 2);
int newTotal = buffer.size();
scrubSlider.setMaximum(newTotal);
if (wasAtLatest || currentIndex < 0) {
// Keep tracked with latest frame
displayScreen(newTotal - 1);
} else {
// Keep operator's active scrub position, update label
displayScreen(currentIndex);
}
});
}
private void copyCurrentScreen() {
if (historyBuffer == null || currentIndex < 0) return;
ScreenSnapshot snap = historyBuffer.get(currentIndex);
if (snap != null) {
setClipboardText(snap.getText());
}
}
private void copySelectedText() {
String selected = previewArea.getSelectedText();
if (selected != null && !selected.isEmpty()) {
setClipboardText(selected);
} else {
copyCurrentScreen();
}
}
private void copyAsTable() {
if (historyBuffer == null || currentIndex < 0) return;
ScreenSnapshot snap = historyBuffer.get(currentIndex);
if (snap != null) {
setClipboardText(snap.getFormattedTable());
}
}
private void setClipboardText(String text) {
try {
Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
cb.setContents(new StringSelection(text), null);
} catch (Exception e) {
Toolkit.getDefaultToolkit().beep();
}
}
}
@@ -25,6 +25,7 @@ public class ScriptDialog extends JDialog {
super(parent, "Run Keystrokes / ECL Script", false); super(parent, "Run Keystrokes / ECL Script", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
setSize(600, 480); setSize(600, 480);
@@ -35,7 +36,7 @@ public class ScriptDialog extends JDialog {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Header // Header
JPanel topPanel = new JPanel(new BorderLayout(5, 5)); JPanel topPanel = new JPanel(new BorderLayout(5, 5));
@@ -48,7 +49,7 @@ public class ScriptDialog extends JDialog {
// Script area // Script area
scriptArea = new JTextArea(); scriptArea = new JTextArea();
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); scriptArea.setFont(ThemeManager.getMonospacedUiFont());
scriptArea.setLineWrap(true); scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false); scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea); ThemeManager.styleTextArea(scriptArea);
@@ -74,7 +75,7 @@ public class ScriptDialog extends JDialog {
for (String token : tokens) { for (String token : tokens) {
JButton btn = new JButton(token); JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11)); btn.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -2));
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
btn.setFocusable(false); btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4)); btn.setMargin(new Insets(2, 4, 2, 4));
@@ -94,7 +95,7 @@ public class ScriptDialog extends JDialog {
statusLabel = new JLabel("Ready"); statusLabel = new JLabel("Ready");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
File diff suppressed because it is too large Load Diff
@@ -18,27 +18,64 @@ public class StatusBar extends JPanel {
private final JLabel tlsStatus; private final JLabel tlsStatus;
private final JLabel luName; private final JLabel luName;
private final JLabel lockStatus; private final JLabel lockStatus;
private final JLabel insertStatus;
private final JLabel aplStatus;
private final JLabel fieldTypeStatus; private final JLabel fieldTypeStatus;
private final JLabel docModeStatus;
private final JLabel wordWrapStatus;
private final JLabel codePageInfo; private final JLabel codePageInfo;
private final JLabel modelInfo; private final JLabel modelInfo;
private final JLabel cursorPosition; private final JLabel cursorPosition;
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel; private TerminalPanel terminalPanel;
private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener;
public StatusBar() { public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(ThemeManager.getStatusBarBg()); setBackground(ThemeManager.getStatusBarBg());
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder())); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim()); connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
lockStatus = createLabel("", oiaFont, ThemeManager.getOiaFgAlert()); lockStatus = createLabel("", oiaFont, ThemeManager.getOiaInputInhibited());
insertStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Insert Mode (^ / Insert key) - Click to toggle", () -> {
if (terminalPanel != null) {
terminalPanel.toggleInsertMode();
} else if (client != null) {
client.toggleInsert();
updateStatus();
}
});
aplStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaFgAlert(), "APL Keyboard Mode (Alt+F3) - Click to toggle", () -> {
if (terminalPanel != null) {
terminalPanel.toggleAplMode();
} else if (client != null) {
client.toggleAplMode();
updateStatus();
}
});
fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
docModeStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Entry Assist Document Mode (Alt+F1) - Click to toggle", () -> {
if (terminalPanel != null) {
terminalPanel.toggleDocMode();
} else if (client != null) {
client.toggleDocMode();
updateStatus();
}
});
wordWrapStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Entry Assist Word Wrap (Alt+F2) - Click to toggle", () -> {
if (terminalPanel != null) {
terminalPanel.toggleWordWrap();
} else if (client != null) {
client.toggleWordWrap();
updateStatus();
}
});
codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal()); cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
@@ -51,9 +88,17 @@ public class StatusBar extends JPanel {
add(luName); add(luName);
add(Box.createHorizontalStrut(12)); add(Box.createHorizontalStrut(12));
add(lockStatus); add(lockStatus);
add(Box.createHorizontalStrut(8));
add(insertStatus);
add(Box.createHorizontalStrut(8));
add(aplStatus);
add(Box.createHorizontalStrut(10)); add(Box.createHorizontalStrut(10));
add(fieldTypeStatus); add(fieldTypeStatus);
add(Box.createHorizontalGlue()); add(Box.createHorizontalGlue());
add(docModeStatus);
add(Box.createHorizontalStrut(8));
add(wordWrapStatus);
add(Box.createHorizontalStrut(12));
add(codePageInfo); add(codePageInfo);
add(Box.createHorizontalStrut(12)); add(Box.createHorizontalStrut(12));
add(modelInfo); add(modelInfo);
@@ -69,19 +114,78 @@ public class StatusBar extends JPanel {
return label; return label;
} }
private JLabel createClickableLabel(String text, Font font, Color fg, String tooltip, Runnable onClick) {
JLabel label = new JLabel(text);
label.setFont(font);
label.setForeground(fg);
label.setToolTipText(tooltip);
label.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR));
label.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (onClick != null) {
onClick.run();
}
}
});
return label;
}
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) { public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
if (this.client != null && this.client.getOIA() != null && this.oiaNotifyListener != null) {
this.client.getOIA().unregisterOIAEvent(this.oiaNotifyListener);
}
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
if (this.client != null && this.client.getOIA() != null) {
this.oiaNotifyListener = event -> updateStatus();
this.client.getOIA().registerOIAEvent(this.oiaNotifyListener);
}
updateStatus(); updateStatus();
} }
public void applyTheme(UITheme theme) { public void applyTheme(UITheme theme) {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(() -> applyTheme(theme));
return;
}
setBackground(ThemeManager.getStatusBarBg(theme)); setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme))); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
applyFontRecursively(this, oiaFont);
revalidate();
repaint();
updateStatus(); updateStatus();
} }
private void updateBarHeight(Font font) {
int fontHeight = 16;
try {
FontMetrics fm = getFontMetrics(font);
if (fm != null) fontHeight = fm.getHeight();
} catch (Exception ignored) {
fontHeight = font.getSize() + 4;
}
int barHeight = Math.max(22, fontHeight + 6);
setPreferredSize(new Dimension(800, barHeight));
}
private void applyFontRecursively(Component comp, Font font) {
if (comp instanceof JLabel) {
comp.setFont(font);
} else if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyFontRecursively(child, font);
}
}
}
public void updateStatus() { public void updateStatus() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::updateStatus);
return;
}
UITheme theme = ThemeManager.getTheme(); UITheme theme = ThemeManager.getTheme();
if (client == null) { if (client == null) {
connectionStatus.setText("Not Connected"); connectionStatus.setText("Not Connected");
@@ -89,10 +193,16 @@ public class StatusBar extends JPanel {
tlsStatus.setText(""); tlsStatus.setText("");
luName.setText(""); luName.setText("");
lockStatus.setText(""); lockStatus.setText("");
lockStatus.setForeground(ThemeManager.getOiaInputInhibited(theme));
insertStatus.setText("");
aplStatus.setText("");
fieldTypeStatus.setText(""); fieldTypeStatus.setText("");
docModeStatus.setText("");
wordWrapStatus.setText("");
codePageInfo.setText(""); codePageInfo.setText("");
modelInfo.setText(""); modelInfo.setText("");
cursorPosition.setText("001/001"); cursorPosition.setText("001/001");
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
return; return;
} }
@@ -103,6 +213,10 @@ public class StatusBar extends JPanel {
connectionStatus.setText("Not Connected"); connectionStatus.setText("Not Connected");
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme)); connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
break; break;
case RECONNECTING:
connectionStatus.setText("Reconnecting...");
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
break;
case TCP_PENDING: case TCP_PENDING:
case TELNET_PENDING: case TELNET_PENDING:
connectionStatus.setText("Connecting..."); connectionStatus.setText("Connecting...");
@@ -110,25 +224,25 @@ public class StatusBar extends JPanel {
break; break;
case CONNECTED_3270: case CONNECTED_3270:
connectionStatus.setText("TN3270"); connectionStatus.setText("TN3270");
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme)); connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break; break;
case CONNECTED_TN3270E: case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E"); connectionStatus.setText("TN3270E");
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme)); connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break; break;
case CONNECTED_SSCP: case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU"); connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme)); connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break; break;
case CONNECTED_NVT: case CONNECTED_NVT:
case CONNECTED_NVT_CHAR: case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT: case CONNECTED_E_NVT:
connectionStatus.setText("NVT"); connectionStatus.setText("NVT");
connectionStatus.setForeground(ThemeManager.getOiaFgNormal(theme)); connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break; break;
case CONNECTED_UNBOUND: case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound"); connectionStatus.setText("Unbound");
connectionStatus.setForeground(ThemeManager.getOiaFgWarn(theme)); connectionStatus.setForeground(ThemeManager.getOiaAttention(theme));
break; break;
default: default:
connectionStatus.setText(state.name()); connectionStatus.setText(state.name());
@@ -144,11 +258,11 @@ public class StatusBar extends JPanel {
String protocol = session != null ? session.getProtocol() : "TLS"; String protocol = session != null ? session.getProtocol() : "TLS";
if (verified) { if (verified) {
tlsStatus.setText("🔒 TLS"); tlsStatus.setText("🔒 TLS");
tlsStatus.setForeground(ThemeManager.getOiaFgNormal(theme)); tlsStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)"); tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else { } else {
tlsStatus.setText("🔓 TLS (Unverified)"); tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(ThemeManager.getOiaFgWarn(theme)); tlsStatus.setForeground(ThemeManager.getOiaAttention(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)"); tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
} }
} else { } else {
@@ -164,42 +278,85 @@ public class StatusBar extends JPanel {
lu = "LU:" + client.getConfig().getLuName(); lu = "LU:" + client.getConfig().getLuName();
} }
luName.setText(lu); luName.setText(lu);
luName.setForeground(ThemeManager.getOiaFgNormal(theme)); luName.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
// Lock / Inhibit status // Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited(); int inhibit = client.getOIA().getInputInhibited();
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) { if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
Color lockFg = ThemeManager.getOiaInputInhibited(theme);
switch (inhibit) { switch (inhibit) {
case ECLConstants.INHIBIT_SYSTEM_LOCK: case ECLConstants.INHIBIT_SYSTEM_LOCK:
lockStatus.setText("X SYSTEM"); lockStatus.setText("X SYSTEM");
lockFg = ThemeManager.getOiaInputInhibited(theme);
break; break;
case ECLConstants.INHIBIT_COMM_CHECK: case ECLConstants.INHIBIT_COMM_CHECK:
lockStatus.setText("X COMM"); lockStatus.setText("X COMM");
lockFg = ThemeManager.getOiaCommCheck(theme);
break; break;
case ECLConstants.INHIBIT_NUMERIC_ONLY: case ECLConstants.INHIBIT_NUMERIC_ONLY:
lockStatus.setText("X NUM"); lockStatus.setText("X NUM");
lockFg = ThemeManager.getOiaAttention(theme);
break; break;
case ECLConstants.INHIBIT_PROTECTED_FIELD: case ECLConstants.INHIBIT_PROTECTED_FIELD:
lockStatus.setText("X PROT"); lockStatus.setText("X PROT");
lockFg = ThemeManager.getOiaInputInhibited(theme);
break; break;
case ECLConstants.INHIBIT_OVERFLOW: case ECLConstants.INHIBIT_OVERFLOW:
lockStatus.setText("X >"); lockStatus.setText("X >");
lockFg = ThemeManager.getOiaAttention(theme);
break; break;
case ECLConstants.INHIBIT_OPERATOR_DUE: case ECLConstants.INHIBIT_OPERATOR_DUE:
lockStatus.setText("X OP"); lockStatus.setText("X OP");
lockFg = ThemeManager.getOiaAttention(theme);
break; break;
default: default:
lockStatus.setText("X LOCKED"); lockStatus.setText("X LOCKED");
lockFg = ThemeManager.getOiaInputInhibited(theme);
break; break;
} }
lockStatus.setForeground(ThemeManager.getOiaFgAlert(theme)); lockStatus.setForeground(lockFg);
} else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT");
lockStatus.setForeground(ThemeManager.getOiaFgNormal(theme));
} else { } else {
lockStatus.setText(""); lockStatus.setText("");
} }
// Insert Mode Indicator
boolean isInsert = false;
if (terminalPanel != null) {
isInsert = terminalPanel.isInsertMode();
} else if (client.getInputProcessor() != null) {
isInsert = client.getInputProcessor().isInsertMode();
}
if (isInsert) {
insertStatus.setText("^ INS");
insertStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
insertStatus.setText("");
}
// APL Keyboard Mode Indicator
if (client.getInputProcessor() != null && client.getInputProcessor().isAplKeyboardMode()) {
aplStatus.setText("APL");
aplStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
} else {
aplStatus.setText("");
}
// Entry Assist DOC Mode Indicator
if (client.getScreenBuffer() != null && client.getScreenBuffer().isEntryAssistDOCmode()) {
docModeStatus.setText("DOC");
docModeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
docModeStatus.setText("");
}
// Entry Assist Word Wrap Indicator
if (client.getScreenBuffer() != null && client.getScreenBuffer().isEntryAssistWordWrap()) {
wordWrapStatus.setText("V");
wordWrapStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else {
wordWrapStatus.setText("");
}
// Field status: Numeric vs Alphanumeric // Field status: Numeric vs Alphanumeric
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) { if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) { if (client.getOIA().isNumeric()) {
@@ -223,7 +380,13 @@ public class StatusBar extends JPanel {
ScreenBuffer sb = client.getScreenBuffer(); ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows(); int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols(); int cols = sb.getDisplayCols();
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]"); String modelName;
if (client.getConfig().isDynamicModel() || (client.getConfig().getModel() != null && client.getConfig().getModel().isDynamic())) {
modelName = "IBM-DYNAMIC [" + rows + "x" + cols + "]";
} else {
modelName = client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]";
}
modelInfo.setText(modelName);
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme)); modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
// Cursor position and buffer address // Cursor position and buffer address
@@ -233,4 +396,8 @@ public class StatusBar extends JPanel {
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr)); cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme)); cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
} }
public String getLockStatusText() {
return lockStatus != null ? lockStatus.getText() : "";
}
} }
File diff suppressed because it is too large Load Diff
@@ -20,6 +20,7 @@ import java.awt.event.MouseEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import haus.nightmare.j3270.config.Settings;
/** /**
* Centralized theme manager for j3270 Java desktop UI. * Centralized theme manager for j3270 Java desktop UI.
@@ -33,6 +34,25 @@ public final class ThemeManager {
private ThemeManager() {} private ThemeManager() {}
public static Font getUiFont() {
return getUiFont(Font.PLAIN, 0);
}
public static Font getUiFont(int style, int sizeDelta) {
String family = Settings.getUiFontFamily();
int size = Math.max(8, Settings.getUiFontSize() + sizeDelta);
return new Font(family, style, size);
}
public static Font getMonospacedUiFont() {
return getMonospacedUiFont(Font.PLAIN, 0);
}
public static Font getMonospacedUiFont(int style, int sizeDelta) {
int size = Math.max(8, Settings.getUiFontSize() + sizeDelta);
return new Font(Font.MONOSPACED, style, size);
}
public static UITheme getTheme() { public static UITheme getTheme() {
return currentTheme; return currentTheme;
} }
@@ -244,6 +264,38 @@ public final class ThemeManager {
return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0); return t == UITheme.DARK ? new Color(255, 190, 70) : new Color(180, 100, 0);
} }
// Host On-Demand OIA Category Colors (oSI, oII, oAI, oEI, oOB)
public static final Color HOD_OIA_STATUS_SYS_AVAIL = new Color(120, 144, 240); // oSI: CUSTOMBLUE
public static final Color HOD_OIA_INPUT_INHIBITED = new Color(255, 255, 255); // oII: White
public static final Color HOD_OIA_ATTENTION_WARN = new Color(255, 255, 0); // oAI: Yellow
public static final Color HOD_OIA_COMM_CHECK_ERROR = new Color(255, 0, 0); // oEI: Red
public static final Color HOD_OIA_BG_BLACK = new Color(0, 0, 0); // oOB: Black
public static Color getOiaStatusSysAvail() { return getOiaStatusSysAvail(currentTheme); }
public static Color getOiaStatusSysAvail(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_STATUS_SYS_AVAIL : new Color(0, 90, 200);
}
public static Color getOiaInputInhibited() { return getOiaInputInhibited(currentTheme); }
public static Color getOiaInputInhibited(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_INPUT_INHIBITED : new Color(20, 20, 20);
}
public static Color getOiaAttention() { return getOiaAttention(currentTheme); }
public static Color getOiaAttention(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_ATTENTION_WARN : new Color(180, 100, 0);
}
public static Color getOiaCommCheck() { return getOiaCommCheck(currentTheme); }
public static Color getOiaCommCheck(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_COMM_CHECK_ERROR : new Color(190, 20, 20);
}
public static Color getOiaBackground() { return getOiaBackground(currentTheme); }
public static Color getOiaBackground(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_BG_BLACK : getStatusBarBg(t);
}
// ========================================================================= // =========================================================================
// Button Variants & Color helpers // Button Variants & Color helpers
// ========================================================================= // =========================================================================
@@ -401,6 +453,47 @@ public final class ThemeManager {
UIManager.put("TitledBorder.titleColor", fgMain); UIManager.put("TitledBorder.titleColor", fgMain);
UIManager.put("OptionPane.background", bgPanel); UIManager.put("OptionPane.background", bgPanel);
UIManager.put("OptionPane.messageForeground", fgMain); UIManager.put("OptionPane.messageForeground", fgMain);
// UI Font Defaults
Font uiFont = getUiFont();
Font uiFontBold = getUiFont(Font.BOLD, 0);
Font monoUiFont = getMonospacedUiFont();
UIManager.put("Button.font", uiFont);
UIManager.put("ToggleButton.font", uiFont);
UIManager.put("RadioButton.font", uiFont);
UIManager.put("CheckBox.font", uiFont);
UIManager.put("ColorChooser.font", uiFont);
UIManager.put("ComboBox.font", uiFont);
UIManager.put("Label.font", uiFont);
UIManager.put("List.font", uiFont);
UIManager.put("MenuBar.font", uiFont);
UIManager.put("MenuItem.font", uiFont);
UIManager.put("RadioButtonMenuItem.font", uiFont);
UIManager.put("CheckBoxMenuItem.font", uiFont);
UIManager.put("Menu.font", uiFont);
UIManager.put("PopupMenu.font", uiFont);
UIManager.put("OptionPane.font", uiFont);
UIManager.put("OptionPane.messageFont", uiFont);
UIManager.put("OptionPane.buttonFont", uiFont);
UIManager.put("Panel.font", uiFont);
UIManager.put("ProgressBar.font", uiFont);
UIManager.put("ScrollPane.font", uiFont);
UIManager.put("Viewport.font", uiFont);
UIManager.put("TabbedPane.font", uiFont);
UIManager.put("Table.font", uiFont);
UIManager.put("TableHeader.font", uiFontBold);
UIManager.put("TextField.font", uiFont);
UIManager.put("PasswordField.font", uiFont);
UIManager.put("TextArea.font", monoUiFont);
UIManager.put("TextPane.font", uiFont);
UIManager.put("EditorPane.font", uiFont);
UIManager.put("TitledBorder.font", uiFontBold);
UIManager.put("ToolBar.font", uiFont);
UIManager.put("ToolTip.font", uiFont);
UIManager.put("Tree.font", uiFont);
UIManager.put("Spinner.font", uiFont);
UIManager.put("FormattedTextField.font", uiFont);
} }
// ========================================================================= // =========================================================================
@@ -414,6 +507,7 @@ public final class ThemeManager {
public static JButton styleButton(JButton button, ButtonVariant variant) { public static JButton styleButton(JButton button, ButtonVariant variant) {
if (button == null) return null; if (button == null) return null;
button.setUI(new StyledButtonUI(variant)); button.setUI(new StyledButtonUI(variant));
button.setFont(getUiFont());
button.setFocusPainted(false); button.setFocusPainted(false);
button.setOpaque(false); button.setOpaque(false);
button.setContentAreaFilled(false); button.setContentAreaFilled(false);
@@ -433,6 +527,7 @@ public final class ThemeManager {
public static JTextField styleTextField(JTextField field) { public static JTextField styleTextField(JTextField field) {
if (field == null) return null; if (field == null) return null;
field.setFont(getUiFont());
field.setBackground(getBgComponent()); field.setBackground(getBgComponent());
field.setForeground(getFgMain()); field.setForeground(getFgMain());
field.setCaretColor(getFgMain()); field.setCaretColor(getFgMain());
@@ -446,6 +541,7 @@ public final class ThemeManager {
public static JTextArea styleTextArea(JTextArea area) { public static JTextArea styleTextArea(JTextArea area) {
if (area == null) return null; if (area == null) return null;
area.setFont(getMonospacedUiFont());
area.setBackground(getCodeAreaBg()); area.setBackground(getCodeAreaBg());
area.setForeground(getCodeAreaFg()); area.setForeground(getCodeAreaFg());
area.setCaretColor(getFgMain()); area.setCaretColor(getFgMain());
@@ -456,6 +552,7 @@ public final class ThemeManager {
public static JComboBox<?> styleComboBox(JComboBox<?> box) { public static JComboBox<?> styleComboBox(JComboBox<?> box) {
if (box == null) return null; if (box == null) return null;
box.setFont(getUiFont());
box.setBackground(getBgComponent()); box.setBackground(getBgComponent());
box.setForeground(getFgMain()); box.setForeground(getFgMain());
box.setRenderer(new DefaultListCellRenderer() { box.setRenderer(new DefaultListCellRenderer() {
@@ -478,12 +575,14 @@ public final class ThemeManager {
public static JTable styleTable(JTable table) { public static JTable styleTable(JTable table) {
if (table == null) return null; if (table == null) return null;
table.setFont(getUiFont());
table.setBackground(getBgComponent()); table.setBackground(getBgComponent());
table.setForeground(getFgMain()); table.setForeground(getFgMain());
table.setSelectionBackground(getSelectionBg()); table.setSelectionBackground(getSelectionBg());
table.setSelectionForeground(getSelectionFg()); table.setSelectionForeground(getSelectionFg());
table.setGridColor(getTableGrid()); table.setGridColor(getTableGrid());
table.setRowHeight(24); int rowHeight = Math.max(24, getUiFont().getSize() + 10);
table.setRowHeight(rowHeight);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() { table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override @Override
@@ -505,14 +604,14 @@ public final class ThemeManager {
if (header != null) { if (header != null) {
header.setBackground(getTableHeaderBg()); header.setBackground(getTableHeaderBg());
header.setForeground(getTableHeaderFg()); header.setForeground(getTableHeaderFg());
header.setFont(header.getFont().deriveFont(Font.BOLD)); header.setFont(getUiFont(Font.BOLD, 0));
header.setDefaultRenderer(new DefaultTableCellRenderer() { header.setDefaultRenderer(new DefaultTableCellRenderer() {
@Override @Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) { public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col); super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBackground(getTableHeaderBg()); setBackground(getTableHeaderBg());
setForeground(getTableHeaderFg()); setForeground(getTableHeaderFg());
setFont(getFont().deriveFont(Font.BOLD)); setFont(getUiFont(Font.BOLD, 0));
setBorder(BorderFactory.createCompoundBorder( setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()), BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
new EmptyBorder(4, 6, 4, 6))); new EmptyBorder(4, 6, 4, 6)));
@@ -526,6 +625,7 @@ public final class ThemeManager {
public static JTabbedPane styleTabbedPane(JTabbedPane tp) { public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
if (tp == null) return null; if (tp == null) return null;
tp.setUI(new StyledTabbedPaneUI()); tp.setUI(new StyledTabbedPaneUI());
tp.setFont(getUiFont());
tp.setBackground(getBgMain()); tp.setBackground(getBgMain());
tp.setForeground(getFgMain()); tp.setForeground(getFgMain());
return tp; return tp;
@@ -534,6 +634,7 @@ public final class ThemeManager {
public static JMenuBar styleMenuBar(JMenuBar bar) { public static JMenuBar styleMenuBar(JMenuBar bar) {
if (bar == null) return null; if (bar == null) return null;
bar.setUI(new StyledMenuBarUI()); bar.setUI(new StyledMenuBarUI());
bar.setFont(getUiFont());
bar.setBackground(getMenuBarBg()); bar.setBackground(getMenuBarBg());
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder())); bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
return bar; return bar;
@@ -542,6 +643,7 @@ public final class ThemeManager {
public static JMenu styleMenu(JMenu menu) { public static JMenu styleMenu(JMenu menu) {
if (menu == null) return null; if (menu == null) return null;
menu.setUI(new StyledMenuUI()); menu.setUI(new StyledMenuUI());
menu.setFont(getUiFont());
menu.setForeground(getMenuBarFg()); menu.setForeground(getMenuBarFg());
menu.setBackground(getMenuBarBg()); menu.setBackground(getMenuBarBg());
menu.setOpaque(false); menu.setOpaque(false);
@@ -551,6 +653,7 @@ public final class ThemeManager {
public static JMenuItem styleMenuItem(JMenuItem item) { public static JMenuItem styleMenuItem(JMenuItem item) {
if (item == null) return null; if (item == null) return null;
item.setUI(new StyledMenuItemUI()); item.setUI(new StyledMenuItemUI());
item.setFont(getUiFont());
item.setBackground(getMenuPopupBg()); item.setBackground(getMenuPopupBg());
item.setForeground(getMenuItemFg()); item.setForeground(getMenuItemFg());
return item; return item;
@@ -559,6 +662,7 @@ public final class ThemeManager {
public static JPopupMenu stylePopupMenu(JPopupMenu popup) { public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
if (popup == null) return null; if (popup == null) return null;
popup.setUI(new StyledPopupMenuUI()); popup.setUI(new StyledPopupMenuUI());
popup.setFont(getUiFont());
popup.setBackground(getMenuPopupBg()); popup.setBackground(getMenuPopupBg());
popup.setBorder(BorderFactory.createCompoundBorder( popup.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1), new LineBorder(getBorder(), 1),
@@ -578,12 +682,14 @@ public final class ThemeManager {
public static JSpinner styleSpinner(JSpinner sp) { public static JSpinner styleSpinner(JSpinner sp) {
if (sp == null) return null; if (sp == null) return null;
sp.setFont(getUiFont());
sp.setBackground(getBgComponent()); sp.setBackground(getBgComponent());
sp.setForeground(getFgMain()); sp.setForeground(getFgMain());
sp.setBorder(new LineBorder(getBorder(), 1)); sp.setBorder(new LineBorder(getBorder(), 1));
JComponent editor = sp.getEditor(); JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) { if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField(); JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setFont(getUiFont());
tf.setBackground(getBgComponent()); tf.setBackground(getBgComponent());
tf.setForeground(getFgMain()); tf.setForeground(getFgMain());
tf.setCaretColor(getFgMain()); tf.setCaretColor(getFgMain());
@@ -594,6 +700,7 @@ public final class ThemeManager {
public static JCheckBox styleCheckBox(JCheckBox cb) { public static JCheckBox styleCheckBox(JCheckBox cb) {
if (cb == null) return null; if (cb == null) return null;
cb.setFont(getUiFont());
cb.setOpaque(false); cb.setOpaque(false);
cb.setForeground(getFgMain()); cb.setForeground(getFgMain());
cb.setFocusPainted(false); cb.setFocusPainted(false);
@@ -602,6 +709,7 @@ public final class ThemeManager {
public static JRadioButton styleRadioButton(JRadioButton rb) { public static JRadioButton styleRadioButton(JRadioButton rb) {
if (rb == null) return null; if (rb == null) return null;
rb.setFont(getUiFont());
rb.setOpaque(false); rb.setOpaque(false);
rb.setForeground(getFgMain()); rb.setForeground(getFgMain());
rb.setFocusPainted(false); rb.setFocusPainted(false);
@@ -614,7 +722,7 @@ public final class ThemeManager {
title, title,
TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION, TitledBorder.DEFAULT_POSITION,
new Font(Font.SANS_SERIF, Font.BOLD, 12), getUiFont(Font.BOLD, 0),
getFgMain()); getFgMain());
} }
@@ -634,6 +742,16 @@ public final class ThemeManager {
return; return;
} }
if (comp instanceof StatusBar) {
((StatusBar) comp).applyTheme(theme);
return;
}
if (comp instanceof TerminalPanel) {
comp.repaint();
return;
}
if (comp instanceof JMenuBar) { if (comp instanceof JMenuBar) {
styleMenuBar((JMenuBar) comp); styleMenuBar((JMenuBar) comp);
for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) { for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) {
@@ -690,6 +808,7 @@ public final class ThemeManager {
if (comp instanceof JButton) { if (comp instanceof JButton) {
JButton btn = (JButton) comp; JButton btn = (JButton) comp;
btn.setFont(getUiFont());
if (!(btn.getUI() instanceof StyledButtonUI)) { if (!(btn.getUI() instanceof StyledButtonUI)) {
styleButton(btn, ButtonVariant.DEFAULT); styleButton(btn, ButtonVariant.DEFAULT);
} }
@@ -728,6 +847,21 @@ public final class ThemeManager {
if (comp instanceof JLabel) { if (comp instanceof JLabel) {
comp.setForeground(getFgMain(theme)); comp.setForeground(getFgMain(theme));
JLabel label = (JLabel) comp;
Font cur = label.getFont();
int style = (cur != null && cur.isBold()) ? Font.BOLD : Font.PLAIN;
Object deltaProp = label.getClientProperty("uiFontSizeDelta");
int delta = 0;
if (deltaProp instanceof Integer) {
delta = (Integer) deltaProp;
} else {
int baseSize = Settings.getUiFontSize();
if (cur != null && cur.getSize() > baseSize) {
delta = cur.getSize() - baseSize;
}
label.putClientProperty("uiFontSizeDelta", delta);
}
label.setFont(getUiFont(style, delta));
return; return;
} }
@@ -741,6 +875,7 @@ public final class ThemeManager {
if (b instanceof TitledBorder) { if (b instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) b; TitledBorder tb = (TitledBorder) b;
tb.setTitleColor(getFgMain(theme)); tb.setTitleColor(getFgMain(theme));
tb.setTitleFont(getUiFont(Font.BOLD, 0));
tb.setBorder(new LineBorder(getBorder(theme), 1)); tb.setBorder(new LineBorder(getBorder(theme), 1));
} }
} else if (comp instanceof Container) { } else if (comp instanceof Container) {
@@ -17,6 +17,7 @@ public class UntrustedCertificateDialog extends JDialog {
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) { public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
super(parent, "Untrusted SSL/TLS Certificate", true); super(parent, "Untrusted SSL/TLS Certificate", true);
AppIcon.applyTo(this);
buildUI(host, port, chain, exception); buildUI(host, port, chain, exception);
pack(); pack();
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
@@ -31,18 +32,20 @@ public class UntrustedCertificateDialog extends JDialog {
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setOpaque(false); headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️"); JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28)); iconLabel.putClientProperty("uiFontSizeDelta", 15);
iconLabel.setFont(ThemeManager.getUiFont(Font.PLAIN, 15));
headerPanel.add(iconLabel); headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setOpaque(false); titleBox.setOpaque(false);
JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); titleLabel.putClientProperty("uiFontSizeDelta", 3);
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
titleLabel.setForeground(ThemeManager.getOiaFgWarn()); titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel); titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); 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.setFont(ThemeManager.getUiFont());
subtitleLabel.setForeground(ThemeManager.getFgMuted()); subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel); titleBox.add(subtitleLabel);
headerPanel.add(titleBox); headerPanel.add(titleBox);
@@ -69,7 +72,7 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString()); JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false); detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); detailsArea.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(detailsArea); ThemeManager.styleTextArea(detailsArea);
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@@ -91,7 +94,7 @@ public class UntrustedCertificateDialog extends JDialog {
JButton trustBtn = new JButton("Connect Anyway"); JButton trustBtn = new JButton("Connect Anyway");
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER); ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); trustBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
trustBtn.addActionListener(e -> { trustBtn.addActionListener(e -> {
accepted = true; accepted = true;
dispose(); dispose();
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="124 738 320 320"
width="320"
height="320"
xml:space="preserve"
id="svg_j3270_icon"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="124" y="738" width="320" height="320" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+146
View File
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="144 814 280 169"
width="280"
height="169"
xml:space="preserve"
id="svg_lib3270j_cropped"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="144" y="814" width="280" height="169" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -0,0 +1,57 @@
package haus.nightmare.j3270;
import haus.nightmare.j3270.ui.AppIcon;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AppIconTest {
@Test
public void testIconsAreLoaded() {
List<Image> icons = AppIcon.getAppIcons();
assertNotNull(icons, "Icons list must not be null");
assertFalse(icons.isEmpty(), "At least one icon image must be loaded");
// Verify standard sizes are present
int[] expectedSizes = {16, 24, 32, 48, 64, 128, 256, 512};
for (int expectedSize : expectedSizes) {
boolean found = icons.stream().anyMatch(img -> img.getWidth(null) == expectedSize && img.getHeight(null) == expectedSize);
assertTrue(found, "Expected icon of size " + expectedSize + "x" + expectedSize + " was not found");
}
}
@Test
public void testDefaultIcon() {
Image defaultIcon = AppIcon.getDefaultIcon();
assertNotNull(defaultIcon, "Default icon must not be null");
assertEquals(256, defaultIcon.getWidth(null), "Default icon width should be 256");
assertEquals(256, defaultIcon.getHeight(null), "Default icon height should be 256");
}
@Test
public void testApplyToInHeadless() {
// Must execute without exception even in headless environment
assertDoesNotThrow(() -> AppIcon.applyTo(null));
if (!GraphicsEnvironment.isHeadless()) {
JFrame frame = new JFrame();
assertDoesNotThrow(() -> AppIcon.applyTo(frame));
frame.dispose();
}
}
@Test
public void testSvgResourcesExist() {
URL svgUrl = getClass().getResource("/icons/lib3270j.svg");
assertNotNull(svgUrl, "lib3270j.svg must exist in /icons/");
URL iconSvgUrl = getClass().getResource("/icons/j3270_icon.svg");
assertNotNull(iconSvgUrl, "j3270_icon.svg must exist in /icons/");
}
}
@@ -0,0 +1,93 @@
package haus.nightmare.j3270;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
public class LoggingConfigurationTest {
private PrintStream originalOut;
private PrintStream originalErr;
@BeforeEach
public void setUp() {
originalOut = System.out;
originalErr = System.err;
}
@AfterEach
public void tearDown() {
System.setOut(originalOut);
System.setErr(originalErr);
// Ensure all handlers are closed and logging reset
J3270App.configureLogging(false);
}
@Test
public void testLoggingDisabledWhenDebugFalse() {
File logFile = new File("j3270.log");
if (logFile.exists()) {
logFile.delete();
}
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
System.setErr(new PrintStream(errContent));
J3270App.configureLogging(false);
// Root logger should have no handlers attached
Logger rootLogger = Logger.getLogger("");
assertEquals(0, rootLogger.getHandlers().length, "Root logger should have no handlers when debug is disabled");
assertEquals(Level.OFF, rootLogger.getLevel(), "Root logger level should be OFF when debug is disabled");
// Application loggers should be OFF
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare").getLevel());
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.j3270").getLevel());
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
// Emit log records at all levels
Logger appLogger = Logger.getLogger("haus.nightmare.j3270.J3270App");
appLogger.severe("Test SEVERE message");
appLogger.warning("Test WARNING message");
appLogger.info("Test INFO message");
appLogger.fine("Test FINE message");
// Verify nothing was written to stdout or stderr
assertEquals(0, outContent.size(), "Standard output should be empty when debug is disabled");
assertEquals(0, errContent.size(), "Standard error should be empty when debug is disabled");
// Verify j3270.log was NOT created
assertFalse(logFile.exists(), "j3270.log should NOT be created when debug is disabled");
}
@Test
public void testLoggingEnabledWhenDebugTrue() {
J3270App.configureLogging(true);
Logger rootLogger = Logger.getLogger("");
Handler[] handlers = rootLogger.getHandlers();
assertTrue(handlers.length >= 2, "Root logger should have at least ConsoleHandler and FileHandler when debug is enabled");
assertEquals(Level.ALL, rootLogger.getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare").getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.j3270").getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
File logFile = new File("j3270.log");
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled");
// Clean up
J3270App.configureLogging(false);
}
}
@@ -0,0 +1,102 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.lib3270j.ft.FTConfig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.HeadlessException;
import static org.junit.jupiter.api.Assertions.*;
public class FileTransferSessionAndCmsTest {
@BeforeEach
@AfterEach
public void cleanup() {
FileTransferDialog.resetSessionState();
}
@Test
public void testCmsFilenamePeriodReplacement() {
// Test replacing periods with spaces for CMS
assertEquals("fscms exec", FileTransferDialog.formatHostFilename("fscms.exec", FTConfig.HostType.CMS));
assertEquals("fscms exec", FileTransferDialog.formatHostFilename("/var/tmp/fscms.exec", FTConfig.HostType.CMS));
assertEquals("test script exec", FileTransferDialog.formatHostFilename("test.script.exec", FTConfig.HostType.CMS));
assertEquals("noextension", FileTransferDialog.formatHostFilename("noextension", FTConfig.HostType.CMS));
// TSO and CICS should retain periods
assertEquals("fscms.exec", FileTransferDialog.formatHostFilename("fscms.exec", FTConfig.HostType.TSO));
assertEquals("my.dataset.name", FileTransferDialog.formatHostFilename("/path/to/my.dataset.name", FTConfig.HostType.TSO));
assertEquals("cics.file", FileTransferDialog.formatHostFilename("cics.file", FTConfig.HostType.CICS));
}
@Test
public void testSessionStateRememberedInMemory() {
assertNull(FileTransferDialog.getLastTransferState());
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.CMS;
state.isSend = true;
state.localFile = "/tmp/fscms.exec";
state.hostFile = "fscms exec";
state.isAscii = true;
state.mtu = 8192;
state.crFlag = false;
state.remapFlag = false;
state.append = true;
state.overwrite = true;
state.options = "CLEAR";
FileTransferDialog.setLastTransferState(state);
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
assertNotNull(retrieved);
assertEquals(FTConfig.HostType.CMS, retrieved.hostType);
assertTrue(retrieved.isSend);
assertEquals("/tmp/fscms.exec", retrieved.localFile);
assertEquals("fscms exec", retrieved.hostFile);
assertTrue(retrieved.isAscii);
assertEquals(8192, retrieved.mtu);
assertFalse(retrieved.crFlag);
assertFalse(retrieved.remapFlag);
assertTrue(retrieved.append);
assertTrue(retrieved.overwrite);
assertEquals("CLEAR", retrieved.options);
}
@Test
public void testDialogPrefillingFromSessionState() {
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.CMS;
state.isSend = true;
state.localFile = "/tmp/fscms.exec";
state.hostFile = "fscms exec";
state.isAscii = false; // Binary
state.mtu = 4096;
state.crFlag = true;
state.remapFlag = true;
state.append = false;
state.overwrite = true;
state.options = "ASCII";
FileTransferDialog.setLastTransferState(state);
try {
FileTransferDialog dialog = new FileTransferDialog(null, null);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem());
assertTrue(dialog.getSendRadio().isSelected());
assertFalse(dialog.getReceiveRadio().isSelected());
assertEquals("/tmp/fscms.exec", dialog.getLocalFileField().getText());
assertEquals("fscms exec", dialog.getHostFileField().getText());
assertFalse(dialog.getAsciiRadio().isSelected());
assertTrue(dialog.getBinaryRadio().isSelected());
assertEquals(4096, dialog.getMtuCombo().getSelectedItem());
assertTrue(dialog.getOverwriteCheck().isSelected());
assertEquals("ASCII", dialog.getOptionsField().getText());
dialog.dispose();
} catch (HeadlessException e) {
// Headless environment; verified through state retention
}
}
}
@@ -0,0 +1,343 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.j3270.ui.ConnectDialog;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class HostStorageAndProxyTest {
private List<SavedHost> originalHosts;
@BeforeEach
public void setUp() {
originalHosts = HostStorage.getSavedHosts();
HostStorage.clearAll();
}
@AfterEach
public void tearDown() {
HostStorage.clearAll();
HostStorage.saveAll(originalHosts);
}
@Test
@DisplayName("SavedHost correctly models per-connection proxy and converts to ConnectionConfig")
public void testSavedHostProxyToConnectionConfig() {
// 1. Host with SOCKS5 proxy and auth
SavedHost host1 = new SavedHost(
UUID.randomUUID().toString(),
"Prod Mainframe",
"mvs.corp.local",
2323,
4,
43,
80,
"TSO01",
false,
"TSO",
true,
true,
true,
"BOTH",
"037",
"SOCKS5",
"proxy.internal.corp",
1080,
"admin",
"secret123"
);
ConnectionConfig cfg1 = host1.toConnectionConfig();
assertEquals("mvs.corp.local", cfg1.getHost());
assertEquals(2323, cfg1.getPort());
assertTrue(cfg1.isUseTls());
assertEquals("TSO01", cfg1.getLuName());
assertEquals(ConnectionConfig.ProxyType.SOCKS5, cfg1.getProxyType());
assertEquals("proxy.internal.corp", cfg1.getProxyHost());
assertEquals(1080, cfg1.getProxyPort());
assertEquals("admin", cfg1.getProxyUsername());
assertEquals("secret123", cfg1.getProxyPassword());
// 2. Host with direct connection (NONE proxy)
SavedHost host2 = new SavedHost(
UUID.randomUUID().toString(),
"Local VM370",
"127.0.0.1",
23,
2,
24,
80,
"",
false,
"CMS",
false,
false,
false,
"BOTH",
"037",
"NONE",
"",
0,
"",
""
);
ConnectionConfig cfg2 = host2.toConnectionConfig();
assertEquals("127.0.0.1", cfg2.getHost());
assertEquals(23, cfg2.getPort());
assertFalse(cfg2.isUseTls());
assertEquals(ConnectionConfig.ProxyType.NONE, cfg2.getProxyType());
assertNull(cfg2.getProxyHost());
}
@Test
@DisplayName("HostStorage persists and restores multiple host profiles with isolated per-connection proxies")
public void testHostStoragePersistenceAndIsolation() {
SavedHost h1 = new SavedHost(
"id-1",
"System A (SOCKS5)",
"sysa.corp",
23,
4,
62,
160,
"",
false,
"TSO",
false,
true,
true,
"BOTH",
"037",
"SOCKS5",
"socks.gateway.net",
1080,
"alice",
"passA"
);
SavedHost h2 = new SavedHost(
"id-2",
"System B (HTTP CONNECT)",
"sysb.dmz",
992,
5,
27,
132,
"LU99",
false,
"TSO",
true,
true,
true,
"BOTH",
"1047",
"HTTP",
"squid.proxy.org",
3128,
"bob",
"passB"
);
SavedHost h3 = new SavedHost(
"id-3",
"System C (Direct)",
"sysc.lan",
23,
2,
24,
80,
"",
true, // autoConnect
"CMS",
false,
true,
true,
"BOTH",
"500",
"NONE",
"",
0,
"",
""
);
HostStorage.saveHost(h1);
HostStorage.saveHost(h2);
HostStorage.saveHost(h3);
List<SavedHost> loaded = HostStorage.getSavedHosts();
assertEquals(3, loaded.size());
SavedHost loadedA = HostStorage.findById("id-1");
assertNotNull(loadedA);
assertEquals("System A (SOCKS5)", loadedA.getName());
assertEquals("SOCKS5", loadedA.getProxyType());
assertEquals("socks.gateway.net", loadedA.getProxyHost());
assertEquals(1080, loadedA.getProxyPort());
assertEquals("alice", loadedA.getProxyUsername());
assertEquals("passA", loadedA.getProxyPassword());
SavedHost loadedB = HostStorage.findById("id-2");
assertNotNull(loadedB);
assertEquals("HTTP", loadedB.getProxyType());
assertEquals("squid.proxy.org", loadedB.getProxyHost());
assertEquals(3128, loadedB.getProxyPort());
assertEquals("bob", loadedB.getProxyUsername());
assertEquals("passB", loadedB.getProxyPassword());
SavedHost loadedC = HostStorage.findById("id-3");
assertNotNull(loadedC);
assertEquals("NONE", loadedC.getProxyType());
assertEquals("", loadedC.getProxyHost());
assertTrue(loadedC.isAutoConnect());
// Verify getAutoConnectHost returns System C with its own connection settings
SavedHost auto = HostStorage.getAutoConnectHost();
assertNotNull(auto);
assertEquals("id-3", auto.getId());
assertEquals(ConnectionConfig.ProxyType.NONE, auto.toConnectionConfig().getProxyType());
// Verify deleteHost
HostStorage.deleteHost("id-2");
assertEquals(2, HostStorage.getSavedHosts().size());
assertNull(HostStorage.findById("id-2"));
}
@Test
@DisplayName("HostStorage JSON matches a3270 format and parses a3270 JSON strings")
public void testA3270JsonFormatCompatibility() {
// String representative of a3270 saved_hosts preference JSON
String a3270Json = "[\n" +
" {\n" +
" \"id\": \"android-uuid-1\",\n" +
" \"name\": \"Dallas Mainframe\",\n" +
" \"host\": \"dallas.ibm.com\",\n" +
" \"port\": 2323,\n" +
" \"model\": 4,\n" +
" \"dynamicRows\": 62,\n" +
" \"dynamicCols\": 160,\n" +
" \"luName\": \"DAL01\",\n" +
" \"autoConnect\": false,\n" +
" \"hostType\": \"TSO\",\n" +
" \"useTls\": true,\n" +
" \"tlsVerifyCert\": false,\n" +
" \"tn3270e\": true,\n" +
" \"graphicsMode\": \"BOTH\",\n" +
" \"codePage\": \"037\",\n" +
" \"proxyType\": \"SOCKS5\",\n" +
" \"proxyHost\": \"10.20.30.40\",\n" +
" \"proxyPort\": 9050,\n" +
" \"proxyUsername\": \"dallasuser\",\n" +
" \"proxyPassword\": \"dallaspass\"\n" +
" }\n" +
"]";
List<SavedHost> parsed = HostStorage.parseHostsJson(a3270Json);
assertEquals(1, parsed.size());
SavedHost h = parsed.get(0);
assertEquals("android-uuid-1", h.getId());
assertEquals("Dallas Mainframe", h.getName());
assertEquals("dallas.ibm.com", h.getHost());
assertEquals(2323, h.getPort());
assertEquals(4, h.getModel());
assertTrue(h.isUseTls());
assertFalse(h.isTlsVerifyCert());
assertEquals("SOCKS5", h.getProxyType());
assertEquals("10.20.30.40", h.getProxyHost());
assertEquals(9050, h.getProxyPort());
assertEquals("dallasuser", h.getProxyUsername());
assertEquals("dallaspass", h.getProxyPassword());
// Roundtrip serialization
String serialized = HostStorage.serializeHostsJson(parsed);
List<SavedHost> roundtripped = HostStorage.parseHostsJson(serialized);
assertEquals(1, roundtripped.size());
SavedHost r = roundtripped.get(0);
assertEquals(h.getId(), r.getId());
assertEquals(h.getProxyType(), r.getProxyType());
assertEquals(h.getProxyHost(), r.getProxyHost());
assertEquals(h.getProxyPort(), r.getProxyPort());
assertEquals(h.getProxyUsername(), r.getProxyUsername());
assertEquals(h.getProxyPassword(), r.getProxyPassword());
}
@Test
@DisplayName("ConnectDialog loads profile proxy and applies per-connection proxy to ConnectionConfig")
public void testConnectDialogProxyHandling() {
if (!GraphicsEnvironment.isHeadless()) {
ConnectDialog dialog = new ConnectDialog((Frame) null);
dialog.setInitialHost("test.mainframe.com");
dialog.setInitialPort(23);
dialog.setInitialProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", 1080, "testuser", "testpass");
assertEquals(ConnectionConfig.ProxyType.SOCKS5, dialog.getSelectedProxyType());
assertEquals("127.0.0.1", dialog.getProxyHost());
assertEquals(1080, dialog.getProxyPort());
assertEquals("testuser", dialog.getProxyUsername());
assertEquals("testpass", dialog.getProxyPassword());
// Loading a profile updates dialog proxy fields
SavedHost profile = new SavedHost(
"p1", "Profile 1", "prod.ibm.net", 992, 4, 43, 80, "", false,
"TSO", true, true, true, "BOTH", "037",
"HTTP", "proxy.corp.net", 8080, "corpuser", "corppass"
);
dialog.loadProfile(profile);
assertEquals(ConnectionConfig.ProxyType.HTTP, dialog.getSelectedProxyType());
assertEquals("proxy.corp.net", dialog.getProxyHost());
assertEquals(8080, dialog.getProxyPort());
assertEquals("corpuser", dialog.getProxyUsername());
assertEquals("corppass", dialog.getProxyPassword());
dialog.dispose();
}
}
@Test
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
public void testSavedHostGraphicsModeSerializationRoundtrip() {
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
SavedHost host = new SavedHost(
UUID.randomUUID().toString(),
"Gfx " + gm.name(),
"test.host.com",
23,
4,
24,
80,
"",
false,
"TSO",
false,
false,
true,
gm.name(),
"037",
"NONE",
"",
0,
"",
""
);
String json = HostStorage.serializeHostsJson(List.of(host));
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
assertEquals(1, parsed.size());
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
"ConnectionConfig must reflect GraphicsMode." + gm.name());
}
}
}
@@ -0,0 +1,298 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.*;
public class BehaviorSettingsTest {
private boolean origAutoReconnect;
private int origReconnectMaxRetries;
private boolean origInputMask;
private String origInputMaskChar;
private boolean origBlockPaste;
@BeforeEach
public void setUp() {
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
origInputMask = Settings.getInputMask();
origInputMaskChar = Settings.getInputMaskChar();
origBlockPaste = Settings.getBlockPaste();
}
@AfterEach
public void tearDown() {
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
Settings.setInputMask(origInputMask);
Settings.setInputMaskChar(origInputMaskChar);
Settings.setBlockPaste(origBlockPaste);
}
@Test
@DisplayName("Auto-reconnect settings get/set and alias consistency")
public void testAutoReconnectSettingsPersistence() {
Settings.setAutoConnectAutoReconnect(true);
assertTrue(Settings.getAutoConnectAutoReconnect());
assertTrue(Settings.getAutoReconnect());
Settings.setAutoReconnect(false);
assertFalse(Settings.getAutoConnectAutoReconnect());
assertFalse(Settings.getAutoReconnect());
Settings.setAutoConnectReconnectMaxRetries(8);
assertEquals(8, Settings.getAutoConnectReconnectMaxRetries());
}
@Test
@DisplayName("Input mask settings get/set and defaults")
public void testInputMaskSettingsPersistence() {
Settings.setInputMask(false);
assertFalse(Settings.getInputMask());
assertFalse(Settings.getInputMaskEnabled());
Settings.setInputMaskEnabled(true);
assertTrue(Settings.getInputMask());
assertTrue(Settings.getInputMaskEnabled());
Settings.setInputMaskChar("#");
assertEquals("#", Settings.getInputMaskChar());
Settings.setInputMaskChar("*");
assertEquals("*", Settings.getInputMaskChar());
// Empty string should fall back to '*'
Settings.setInputMaskChar("");
assertEquals("*", Settings.getInputMaskChar());
}
@Test
@DisplayName("Block paste setting defaults to true and binds to SettingsDialog")
public void testBlockPasteSettingAndDialogBinding() throws Exception {
// Default must be true
assertTrue(Settings.getBlockPaste(), "Settings.getBlockPaste() should default to true");
Settings.setBlockPaste(false);
assertFalse(Settings.getBlockPaste());
Settings.setBlockPaste(true);
assertTrue(Settings.getBlockPaste());
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
Field blockPasteField = SettingsDialog.class.getDeclaredField("blockPasteCheck");
blockPasteField.setAccessible(true);
JCheckBox blockPasteCheck = (JCheckBox) blockPasteField.get(dialog);
assertNotNull(blockPasteCheck, "blockPasteCheck must exist in SettingsDialog");
assertEquals("Block paste", blockPasteCheck.getText());
assertEquals(Settings.getBlockPaste(), blockPasteCheck.isSelected());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("INI export and load preserves autoReconnect and inputMask")
public void testIniExportAndLoad() throws Exception {
Settings.setAutoConnectAutoReconnect(true);
Settings.setAutoConnectReconnectMaxRetries(12);
Settings.setInputMask(false);
Settings.setInputMaskChar("@");
File tempFile = File.createTempFile("j3270_behavior_test", ".ini");
tempFile.deleteOnExit();
Settings.exportToIniFile(tempFile.getAbsolutePath());
// Reset to different values
Settings.setAutoConnectAutoReconnect(false);
Settings.setAutoConnectReconnectMaxRetries(3);
Settings.setInputMask(true);
Settings.setInputMaskChar("*");
// Load back from INI
Settings.loadFromIniFile(tempFile.getAbsolutePath());
assertTrue(Settings.getAutoConnectAutoReconnect());
assertEquals(12, Settings.getAutoConnectReconnectMaxRetries());
assertFalse(Settings.getInputMask());
assertEquals("@", Settings.getInputMaskChar());
tempFile.delete();
}
@Test
@DisplayName("TerminalPanel reloads input mask settings correctly")
public void testTerminalPanelInputMaskReload() {
try {
TerminalPanel panel = new TerminalPanel();
Settings.setInputMask(false);
Settings.setInputMaskChar("$");
panel.reloadSettings();
assertFalse(panel.isInputMaskEnabled());
assertEquals("$", panel.getInputMaskChar());
panel.setInputMaskEnabled(true);
assertTrue(panel.isInputMaskEnabled());
panel.setInputMaskChar("#");
assertEquals("#", panel.getInputMaskChar());
} catch (HeadlessException ignored) {
// Safe fallback for headless runner
}
}
@Test
@DisplayName("SettingsDialog contains Auto-Reconnect checkbox and Input Mask controls in Behavior panel")
public void testSettingsDialogBehaviorControls() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// In automated/headless environments, JFrame cannot be initialized
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Access private fields in SettingsDialog to verify component bindings
Field autoRecField = SettingsDialog.class.getDeclaredField("autoReconnectCheck");
autoRecField.setAccessible(true);
JCheckBox autoReconnectCheck = (JCheckBox) autoRecField.get(dialog);
assertNotNull(autoReconnectCheck, "autoReconnectCheck must exist in SettingsDialog");
assertEquals("Auto-Reconnect on Disconnect", autoReconnectCheck.getText());
assertEquals(Settings.getAutoConnectAutoReconnect(), autoReconnectCheck.isSelected());
Field inputMaskCheckField = SettingsDialog.class.getDeclaredField("inputMaskCheck");
inputMaskCheckField.setAccessible(true);
JCheckBox inputMaskCheck = (JCheckBox) inputMaskCheckField.get(dialog);
assertNotNull(inputMaskCheck, "inputMaskCheck must exist in SettingsDialog");
assertEquals(Settings.getInputMask(), inputMaskCheck.isSelected());
Field inputMaskCharField = SettingsDialog.class.getDeclaredField("inputMaskCharField");
inputMaskCharField.setAccessible(true);
JTextField maskCharField = (JTextField) inputMaskCharField.get(dialog);
assertNotNull(maskCharField, "inputMaskCharField must exist in SettingsDialog");
assertEquals(Settings.getInputMaskChar(), maskCharField.getText());
assertEquals(inputMaskCheck.isSelected(), maskCharField.isEnabled());
// Test interaction: unchecking inputMask disables character field
inputMaskCheck.setSelected(false);
for (java.awt.event.ActionListener al : inputMaskCheck.getActionListeners()) {
al.actionPerformed(new java.awt.event.ActionEvent(inputMaskCheck, java.awt.event.ActionEvent.ACTION_PERFORMED, ""));
}
assertFalse(maskCharField.isEnabled());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("SettingsDialog tabs are organized into Appearance, Behavior, Connection, and Entry Assist")
public void testSettingsDialogReorganizedTabsAndGrouping() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Find top-level JTabbedPane
JTabbedPane mainTabs = null;
for (Component c : dialog.getContentPane().getComponents()) {
if (c instanceof JTabbedPane) {
mainTabs = (JTabbedPane) c;
break;
}
}
assertNotNull(mainTabs, "Main JTabbedPane must exist");
assertEquals(4, mainTabs.getTabCount(), "Must have 4 main tabs");
assertEquals("Appearance", mainTabs.getTitleAt(0));
assertEquals("Behavior", mainTabs.getTitleAt(1));
assertEquals("Connection", mainTabs.getTitleAt(2));
assertEquals("Entry Assist", mainTabs.getTitleAt(3));
// Verify Appearance subtabs: Display & Font, Colors
Component appComp = mainTabs.getComponentAt(0);
assertTrue(appComp instanceof JPanel);
JTabbedPane appTabs = findTabbedPane((JPanel) appComp);
assertNotNull(appTabs, "Appearance panel must have subtabs");
assertEquals(2, appTabs.getTabCount());
assertEquals("Display & Font", appTabs.getTitleAt(0));
assertEquals("Colors", appTabs.getTitleAt(1));
// Verify Behavior subtabs: General, Keymap
Component behComp = mainTabs.getComponentAt(1);
assertTrue(behComp instanceof JPanel);
JTabbedPane behTabs = findTabbedPane((JPanel) behComp);
assertNotNull(behTabs, "Behavior panel must have subtabs");
assertEquals(2, behTabs.getTabCount());
assertEquals("General", behTabs.getTitleAt(0));
assertEquals("Keymap", behTabs.getTitleAt(1));
// Verify controls exist in SettingsDialog
Field cursorStyleField = SettingsDialog.class.getDeclaredField("cursorStyleBox");
cursorStyleField.setAccessible(true);
assertNotNull(cursorStyleField.get(dialog), "cursorStyleBox must exist");
Field crosshairField = SettingsDialog.class.getDeclaredField("crosshairRulerCheck");
crosshairField.setAccessible(true);
assertNotNull(crosshairField.get(dialog), "crosshairRulerCheck must exist");
Field fourColorField = SettingsDialog.class.getDeclaredField("fourColorOverrideCheck");
fourColorField.setAccessible(true);
assertNotNull(fourColorField.get(dialog), "fourColorOverrideCheck must exist");
Field numericLockField = SettingsDialog.class.getDeclaredField("numericFieldLockCheck");
numericLockField.setAccessible(true);
assertNotNull(numericLockField.get(dialog), "numericFieldLockCheck must exist");
Field autoSkipField = SettingsDialog.class.getDeclaredField("autoSkipCheck");
autoSkipField.setAccessible(true);
assertNotNull(autoSkipField.get(dialog), "autoSkipCheck must exist");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
private static JTabbedPane findTabbedPane(Container container) {
for (Component c : container.getComponents()) {
if (c instanceof JTabbedPane) {
return (JTabbedPane) c;
}
if (c instanceof Container) {
JTabbedPane nested = findTabbedPane((Container) c);
if (nested != null) return nested;
}
}
return null;
}
}
@@ -0,0 +1,209 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.Dimension;
import java.awt.HeadlessException;
import java.awt.event.ComponentEvent;
import static org.junit.jupiter.api.Assertions.*;
public class FontSettingsPersistenceTest {
private int originalFontSize;
private String originalFontFamily;
private int originalUiFontSize;
private String originalUiFontFamily;
@BeforeEach
public void setup() {
originalFontSize = Settings.getFontSize();
originalFontFamily = Settings.getFontFamily();
originalUiFontSize = Settings.getUiFontSize();
originalUiFontFamily = Settings.getUiFontFamily();
}
@AfterEach
public void tearDown() {
Settings.setFontSize(originalFontSize);
Settings.setFontFamily(originalFontFamily);
Settings.setUiFontSize(originalUiFontSize);
Settings.setUiFontFamily(originalUiFontFamily);
}
@Test
public void testSettingsPersistence() {
Settings.setFontSize(22);
Settings.setFontFamily("Monospaced");
assertEquals(22, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
Settings.setFontSize(18);
assertEquals(18, Settings.getFontSize());
}
@Test
public void testUiFontSettingsIndependence() {
// Configure terminal fonts
Settings.setFontFamily("Courier");
Settings.setFontSize(20);
// Configure UI fonts
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(15);
// Verify independent getters
assertEquals("Courier", Settings.getFontFamily());
assertEquals(20, Settings.getFontSize());
assertEquals("Dialog", Settings.getUiFontFamily());
assertEquals(15, Settings.getUiFontSize());
// Change UI font and ensure terminal font is untouched
Settings.setUiFontSize(18);
Settings.setUiFontFamily("SansSerif");
assertEquals("Courier", Settings.getFontFamily(), "Terminal font family must not change when UI font changes");
assertEquals(20, Settings.getFontSize(), "Terminal font size must not change when UI font changes");
assertEquals("SansSerif", Settings.getUiFontFamily());
assertEquals(18, Settings.getUiFontSize());
// Change terminal font and ensure UI font is untouched
Settings.setFontSize(24);
Settings.setFontFamily("IBM 3270");
assertEquals("SansSerif", Settings.getUiFontFamily(), "UI font family must not change when terminal font changes");
assertEquals(18, Settings.getUiFontSize(), "UI font size must not change when terminal font changes");
assertEquals("IBM 3270", Settings.getFontFamily());
assertEquals(24, Settings.getFontSize());
}
@Test
public void testUiFontIniImportExport() throws Exception {
java.io.File tempIni = java.io.File.createTempFile("j3270_ui_font_test", ".ini");
tempIni.deleteOnExit();
try {
try (java.io.PrintWriter pw = new java.io.PrintWriter(tempIni)) {
pw.println("[appearance]");
pw.println("uiFontFamily = Dialog");
pw.println("uiFontSize = 16");
pw.println("fontFamily = Monospaced");
pw.println("fontSize = 22");
}
Settings.loadFromIniFile(tempIni.getAbsolutePath());
assertEquals("Dialog", Settings.getUiFontFamily());
assertEquals(16, Settings.getUiFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
assertEquals(22, Settings.getFontSize());
// Export to another file and check contents
java.io.File exportIni = java.io.File.createTempFile("j3270_ui_font_export", ".ini");
exportIni.deleteOnExit();
Settings.exportToIniFile(exportIni.getAbsolutePath());
String exported = new String(java.nio.file.Files.readAllBytes(exportIni.toPath()));
assertTrue(exported.contains("uiFontFamily = Dialog"), "Exported INI must contain uiFontFamily");
assertTrue(exported.contains("uiFontSize = 16"), "Exported INI must contain uiFontSize");
assertTrue(exported.contains("fontFamily = Monospaced"), "Exported INI must contain terminal fontFamily");
assertTrue(exported.contains("fontSize = 22"), "Exported INI must contain terminal fontSize");
} finally {
tempIni.delete();
}
}
@Test
public void testTerminalPanelResizeDoesNotResetFontSize() {
try {
Settings.setFontSize(24);
Settings.setFontFamily("Monospaced");
TerminalPanel panel = new TerminalPanel();
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
// Simulate window / component resize events
panel.setSize(new Dimension(800, 600));
ComponentEvent resizeEvent1 = new ComponentEvent(panel, ComponentEvent.COMPONENT_RESIZED);
for (java.awt.event.ComponentListener cl : panel.getComponentListeners()) {
cl.componentResized(resizeEvent1);
}
// Verify font size and family did NOT reset
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
// Simulate another resize to smaller dimension
panel.setSize(new Dimension(400, 300));
ComponentEvent resizeEvent2 = new ComponentEvent(panel, ComponentEvent.COMPONENT_RESIZED);
for (java.awt.event.ComponentListener cl : panel.getComponentListeners()) {
cl.componentResized(resizeEvent2);
}
assertEquals(24, panel.getFontSize());
assertEquals(24, Settings.getFontSize());
assertEquals("Monospaced", Settings.getFontFamily());
panel.dispose();
} catch (HeadlessException e) {
// In headless environment without display, Settings persistence is verified above
}
}
@Test
public void testSettingsDialogApplyDoesNotIncrementFontSize() throws Exception {
try {
Settings.setUiFontSize(16);
Settings.setFontSize(22);
Settings.setUiFontFamily("Dialog");
Settings.setFontFamily("Monospaced");
ThemeManager.applyUIManagerDefaults(ThemeManager.getTheme());
SettingsDialog dialog = new SettingsDialog(null);
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
applyMethod.setAccessible(true);
// Find all JLabels in SettingsDialog
java.util.List<javax.swing.JLabel> labels = new java.util.ArrayList<>();
findLabelsRecursively(dialog.getContentPane(), labels);
assertFalse(labels.isEmpty(), "SettingsDialog must contain labels");
for (javax.swing.JLabel label : labels) {
assertEquals(16, label.getFont().getSize(),
"Label '" + label.getText() + "' must initially have size 16");
}
// Click Apply multiple times
for (int click = 1; click <= 5; click++) {
applyMethod.invoke(dialog);
assertEquals(16, Settings.getUiFontSize(), "Settings uiFontSize must stay 16");
assertEquals(22, Settings.getFontSize(), "Settings fontSize must stay 22");
for (javax.swing.JLabel label : labels) {
assertEquals(16, label.getFont().getSize(),
"Label '" + label.getText() + "' must remain at size 16 after click #" + click);
}
}
dialog.dispose();
} catch (HeadlessException e) {
// Ignored in headless environment if dialog creation fails
}
}
private void findLabelsRecursively(java.awt.Container container, java.util.List<javax.swing.JLabel> labels) {
for (java.awt.Component c : container.getComponents()) {
if (c instanceof javax.swing.JLabel) {
labels.add((javax.swing.JLabel) c);
} else if (c instanceof java.awt.Container) {
findLabelsRecursively((java.awt.Container) c, labels);
}
}
}
}
@@ -0,0 +1,192 @@
package haus.nightmare.j3270.ui;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for HotspotEngine (Phase 25.2).
* Verifies regex detection of URLs, PF keys, Enter, and menu choices,
* hit-testing, action strings, and word boundary isolation.
*/
public class HotspotEngineTest {
private HotspotEngine engine;
@BeforeEach
public void setUp() {
engine = new HotspotEngine();
}
@Test
@DisplayName("Detects HTTP, HTTPS, FTP, and WWW URLs with action strings")
public void testUrlDetection() {
String screen = "Visit https://ibm.com/support or http://example.org:8080/path?q=1 for help.\n" +
"Download from ftp://ftp.archive.org/pub/files or browse www.google.com.";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
assertEquals(4, hotspots.size());
HotspotEngine.Hotspot h0 = hotspots.get(0);
assertEquals(HotspotEngine.HotspotType.URL, h0.getType());
assertEquals("https://ibm.com/support", h0.getDisplayText());
assertEquals("https://ibm.com/support", h0.getAction());
assertEquals(0, h0.getStartRow());
assertEquals(6, h0.getStartCol());
assertTrue(h0.contains(0, 10));
assertFalse(h0.contains(0, 5));
HotspotEngine.Hotspot h1 = hotspots.get(1);
assertEquals("http://example.org:8080/path?q=1", h1.getDisplayText());
assertEquals("http://example.org:8080/path?q=1", h1.getAction());
HotspotEngine.Hotspot h2 = hotspots.get(2);
assertEquals("ftp://ftp.archive.org/pub/files", h2.getDisplayText());
assertEquals("ftp://ftp.archive.org/pub/files", h2.getAction());
HotspotEngine.Hotspot h3 = hotspots.get(3);
assertEquals("www.google.com", h3.getDisplayText());
assertEquals("http://www.google.com", h3.getAction(), "www. URLs should have http:// prepended for action");
}
@Test
@DisplayName("Detects PF1..PF24, F1..F24, PA1..PA3, and [ENTER] function keys")
public void testFunctionKeyDetection() {
String screen = "PF1=Help PF3=Exit PF7=Up PF8=Down PF12=Cancel\n" +
"F1=Help F12=Cancel PA1=Abort [ENTER]=Continue";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
assertEquals(9, hotspots.size());
// PF1
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(0).getType());
assertEquals("PF1", hotspots.get(0).getDisplayText());
assertEquals("[pf1]", hotspots.get(0).getAction());
// PF3
assertEquals("PF3", hotspots.get(1).getDisplayText());
assertEquals("[pf3]", hotspots.get(1).getAction());
// PF7
assertEquals("PF7", hotspots.get(2).getDisplayText());
assertEquals("[pf7]", hotspots.get(2).getAction());
// PF8
assertEquals("PF8", hotspots.get(3).getDisplayText());
assertEquals("[pf8]", hotspots.get(3).getAction());
// PF12
assertEquals("PF12", hotspots.get(4).getDisplayText());
assertEquals("[pf12]", hotspots.get(4).getAction());
// F1 -> [pf1]
assertEquals("F1", hotspots.get(5).getDisplayText());
assertEquals("[pf1]", hotspots.get(5).getAction());
// F12 -> [pf12]
assertEquals("F12", hotspots.get(6).getDisplayText());
assertEquals("[pf12]", hotspots.get(6).getAction());
// PA1 -> [pa1]
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(7).getType());
assertEquals("PA1", hotspots.get(7).getDisplayText());
assertEquals("[pa1]", hotspots.get(7).getAction());
// [ENTER] -> [enter]
assertEquals(HotspotEngine.HotspotType.ENTER, hotspots.get(8).getType());
assertEquals("[ENTER]", hotspots.get(8).getDisplayText());
assertEquals("[enter]", hotspots.get(8).getAction());
}
@Test
@DisplayName("Isolates function keys with strict word boundaries to avoid false positives")
public void testWordBoundaryIsolation() {
String screen = "STAFF1 MEMBER SPF12 FILE TRANSFER PROMPT";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 1, 80);
// Neither STAFF1 nor SPF12 nor TRANSFER should be detected as PF1, F1, or F12
assertEquals(0, hotspots.size(), "Embedded letters in words should not trigger PF key hotspots");
}
@Test
@DisplayName("Detects numbered menu choices and arrow prompts")
public void testMenuChoiceDetection() {
String screen = " 1. User Settings\n" +
" 2. Session Profiles\n" +
"===> 3";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 3, 80);
assertEquals(3, hotspots.size());
HotspotEngine.Hotspot m1 = hotspots.get(0);
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, m1.getType());
assertEquals("1.", m1.getDisplayText());
assertEquals("1[enter]", m1.getAction());
assertEquals(0, m1.getStartRow());
HotspotEngine.Hotspot m2 = hotspots.get(1);
assertEquals("2.", m2.getDisplayText());
assertEquals("2[enter]", m2.getAction());
assertEquals(1, m2.getStartRow());
HotspotEngine.Hotspot m3 = hotspots.get(2);
assertEquals("===> 3", m3.getDisplayText());
assertEquals("3[enter]", m3.getAction());
assertEquals(2, m3.getStartRow());
}
@Test
@DisplayName("Hit-testing finds hotspot under specific coordinate")
public void testFindHotspot() {
String screen = "PF3=Exit https://github.com ===> 1";
engine.scanText(screen, 1, 80);
// Under PF3 (cols 0..2)
HotspotEngine.Hotspot hPf = engine.findHotspot(0, 1);
assertNotNull(hPf);
assertEquals("PF3", hPf.getDisplayText());
// Space between hotspots
assertNull(engine.findHotspot(0, 5));
// Under URL (cols 11..28)
HotspotEngine.Hotspot hUrl = engine.findHotspot(0, 15);
assertNotNull(hUrl);
assertEquals("https://github.com", hUrl.getDisplayText());
// Under prompt (cols 32..37)
HotspotEngine.Hotspot hPrompt = engine.findHotspot(0, 34);
assertNotNull(hPrompt);
assertEquals("===> 1", hPrompt.getDisplayText());
}
@Test
@DisplayName("Toggling engine options enables or disables specific hotspot categories")
public void testTogglingCategories() {
String screen = "PF3=Exit https://example.com 1. Options";
engine.scanText(screen, 1, 80);
assertEquals(3, engine.getHotspots().size());
// Disable URLs
engine.setUrlEnabled(false);
List<HotspotEngine.Hotspot> noUrls = engine.scanText(screen, 1, 80);
assertEquals(2, noUrls.size());
assertFalse(noUrls.stream().anyMatch(h -> h.getType() == HotspotEngine.HotspotType.URL));
// Disable PF keys
engine.setPfKeyEnabled(false);
List<HotspotEngine.Hotspot> menuOnly = engine.scanText(screen, 1, 80);
assertEquals(1, menuOnly.size());
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, menuOnly.get(0).getType());
// Disable menu choices
engine.setMenuChoiceEnabled(false);
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
assertEquals(0, empty.size());
}
}
@@ -0,0 +1,157 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
public class ImportConfigTest {
private String origFontFamily;
private int origFontSize;
private String origHost;
private int origPort;
private boolean origAutoReconnect;
@BeforeEach
public void setUp() {
origFontFamily = Settings.getFontFamily();
origFontSize = Settings.getFontSize();
origHost = Settings.getAutoConnectHost();
origPort = Settings.getAutoConnectPort();
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
}
@AfterEach
public void tearDown() {
Settings.setFontFamily(origFontFamily);
Settings.setFontSize(origFontSize);
Settings.setAutoConnectHost(origHost);
Settings.setAutoConnectPort(origPort);
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
}
@Test
@DisplayName("SettingsDialog contains Import Config button positioned to the left of Export Config")
public void testImportButtonPlacementAndProperties() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JButton btnImport = dialog.getBtnImport();
JButton btnExport = dialog.getBtnExport();
assertNotNull(btnImport, "Import Config button must exist");
assertNotNull(btnExport, "Export Config button must exist");
assertEquals("Import Config...", btnImport.getText());
assertEquals("Export Config...", btnExport.getText());
// Verify btnImport is in the same container and positioned before (to the left of) btnExport
Container parent = btnImport.getParent();
assertNotNull(parent, "Import button must have a parent container");
assertSame(parent, btnExport.getParent(), "Import and Export buttons must share the same parent panel");
Component[] comps = parent.getComponents();
int importIdx = -1;
int exportIdx = -1;
for (int i = 0; i < comps.length; i++) {
if (comps[i] == btnImport) importIdx = i;
if (comps[i] == btnExport) exportIdx = i;
}
assertTrue(importIdx >= 0, "btnImport must be in parent container");
assertTrue(exportIdx >= 0, "btnExport must be in parent container");
assertTrue(importIdx < exportIdx, "btnImport must be placed to the left of btnExport (index " + importIdx + " < " + exportIdx + ")");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("importConfigFile updates Settings and refreshes SettingsDialog controls")
public void testImportConfigFileUpdatesSettingsAndDialog() throws IOException {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
File tempIni = File.createTempFile("j3270_test_import", ".ini");
try {
try (FileWriter writer = new FileWriter(tempIni)) {
writer.write("[appearance]\n");
writer.write("fontSize = 24\n");
writer.write("fontFamily = Monospaced\n");
writer.write("\n[behavior]\n");
writer.write("startupBehavior = AUTO_CONNECT\n");
writer.write("autoConnectHost = testhost.example.org\n");
writer.write("autoConnectPort = 2323\n");
writer.write("autoConnectAutoReconnect = true\n");
}
SettingsDialog dialog = new SettingsDialog(app);
dialog.importConfigFile(tempIni);
// Verify Settings updated
assertEquals(24, Settings.getFontSize());
assertEquals("testhost.example.org", Settings.getAutoConnectHost());
assertEquals(2323, Settings.getAutoConnectPort());
assertTrue(Settings.getAutoConnectAutoReconnect());
// Verify dialog state reflects the imported values
assertEquals(Settings.StartupBehavior.AUTO_CONNECT, Settings.getStartupBehavior());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
} finally {
if (tempIni.exists()) {
tempIni.delete();
}
}
}
@Test
@DisplayName("importConfigFile throws FileNotFoundException for nonexistent file")
public void testImportNonExistentFileThrows() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
File nonExistent = new File("/path/to/nonexistent/file_" + System.currentTimeMillis() + ".ini");
assertThrows(FileNotFoundException.class, () -> {
dialog.importConfigFile(nonExistent);
});
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
}
@@ -0,0 +1,337 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.HeadlessException;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
public class KeyBindingsTest {
@Test
public void testDefaultBindingsContainRequiredKeys() {
String insertBindings = Settings.getDefaultBinding("INSERT");
assertNotNull(insertBindings);
assertTrue(insertBindings.contains("INSERT"), "Default INSERT binding should contain INSERT key");
if (Settings.isMac()) {
assertTrue(insertBindings.contains("HELP"), "macOS INSERT binding should contain HELP key");
assertTrue(insertBindings.contains("alt I"), "macOS INSERT binding should contain alt I fallback");
} else {
assertTrue(insertBindings.contains("ctrl I"), "Non-macOS INSERT binding should contain ctrl I fallback");
}
String copyBindings = Settings.getDefaultBinding("COPY");
assertNotNull(copyBindings);
assertTrue(copyBindings.contains("ctrl INSERT"), "Default COPY binding should support 3270 Ctrl+Insert");
assertTrue(copyBindings.contains("ctrl C"), "Default COPY binding should support Ctrl+C");
assertTrue(copyBindings.contains("ctrl shift C"), "Default COPY binding should support Ctrl+Shift+C");
if (Settings.isMac()) {
assertTrue(copyBindings.contains("meta C"), "macOS COPY binding should support Cmd+C");
}
String pasteBindings = Settings.getDefaultBinding("PASTE");
assertNotNull(pasteBindings);
assertTrue(pasteBindings.contains("shift INSERT"), "Default PASTE binding should support 3270 Shift+Insert");
assertTrue(pasteBindings.contains("ctrl V"), "Default PASTE binding should support Ctrl+V");
assertTrue(pasteBindings.contains("ctrl shift V"), "Default PASTE binding should support Ctrl+Shift+V");
if (Settings.isMac()) {
assertTrue(pasteBindings.contains("meta V"), "macOS PASTE binding should support Cmd+V");
}
String selectAllBindings = Settings.getDefaultBinding("SELECTALL");
assertNotNull(selectAllBindings);
assertTrue(selectAllBindings.contains("ctrl A"), "Default SELECTALL binding should support Ctrl+A");
if (Settings.isMac()) {
assertTrue(selectAllBindings.contains("meta A"), "macOS SELECTALL binding should support Cmd+A");
}
}
@Test
public void testTerminalPanelInputMapAndAction() {
TerminalPanel panel = new TerminalPanel();
InputMap im = panel.getInputMap(JComponent.WHEN_FOCUSED);
ActionMap am = panel.getActionMap();
// Check INSERT key mappings
KeyStroke ksInsert = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0);
assertEquals("j3270-INSERT", im.get(ksInsert), "VK_INSERT must map to j3270-INSERT");
if (Settings.isMac()) {
KeyStroke ksHelp = KeyStroke.getKeyStroke(KeyEvent.VK_HELP, 0);
assertEquals("j3270-INSERT", im.get(ksHelp), "VK_HELP must map to j3270-INSERT on Mac");
KeyStroke ksAltI = KeyStroke.getKeyStroke("alt I");
assertEquals("j3270-INSERT", im.get(ksAltI), "alt I must map to j3270-INSERT on Mac");
}
// Check COPY key mappings
KeyStroke ksCtrlIns = KeyStroke.getKeyStroke("ctrl INSERT");
assertEquals("j3270-COPY", im.get(ksCtrlIns), "ctrl INSERT must map to j3270-COPY");
KeyStroke ksCtrlC = KeyStroke.getKeyStroke("ctrl C");
assertEquals("j3270-COPY", im.get(ksCtrlC), "ctrl C must map to j3270-COPY");
if (Settings.isMac()) {
KeyStroke ksMetaC = KeyStroke.getKeyStroke("meta C");
assertEquals("j3270-COPY", im.get(ksMetaC), "meta C must map to j3270-COPY on Mac");
}
// Check PASTE key mappings
KeyStroke ksShiftIns = KeyStroke.getKeyStroke("shift INSERT");
assertEquals("j3270-PASTE", im.get(ksShiftIns), "shift INSERT must map to j3270-PASTE");
KeyStroke ksCtrlV = KeyStroke.getKeyStroke("ctrl V");
assertEquals("j3270-PASTE", im.get(ksCtrlV), "ctrl V must map to j3270-PASTE");
if (Settings.isMac()) {
KeyStroke ksMetaV = KeyStroke.getKeyStroke("meta V");
assertEquals("j3270-PASTE", im.get(ksMetaV), "meta V must map to j3270-PASTE on Mac");
}
// Check SELECTALL key mappings
KeyStroke ksCtrlA = KeyStroke.getKeyStroke("ctrl A");
assertEquals("j3270-SELECTALL", im.get(ksCtrlA), "ctrl A must map to j3270-SELECTALL");
if (Settings.isMac()) {
KeyStroke ksMetaA = KeyStroke.getKeyStroke("meta A");
assertEquals("j3270-SELECTALL", im.get(ksMetaA), "meta A must map to j3270-SELECTALL on Mac");
}
// Test insert mode toggle action
assertFalse(panel.isInsertMode(), "Initial insertMode should be false");
Action insertAction = am.get("j3270-INSERT");
assertNotNull(insertAction, "j3270-INSERT action must exist");
AtomicBoolean modeChangedFired = new AtomicBoolean(false);
panel.addModeChangeListener(() -> modeChangedFired.set(true));
insertAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
assertTrue(panel.isInsertMode(), "insertMode should be true after invoking action");
assertTrue(modeChangedFired.get(), "ModeChangeListener should have fired");
modeChangedFired.set(false);
insertAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
assertFalse(panel.isInsertMode(), "insertMode should be false after invoking action again");
assertTrue(modeChangedFired.get(), "ModeChangeListener should have fired again");
}
@Test
public void testInsertModeSyncWithClient() {
TerminalPanel panel = new TerminalPanel();
panel.toggleInsertMode();
assertTrue(panel.isInsertMode());
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
panel.setClient(client);
// When client is attached, client's inputProcessor should receive current insertMode
assertNotNull(client.getInputProcessor());
assertTrue(client.getInputProcessor().isInsertMode(), "Client inputProcessor should inherit insertMode");
assertTrue(panel.isInsertMode());
// Toggling on panel updates client
panel.toggleInsertMode();
assertFalse(client.getInputProcessor().isInsertMode());
assertFalse(panel.isInsertMode());
panel.toggleInsertMode();
assertTrue(client.getInputProcessor().isInsertMode());
assertTrue(panel.isInsertMode());
}
@Test
public void testJ3270AppModesMenuSync() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
JMenu modesMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null && "Modes".equals(m.getText())) {
modesMenu = m;
break;
}
}
assertNotNull(modesMenu, "Modes menu should exist");
JCheckBoxMenuItem insertItem = null;
for (int i = 0; i < modesMenu.getItemCount(); i++) {
JMenuItem item = modesMenu.getItem(i);
if (item instanceof JCheckBoxMenuItem && "Insert Mode".equals(item.getText())) {
insertItem = (JCheckBoxMenuItem) item;
break;
}
}
assertNotNull(insertItem, "Insert Mode menu item should exist");
assertFalse(insertItem.isSelected(), "Initial Insert Mode item should not be selected");
// Find terminal panel and toggle
TerminalPanel panel = null;
for (java.awt.Component c : app.getContentPane().getComponents()) {
if (c instanceof TerminalPanel) {
panel = (TerminalPanel) c;
break;
}
}
assertNotNull(panel, "TerminalPanel should be found");
// Toggling via panel updates menu item
panel.toggleInsertMode();
assertTrue(panel.isInsertMode());
assertTrue(insertItem.isSelected(), "Menu item should reflect true insert mode");
panel.toggleInsertMode();
assertFalse(panel.isInsertMode());
assertFalse(insertItem.isSelected(), "Menu item should reflect false insert mode");
// Clicking menu item toggles insert mode on panel
insertItem.doClick();
assertTrue(panel.isInsertMode(), "Panel should be in insert mode after clicking menu item");
assertTrue(insertItem.isSelected(), "Menu item should be selected after click");
insertItem.doClick();
assertFalse(panel.isInsertMode(), "Panel should exit insert mode after clicking menu item");
assertFalse(insertItem.isSelected(), "Menu item should be deselected after click");
} finally {
app.dispose();
}
}
@Test
public void testCopyAndPasteActions() {
try {
java.awt.Toolkit.getDefaultToolkit().getSystemClipboard();
} catch (HeadlessException e) {
return;
}
TerminalPanel panel = new TerminalPanel();
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
panel.setClient(client);
var sb = client.getScreenBuffer();
sb.setChar(0, 0, 'J');
sb.setChar(0, 1, '3');
sb.setChar(0, 2, '2');
sb.setChar(0, 3, '7');
sb.setChar(0, 4, '0');
panel.setSelectionRange(0, 4);
assertTrue(panel.hasSelection());
assertEquals("J3270", panel.getSelectedText());
Action copyAction = panel.getActionMap().get("j3270-COPY");
assertNotNull(copyAction);
copyAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
try {
String clip = (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard()
.getData(java.awt.datatransfer.DataFlavor.stringFlavor);
assertEquals("J3270", clip);
} catch (Exception e) {
fail("Clipboard read failed: " + e.getMessage());
}
// Test multiline paste handling
try {
java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
new java.awt.datatransfer.StringSelection("LINE1\nLINE2"), null);
Action pasteAction = panel.getActionMap().get("j3270-PASTE");
assertNotNull(pasteAction);
pasteAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
// Shouldn't throw any exceptions
} catch (Exception e) {
fail("Paste action failed: " + e.getMessage());
}
}
@Test
public void testToggleStatusBar() {
assertEquals("alt B", Settings.getDefaultBinding("STATUS_BAR"));
TerminalPanel panel = new TerminalPanel();
InputMap im = panel.getInputMap(JComponent.WHEN_FOCUSED);
ActionMap am = panel.getActionMap();
KeyStroke ksAltB = KeyStroke.getKeyStroke("alt B");
assertEquals("j3270-STATUS_BAR", im.get(ksAltB), "alt B must map to j3270-STATUS_BAR");
assertNotNull(am.get("j3270-STATUS_BAR"), "j3270-STATUS_BAR action must exist");
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
JMenu viewMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null && "View".equals(m.getText())) {
viewMenu = m;
break;
}
}
assertNotNull(viewMenu);
JCheckBoxMenuItem statusBarItem = null;
for (int i = 0; i < viewMenu.getItemCount(); i++) {
JMenuItem item = viewMenu.getItem(i);
if (item instanceof JCheckBoxMenuItem && "Status Bar".equals(item.getText())) {
statusBarItem = (JCheckBoxMenuItem) item;
break;
}
}
assertNotNull(statusBarItem, "Status Bar menu item must exist in View menu");
assertTrue(statusBarItem.isSelected(), "Status bar should be selected by default");
StatusBar bar = null;
for (java.awt.Component c : app.getContentPane().getComponents()) {
if (c instanceof StatusBar) {
bar = (StatusBar) c;
break;
}
}
assertNotNull(bar, "StatusBar component should exist in content pane");
assertTrue(bar.isVisible(), "StatusBar should be visible by default");
// Test toggling via toggleStatusBar()
app.toggleStatusBar();
assertFalse(bar.isVisible(), "StatusBar should be hidden after toggle");
assertFalse(statusBarItem.isSelected(), "Menu item should be unchecked");
assertFalse(Settings.getStatusBarVisible());
app.toggleStatusBar();
assertTrue(bar.isVisible(), "StatusBar should be shown after second toggle");
assertTrue(statusBarItem.isSelected(), "Menu item should be checked");
assertTrue(Settings.getStatusBarVisible());
// Test toggling via menu item click
statusBarItem.doClick();
assertFalse(bar.isVisible(), "StatusBar should be hidden after menu click");
assertFalse(statusBarItem.isSelected(), "Menu item should be unchecked");
statusBarItem.doClick();
assertTrue(bar.isVisible(), "StatusBar should be shown after menu click");
assertTrue(statusBarItem.isSelected(), "Menu item should be checked");
} finally {
app.dispose();
Settings.setStatusBarVisible(true);
}
}
}
@@ -0,0 +1,105 @@
package haus.nightmare.j3270.ui;
import org.junit.jupiter.api.Test;
import java.io.StringReader;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for Phase 13.3: Keyboard Remap (.kmp) File Importer.
* Tests parsing of scan codes, modifier combinations, bind statements,
* mnemonic actions, and comments.
*/
public class KmpKeymapImportTest {
@Test
public void testParseScanCodeAssignments() throws Exception {
String kmpContent =
"# Sample keyboard profile mapping\n" +
"; IBM scan code mappings\n" +
"KEY43=[enter]\n" +
"S-KEY112=[pf13]\n" +
"C-KEY43=[newline]\n" +
"A-KEY110=[reset]\n" +
"KEY15=[tab]\n" +
"S-KEY15=[backtab]\n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertNotNull(imported);
// KEY43 is ENTER -> ENTER
assertEquals("ENTER", imported.get("ENTER"));
// S-KEY112 is Shift+F1 -> PF13
assertEquals("shift F1", imported.get("PF13"));
// C-KEY43 is Ctrl+ENTER -> NEWLINE
assertEquals("ctrl ENTER", imported.get("NEWLINE"));
// A-KEY110 is Alt+ESCAPE -> ESCAPE
assertEquals("alt ESCAPE", imported.get("ESCAPE"));
// KEY15 is TAB -> TAB
assertEquals("TAB", imported.get("TAB"));
// S-KEY15 is Shift+TAB -> shift TAB
assertEquals("shift TAB", imported.get("shift TAB"));
}
@Test
public void testParseBindStatements() throws Exception {
String kmpContent =
"bind [enter] Ctrl+Enter\n" +
"bind [pf1] F1\n" +
"bind [pf24] Shift+F12\n" +
"bind [eraseeof] End\n" +
"bind [clear] Pause\n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertNotNull(imported);
assertEquals("ctrl ENTER", imported.get("ENTER"));
assertEquals("F1", imported.get("PF1"));
assertEquals("shift F12", imported.get("PF24"));
assertEquals("END", imported.get("END"));
assertEquals("PAUSE", imported.get("CLEAR"));
}
@Test
public void testScanCodeWithMultipleModifiers() {
// C-S-KEY85 -> Ctrl+Shift+PAGE_UP
String res = KeyBindings.parseScanCodeEntry("CS-KEY85");
assertEquals("ctrl shift PAGE_UP", res);
// 2-KEY80 -> Alt+HOME (2- prefix is Alt in scan code syntax)
String altRes = KeyBindings.parseScanCodeEntry("2-KEY80");
assertEquals("alt HOME", altRes);
}
@Test
public void testMnemonicResolution() {
assertEquals("ENTER", KeyBindings.resolveMnemonic("[enter]"));
assertEquals("ENTER", KeyBindings.resolveMnemonic("enterreset"));
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[newline]"));
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[field-exit]"));
assertEquals("PF1", KeyBindings.resolveMnemonic("[pf1]"));
assertEquals("PF12", KeyBindings.resolveMnemonic("[pf12]"));
assertEquals("PF13", KeyBindings.resolveMnemonic("[spf1]"));
assertEquals("ESCAPE", KeyBindings.resolveMnemonic("[reset]"));
assertEquals("ERASE_INPUT", KeyBindings.resolveMnemonic("[erasefld]"));
assertEquals("DOCMODE", KeyBindings.resolveMnemonic("[docmode]"));
assertEquals("WORDWRAP", KeyBindings.resolveMnemonic("[wordwrap]"));
}
@Test
public void testCommentsAndBlankLinesIgnored() throws Exception {
String kmpContent =
"[ProfileHeader]\n" +
" \n" +
"# Full line comment\n" +
"; Another comment\n" +
"KEY42=[enter] ; inline comment\n" +
" \n";
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
assertEquals(1, imported.size());
assertEquals("ENTER", imported.get("ENTER"));
}
}
@@ -58,11 +58,26 @@ public class MenuBarShortcutsTest {
assertNotNull(fileMenu, "File menu should exist"); assertNotNull(fileMenu, "File menu should exist");
assertNotNull(editMenu, "Edit menu should exist"); assertNotNull(editMenu, "Edit menu should exist");
// Verify View Menu items use ALT_DOWN_MASK // Verify View Menu items
Map<String, JMenuItem> viewItems = collectMenuItems(viewMenu); Map<String, JMenuItem> viewItems = collectMenuItems(viewMenu);
assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS); assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS); assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0); assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0);
assertAcceleratorUsesAlt(viewItems.get("Status Bar"), KeyEvent.VK_B);
// Crosshair Ruler must use Alt+Shift+R
JMenuItem rulerItem = viewItems.get("Crosshair Ruler");
assertNotNull(rulerItem, "Crosshair Ruler menu item must exist");
KeyStroke rulerKs = rulerItem.getAccelerator();
assertNotNull(rulerKs, "Crosshair Ruler should have an accelerator");
assertEquals(KeyEvent.VK_R, rulerKs.getKeyCode(), "Key code mismatch for Crosshair Ruler");
int rulerMods = rulerKs.getModifiers();
assertTrue((rulerMods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
"Crosshair Ruler accelerator must have ALT modifier");
assertTrue((rulerMods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK)) != 0,
"Crosshair Ruler accelerator must have SHIFT modifier");
assertEquals(0, rulerMods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
"Crosshair Ruler accelerator must NOT have CTRL or META modifiers");
// Verify Actions Menu items use ALT_DOWN_MASK // Verify Actions Menu items use ALT_DOWN_MASK
Map<String, JMenuItem> actionItems = collectMenuItems(actionsMenu); Map<String, JMenuItem> actionItems = collectMenuItems(actionsMenu);
@@ -75,6 +90,10 @@ public class MenuBarShortcutsTest {
assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q); assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q);
assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L); assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L);
assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T); assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T);
// Ensure Crosshair Ruler and Reset do not collide
assertNotEquals(rulerKs, actionItems.get("Reset").getAccelerator(),
"Crosshair Ruler and Reset accelerators must not collide");
} finally { } finally {
// Dispose frame // Dispose frame
app.dispose(); app.dispose();
@@ -89,6 +108,8 @@ public class MenuBarShortcutsTest {
int mods = ks.getModifiers(); int mods = ks.getModifiers();
assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0, assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
"Menu item " + item.getText() + " accelerator must have ALT modifier"); "Menu item " + item.getText() + " accelerator must have ALT modifier");
assertEquals(0, mods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK),
"Menu item " + item.getText() + " accelerator must NOT have SHIFT modifier");
assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK), 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"); "Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers");
} }
@@ -0,0 +1,114 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Color;
import java.awt.HeadlessException;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 1 UI Overlays and Color Alignment.
* Covers items 1.1, 1.2, and 1.5 of the PhasedUpdates specification.
*/
public class Phase1UiOverlayTest {
@Test
@DisplayName("Item 1.1 & 1.2: TerminalPanel host and mono colors match IBM Host On-Demand specs")
public void testTerminalPanelColorAlignment() {
// Index 10: Orange (255, 162, 0)
assertEquals(new Color(255, 162, 0), TerminalPanel.DEFAULT_HOST_COLORS[10]);
// Index 14: Mustard (160, 160, 0)
assertEquals(new Color(160, 160, 0), TerminalPanel.DEFAULT_HOST_COLORS[14]);
// Index 1: Blue (120, 144, 240)
assertEquals(new Color(120, 144, 240), TerminalPanel.DEFAULT_HOST_COLORS[1]);
// Base 4-Color mono defaults
assertEquals(new Color(0, 255, 0), TerminalPanel.DEFAULT_MONO_NORMAL);
assertEquals(new Color(255, 0, 0), TerminalPanel.DEFAULT_MONO_INTENSIFY);
assertEquals(new Color(0, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED);
assertEquals(new Color(255, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH);
}
@Test
@DisplayName("Item 1.5: ThemeManager HoD OIA category colors match specification")
public void testThemeManagerOiaColors() {
ThemeManager.setTheme(UITheme.DARK);
// oSI: Status / System Available -> CUSTOMBLUE (120, 144, 240)
assertEquals(new Color(120, 144, 240), ThemeManager.getOiaStatusSysAvail());
// oII: Input Inhibited / X SYSTEM -> White (255, 255, 255)
assertEquals(new Color(255, 255, 255), ThemeManager.getOiaInputInhibited());
// oAI: Attention / Reminders / Message Waiting -> Yellow (255, 255, 0)
assertEquals(new Color(255, 255, 0), ThemeManager.getOiaAttention());
// oEI: Error Checks / Comm Check -> Red (255, 0, 0)
assertEquals(new Color(255, 0, 0), ThemeManager.getOiaCommCheck());
// oOB: OIA Separator / Background -> Black (0, 0, 0)
assertEquals(new Color(0, 0, 0), ThemeManager.getOiaBackground());
// Light mode OIA colors should be legible and not white on grey
ThemeManager.setTheme(UITheme.LIGHT);
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited());
assertEquals(new Color(20, 20, 20), ThemeManager.getOiaInputInhibited());
assertEquals(new Color(0, 90, 200), ThemeManager.getOiaStatusSysAvail());
assertEquals(new Color(180, 100, 0), ThemeManager.getOiaAttention());
assertEquals(new Color(190, 20, 20), ThemeManager.getOiaCommCheck());
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), ThemeManager.getOiaBackground());
}
@Test
@DisplayName("Item 1.5: TerminalPanel Crosshair Ruler and Cursor Style controls")
public void testTerminalPanelRulerAndCursorStyle() {
try {
TerminalPanel panel = new TerminalPanel();
// Default cursor style is BLOCK
assertEquals(TerminalPanel.CursorStyle.BLOCK, panel.getCursorStyle());
panel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE);
assertEquals(TerminalPanel.CursorStyle.UNDERLINE, panel.getCursorStyle());
// Crosshair ruler toggling
assertFalse(panel.isCrosshairRulerEnabled());
panel.setCrosshairRulerEnabled(true);
assertTrue(panel.isCrosshairRulerEnabled());
panel.toggleCrosshairRuler();
assertFalse(panel.isCrosshairRulerEnabled());
// Wallpaper image setter
assertNull(panel.getWallpaperImage());
} catch (HeadlessException e) {
// Handled gracefully in headless CI environments
}
}
@Test
@DisplayName("Settings persistence for Crosshair Ruler and Cursor Style")
public void testSettingsPersistence() {
boolean originalRuler = Settings.getCrosshairRuler();
String originalCursor = Settings.getCursorStyle();
try {
Settings.setCrosshairRuler(true);
assertTrue(Settings.getCrosshairRuler());
Settings.setCrosshairRuler(false);
assertFalse(Settings.getCrosshairRuler());
Settings.setCursorStyle("UNDERLINE");
assertEquals("UNDERLINE", Settings.getCursorStyle());
Settings.setCursorStyle("BLOCK");
assertEquals("BLOCK", Settings.getCursorStyle());
} finally {
Settings.setCrosshairRuler(originalRuler);
Settings.setCursorStyle(originalCursor);
}
}
}
@@ -0,0 +1,95 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.graphics.GocaConstants;
import haus.nightmare.lib3270j.graphics.GraphicsPlane;
import haus.nightmare.lib3270j.graphics.HODWallpaper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.image.BufferedImage;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 2 UI Vector Graphics, Wallpaper, and Cursor Overlays.
*/
public class Phase2UiGraphicsTest {
@Test
@DisplayName("Test TerminalPanel Wallpaper Integration (Tile, Center, Stretch)")
public void testTerminalPanelWallpaperModes() {
try {
TerminalPanel panel = new TerminalPanel();
assertNull(panel.getHodWallpaper());
assertNull(panel.getWallpaperImage());
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
panel.setWallpaperImage(img);
assertNotNull(panel.getWallpaperImage());
assertNotNull(panel.getHodWallpaper());
assertEquals(HODWallpaper.HOD_STRETCH, panel.getHodWallpaper().getDisplay());
panel.setWallpaperMode(HODWallpaper.HOD_TILE);
assertEquals(HODWallpaper.HOD_TILE, panel.getHodWallpaper().getDisplay());
panel.setWallpaperMode(HODWallpaper.HOD_CENTER);
assertEquals(HODWallpaper.HOD_CENTER, panel.getHodWallpaper().getDisplay());
HODWallpaper customWp = new HODWallpaper(img, HODWallpaper.HOD_TILE);
panel.setHodWallpaper(customWp);
assertSame(customWp, panel.getHodWallpaper());
// Clear wallpaper
panel.setWallpaperImage(null);
assertNull(panel.getHodWallpaper());
assertNull(panel.getWallpaperImage());
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
@Test
@DisplayName("Test Graphics Plane Cursor Synchronization and Rendering")
public void testGraphicsCursorOverlay() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(new Dimension(800, 600));
panel.setClient(new haus.nightmare.lib3270j.Telnet3270Client(new haus.nightmare.lib3270j.ConnectionConfig("localhost", 23)));
GraphicsPlane plane = panel.getClient() != null ? panel.getClient().getGraphicsPlane() : null;
if (plane != null) {
// Attach graphics cursor
plane.attachGraphicCursor(100, 100);
plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_CROSSHAIR);
assertTrue(plane.isGraphicCursorAttached());
assertEquals(100, plane.getGraphicCursorX());
assertEquals(100, plane.getGraphicCursorY());
assertEquals(1, plane.getHodCursorShape());
// Paint component to verify no exceptions during rendering
BufferedImage offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = offscreen.createGraphics();
panel.paint(g2);
g2.dispose();
// Change cursor shape to box
plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_BOX);
assertEquals(2, plane.getHodCursorShape());
offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
g2 = offscreen.createGraphics();
panel.paint(g2);
g2.dispose();
// Detach graphics cursor
plane.detachGraphicCursor();
assertFalse(plane.isGraphicCursorAttached());
}
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
}
@@ -0,0 +1,265 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Cursor;
import java.awt.HeadlessException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class SelectionHandlesAndTrimTest {
private boolean origRemainAfterEdit;
private boolean origSizingHandles;
@BeforeEach
public void setUp() {
origRemainAfterEdit = Settings.getTrimRectRemainAfterEdit();
origSizingHandles = Settings.getTrimRectSizingHandles();
}
@AfterEach
public void tearDown() {
Settings.setTrimRectRemainAfterEdit(origRemainAfterEdit);
Settings.setTrimRectSizingHandles(origSizingHandles);
}
private TerminalPanel createTestPanel() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
panel.setClient(client);
return panel;
}
@Test
@DisplayName("Trailing whitespace is trimmed on copied lines in block selection mode")
public void testBlockSelectionTrailingWhitespaceTrim() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
// Populate row 0: "HELLO "
String text0 = "HELLO";
for (int i = 0; i < text0.length(); i++) {
sb.getCell(i).ucs4 = text0.charAt(i);
}
// Populate row 1: "WORLD 123 "
String text1 = "WORLD 123";
for (int i = 0; i < text1.length(); i++) {
sb.getCell(cols + i).ucs4 = text1.charAt(i);
}
// Select rectangle from (row 0, col 0) to (row 1, col 20)
panel.setBlockSelectMode(true);
panel.setSelectionRange(0, cols + 20);
String copied = panel.getSelectedText();
// Should be "HELLO\nWORLD 123" without the trailing spaces up to col 20
assertEquals("HELLO\nWORLD 123", copied);
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Trailing whitespace is trimmed on copied lines in stream selection mode")
public void testStreamSelectionTrailingWhitespaceTrim() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
// Populate row 0: "LINE1 "
String text0 = "LINE1";
for (int i = 0; i < text0.length(); i++) {
sb.getCell(i).ucs4 = text0.charAt(i);
}
// Populate row 1: "LINE2"
String text1 = "LINE2";
for (int i = 0; i < text1.length(); i++) {
sb.getCell(cols + i).ucs4 = text1.charAt(i);
}
panel.setBlockSelectMode(false);
panel.setTrimRectSizingHandles(false);
// Select from row 0, col 0 to row 1, col 10
panel.setSelectionRange(0, cols + 10);
String copied = panel.getSelectedText();
assertEquals("LINE1\nLINE2", copied);
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Drag handle detection identifies 8 resize handles, MOVE inside box, and NONE outside")
public void testDragHandleDetection() {
try {
TerminalPanel panel = createTestPanel();
panel.setBlockSelectMode(true);
panel.setTrimRectSizingHandles(true);
// Without selection, should return NONE
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(100, 100));
// Select rectangle: rows 2 to 5, cols 10 to 20
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
panel.setSelectionRange(2 * cols + 10, 5 * cols + 20);
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
int cw = panel.cellWidth;
int ch = panel.cellHeight;
int x1 = ox + 10 * cw;
int y1 = oy + 2 * ch;
int x2 = ox + 21 * cw;
int y2 = oy + 6 * ch;
int xm = (x1 + x2) / 2;
int ym = (y1 + y2) / 2;
// Test 8 handles
assertEquals(TerminalPanel.DragHandle.NW, panel.getDragHandleAt(x1, y1));
assertEquals(TerminalPanel.DragHandle.N, panel.getDragHandleAt(xm, y1));
assertEquals(TerminalPanel.DragHandle.NE, panel.getDragHandleAt(x2, y1));
assertEquals(TerminalPanel.DragHandle.E, panel.getDragHandleAt(x2, ym));
assertEquals(TerminalPanel.DragHandle.SE, panel.getDragHandleAt(x2, y2));
assertEquals(TerminalPanel.DragHandle.S, panel.getDragHandleAt(xm, y2));
assertEquals(TerminalPanel.DragHandle.SW, panel.getDragHandleAt(x1, y2));
assertEquals(TerminalPanel.DragHandle.W, panel.getDragHandleAt(x1, ym));
// Test inside box -> MOVE
assertEquals(TerminalPanel.DragHandle.MOVE, panel.getDragHandleAt(xm, ym));
// Test far outside -> NONE
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(0, 0));
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(x2 + 50, y2 + 50));
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Cursor mapping for all drag handles")
public void testCursorForDragHandles() {
try {
TerminalPanel panel = createTestPanel();
assertEquals(Cursor.NW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NW).getType());
assertEquals(Cursor.N_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.N).getType());
assertEquals(Cursor.NE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NE).getType());
assertEquals(Cursor.E_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.E).getType());
assertEquals(Cursor.SE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SE).getType());
assertEquals(Cursor.S_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.S).getType());
assertEquals(Cursor.SW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SW).getType());
assertEquals(Cursor.W_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.W).getType());
assertEquals(Cursor.MOVE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.MOVE).getType());
assertEquals(Cursor.DEFAULT_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NONE).getType());
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("trimRectRemainAfterEdit controls whether selection is retained after copy")
public void testTrimRectRemainAfterEdit() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
sb.getCell(0).ucs4 = 'A';
panel.setSelectionRange(0, 5);
assertTrue(panel.hasSelection());
// By default (false), copySelection() should clear the selection
Settings.setTrimRectRemainAfterEdit(false);
panel.copySelection();
assertFalse(panel.hasSelection(), "Selection should be cleared after copy when remainAfterEdit is false");
// When set to true, copySelection() should preserve the selection
Settings.setTrimRectRemainAfterEdit(true);
panel.setSelectionRange(0, 5);
assertTrue(panel.hasSelection());
panel.copySelection();
assertTrue(panel.hasSelection(), "Selection should remain active after copy when remainAfterEdit is true");
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("cutSelection clears unprotected cells, sets FA_MODIFY, and respects remainAfterEdit")
public void testCutSelectionBehavior() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
// Field at 0: Unprotected field
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
String data = "TESTDATA";
for (int i = 0; i < data.length(); i++) {
sb.getCell(1 + i).ucs4 = data.charAt(i);
}
// Select the field contents
panel.setSelectionRange(1, 8);
assertTrue(panel.hasSelection());
Settings.setTrimRectRemainAfterEdit(false);
panel.cutSelection();
// Characters should be replaced with spaces
for (int i = 1; i <= 8; i++) {
assertEquals(' ', sb.getCell(i).ucs4, "Cell " + i + " should be erased to space");
}
// FA_MODIFY should remain or be set
assertTrue((sb.getCell(0).ec & FA_MODIFY) != 0, "FA_MODIFY should be set on field attribute");
assertFalse(panel.hasSelection(), "Selection should be cleared when remainAfterEdit is false");
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("KeyBindings exportKmp generates HoD format and round-trips through parseKmp")
public void testKeyBindingsExportAndParseRoundTrip() throws Exception {
Map<String, String> keymap = new LinkedHashMap<>();
keymap.put("ENTER", "ENTER");
keymap.put("shift TAB", "TAB");
keymap.put("CLEAR", "PAUSE");
keymap.put("PF1", "F1");
keymap.put("PF13", "shift F1");
keymap.put("ESCAPE", "ESCAPE");
StringWriter writer = new StringWriter();
KeyBindings.exportKmp(writer, keymap);
String exported = writer.toString();
assertTrue(exported.contains("[KeyRemap]"), "Exported KMP must contain [KeyRemap] section header");
assertTrue(exported.contains("[enter]"), "Exported KMP must contain HoD mnemonics");
assertTrue(exported.contains("[backtab]"), "Exported KMP must contain [backtab]");
// Roundtrip parse
Map<String, String> parsed = KeyBindings.parseKmp(new StringReader(exported));
assertNotNull(parsed);
assertEquals("ENTER", parsed.get("ENTER"));
assertEquals("TAB", parsed.get("shift TAB"));
assertEquals("PAUSE", parsed.get("CLEAR"));
assertEquals("F1", parsed.get("PF1"));
assertEquals("shift F1", parsed.get("PF13"));
assertEquals("ESCAPE", parsed.get("ESCAPE"));
}
}
@@ -59,4 +59,49 @@ public class StatusBarTest {
// Ignored in headless environments // Ignored in headless environments
} }
} }
@Test
public void testStatusBarXSystemAndXProt() {
try {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_2, false);
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null);
statusBar.updateStatus();
// Initially not locked
assertEquals("", statusBar.getLockStatusText());
// 1. Setup protected field and type into it -> triggers X PROT
client.getScreenBuffer().setFieldAttribute(0, (byte) haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT);
client.getScreenBuffer().setCursorAddress(1);
client.typeCharacter('A');
statusBar.updateStatus();
assertEquals("X PROT", statusBar.getLockStatusText());
// 2. Reset -> clears X PROT
client.reset();
statusBar.updateStatus();
assertEquals("", statusBar.getLockStatusText());
// 3. Send Enter -> triggers X SYSTEM
client.sendEnter();
statusBar.updateStatus();
assertEquals("X SYSTEM", statusBar.getLockStatusText());
// 4. Host unlocks keyboard
client.getInputProcessor().setKeyboardLocked(false);
statusBar.updateStatus();
assertEquals("", statusBar.getLockStatusText());
// 5. Subsequent Enter -> triggers X SYSTEM again
client.sendEnter();
statusBar.updateStatus();
assertEquals("X SYSTEM", statusBar.getLockStatusText());
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
} }
@@ -0,0 +1,101 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.HeadlessException;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TerminalPanelSelectionTest {
@Test
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
String headerText = " ISPF PRIMARY OPTION MENU";
for (int i = 0; i < headerText.length(); i++) {
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
sb.getCell(1 + i).ec = (byte) (0x40 + i);
}
panel.setClient(client);
// Cell dimensions in panel
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener[] mouseListeners = panel.getMouseListeners();
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
assertTrue(mouseListeners.length > 0);
assertTrue(motionListeners.length > 0);
MouseListener ml = mouseListeners[0];
MouseMotionListener mml = motionListeners[0];
// 1. Mouse Pressed at cell (row 0, col 2)
int startX = ox + 2 * cellW + cellW / 2;
int startY = oy + cellH / 2;
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, startX, startY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(pressEvent);
// 2. Drag to cell (row 0, col 15)
int dragX = ox + 15 * cellW + cellW / 2;
int dragY = oy + cellH / 2;
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
mml.mouseDragged(dragEvent);
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
// 3. Mouse Released at cell (row 0, col 15)
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(releaseEvent);
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
String selected = panel.getSelectedText();
assertNotNull(selected);
assertTrue(selected.length() > 0, "Selected text must not be empty");
// 4. A subsequent single click without drag should clear the selection and move the cursor
int clickX = ox + 5 * cellW + cellW / 2;
int clickY = oy + cellH / 2;
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(clickPress);
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(clickRelease);
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
}
@@ -62,8 +62,33 @@ public class ThemeManagerTest {
// Verify Status Bar colors // Verify Status Bar colors
Color sbBg = ThemeManager.getStatusBarBg(theme); Color sbBg = ThemeManager.getStatusBarBg(theme);
Color sbNormal = ThemeManager.getOiaFgNormal(theme); Color sbNormal = ThemeManager.getOiaFgNormal(theme);
Color sbInhibited = ThemeManager.getOiaInputInhibited(theme);
Color sbSysAvail = ThemeManager.getOiaStatusSysAvail(theme);
Color sbAttention = ThemeManager.getOiaAttention(theme);
Color sbCommCheck = ThemeManager.getOiaCommCheck(theme);
assertNotNull(sbBg); assertNotNull(sbBg);
assertNotNull(sbNormal); assertNotNull(sbNormal);
assertNotNull(sbInhibited);
assertNotNull(sbSysAvail);
assertNotNull(sbAttention);
assertNotNull(sbCommCheck);
// Verify status bar contrast: ensure NO white on grey in light mode
double sbBgLum = (0.299 * sbBg.getRed() + 0.587 * sbBg.getGreen() + 0.114 * sbBg.getBlue());
double sbInhibitedLum = (0.299 * sbInhibited.getRed() + 0.587 * sbInhibited.getGreen() + 0.114 * sbInhibited.getBlue());
double sbInhibitedDiff = Math.abs(sbBgLum - sbInhibitedLum);
assertTrue(sbInhibitedDiff > 120, "Status bar input inhibited (X PROT) contrast in " + theme + " must be > 120 (was " + sbInhibitedDiff + ")");
double sbSysAvailLum = (0.299 * sbSysAvail.getRed() + 0.587 * sbSysAvail.getGreen() + 0.114 * sbSysAvail.getBlue());
assertTrue(Math.abs(sbBgLum - sbSysAvailLum) > 100, "Status bar sys avail contrast in " + theme + " must be > 100");
double sbAttentionLum = (0.299 * sbAttention.getRed() + 0.587 * sbAttention.getGreen() + 0.114 * sbAttention.getBlue());
assertTrue(Math.abs(sbBgLum - sbAttentionLum) > 100, "Status bar attention contrast in " + theme + " must be > 100");
if (theme == UITheme.LIGHT) {
assertNotEquals(Color.WHITE, sbInhibited, "Status bar input inhibited text must not be pure white in light mode");
assertNotEquals(Color.WHITE, sbNormal, "Status bar normal text must not be pure white in light mode");
}
// Verify Button colors // Verify Button colors
Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme); Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme);
@@ -218,4 +243,100 @@ public class ThemeManagerTest {
tmpIni.delete(); tmpIni.delete();
} }
} }
@Test
public void testStatusBarLightModeLegibility() {
try {
ThemeManager.setTheme(UITheme.LIGHT);
StatusBar sb = new StatusBar();
sb.applyTheme(UITheme.LIGHT);
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), sb.getBackground());
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited());
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited(UITheme.LIGHT));
// Verify recursive applyTheme does not break StatusBar background
ThemeManager.applyTheme(sb, UITheme.LIGHT);
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), sb.getBackground());
} catch (HeadlessException e) {
// Handled in headless CI
}
}
@Test
public void testUiFontRetrievalAndDefaults() {
String prevFamily = Settings.getUiFontFamily();
int prevSize = Settings.getUiFontSize();
try {
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(16);
Font uiFont = ThemeManager.getUiFont();
assertEquals("Dialog", uiFont.getFamily());
assertEquals(16, uiFont.getSize());
assertEquals(Font.PLAIN, uiFont.getStyle());
Font boldOffset = ThemeManager.getUiFont(Font.BOLD, 2);
assertEquals(18, boldOffset.getSize());
assertEquals(Font.BOLD, boldOffset.getStyle());
Font monoFont = ThemeManager.getMonospacedUiFont();
assertEquals(Font.MONOSPACED, monoFont.getName());
assertEquals(16, monoFont.getSize());
// Test applyUIManagerDefaults sets font properties
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
assertEquals(uiFont, UIManager.get("Button.font"));
assertEquals(uiFont, UIManager.get("Label.font"));
assertEquals(uiFont, UIManager.get("Menu.font"));
assertEquals(uiFont, UIManager.get("Table.font"));
assertEquals(monoFont, UIManager.get("TextArea.font"));
} finally {
Settings.setUiFontFamily(prevFamily);
Settings.setUiFontSize(prevSize);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
}
}
@Test
public void testLabelFontNotIncrementedOnRepeatedThemeApplication() {
String prevFamily = Settings.getUiFontFamily();
int prevSize = Settings.getUiFontSize();
try {
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(16);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
JLabel label = new JLabel("Settings Label");
ThemeManager.applyTheme(label);
assertEquals(16, label.getFont().getSize(), "Initial label font size must match configured uiFontSize");
// Repeated theme applications (e.g. subsequent clicks of 'Apply' in Settings dialog)
// must never increment font size
for (int i = 1; i <= 5; i++) {
ThemeManager.applyTheme(label);
assertEquals(16, label.getFont().getSize(),
"Label font size must remain 16 on subsequent application #" + i);
}
// Test label with custom delta
JLabel headerLabel = new JLabel("Header Label");
headerLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
headerLabel.putClientProperty("uiFontSizeDelta", 3);
ThemeManager.applyTheme(headerLabel);
assertEquals(19, headerLabel.getFont().getSize(), "Custom header label should have base + 3 size");
for (int i = 1; i <= 5; i++) {
ThemeManager.applyTheme(headerLabel);
assertEquals(19, headerLabel.getFont().getSize(),
"Header label font size must remain 19 on subsequent application #" + i);
}
} finally {
Settings.setUiFontFamily(prevFamily);
Settings.setUiFontSize(prevSize);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
}
}
} }
@@ -0,0 +1,45 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Communications event matching IBM Host On-Demand ECLCommEvent.
*/
public class ECLCommEvent extends ECLEvent {
private final boolean status;
private final int eventType;
public ECLCommEvent(Object object, boolean status) {
super(object);
this.status = status;
this.eventType = status ? 1 : 2;
}
public ECLCommEvent(Object object, int eventType, boolean status) {
super(object);
this.eventType = eventType;
this.status = status;
}
public boolean connected() {
return this.status;
}
public boolean isConnected() {
return this.status;
}
public int getEventType() {
return this.eventType;
}
public ECLConnection GetConnection() {
Object src = GetSource();
if (src instanceof ECLConnection) return (ECLConnection) src;
if (src instanceof haus.nightmare.lib3270j.ecl.ECLConnection) {
return new ECLConnection((haus.nightmare.lib3270j.ecl.ECLConnection) src);
}
return null;
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.ECL;
/**
* Communications notification interface matching IBM Host On-Demand ECLCommNotify.
*/
public interface ECLCommNotify {
void NotifyEvent(ECLConnection conn, boolean connected);
default void NotifyStop(ECLConnection conn, int reason) {}
default void NotifyError(ECLConnection conn, ECLErr err) {}
}
@@ -0,0 +1,79 @@
package com.ibm.eNetwork.ECL;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Communications and connection management facade matching IBM Host On-Demand ECLConnection.
*/
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection implements ECLConstants {
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
public ECLConnection() {
super();
setupListener();
}
public ECLConnection(Properties props) {
super(props);
setupListener();
}
public ECLConnection(String host, int port) {
super(host, port);
setupListener();
}
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) {
super(session);
setupListener();
}
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLConnection delegate) {
super(delegate != null ? delegate.getSession() : null);
setupListener();
}
private void setupListener() {
RegisterCommEvent(new haus.nightmare.lib3270j.ecl.ECLCommListener() {
@Override
public void CommNotifyEvent(haus.nightmare.lib3270j.ecl.ECLCommEvent event) {
boolean conn = event != null && event.isConnected();
for (ECLCommNotify n : commNotifies) {
try {
n.NotifyEvent(ECLConnection.this, conn);
} catch (Exception ignored) {}
}
}
});
}
public void RegisterCommEvent(ECLCommNotify notify) {
if (notify != null && !commNotifies.contains(notify)) {
commNotifies.add(notify);
}
}
public void registerCommEvent(ECLCommNotify notify) { RegisterCommEvent(notify); }
public void UnregisterCommEvent(ECLCommNotify notify) {
if (notify != null) {
commNotifies.remove(notify);
}
}
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
@Override
public void StartCommunication() throws IOException {
super.StartCommunication();
}
@Override
public void StopCommunication() {
super.StopCommunication();
}
}
@@ -0,0 +1,138 @@
package com.ibm.eNetwork.ECL;
/**
* IBM Host Access Class Library (HACL) common constants.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLConstants.
*/
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
// Presentation space logical planes (HoD bitmask values)
int TEXT_PLANE = 1;
int COLOR_PLANE = 2;
int DBCS_PLANE = 4;
int GRID_PLANE = 8;
int FIELD_PLANE = 16;
int EXFIELD_PLANE = 32;
int NLS_PLANE = 64;
int HOST_PLANE = 128;
int RAWTEXT_PLANE = 256;
int COLOR_ATTRIBUTES_PLANE = 512;
int ALL_PLANES = 319;
// Search directions
int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2;
// Field filters
int ALL_FIELDS = 0;
int MODIFIED_FIELD = 1;
int ALPHANUM_FIELD = 2;
int HILIGHT_FIELD = 16;
int PROTECTED_FIELD = 32;
int DISPLAYABLE_FIELD = 64;
int PENDETECTABLE_FIELD = 128;
int UNMODIFIED_FIELD = 256;
int NUMERIC_FIELD = 512;
int LOLIGHT_FIELD = 4096;
int UNPROTECTED_FIELD = 8192;
int NON_DISPLAYABLE_FIELD = 16384;
int NON_PENDETECTABLE_FIELD = 32768;
// Standard Mnemonics Constants
String ENTER_STR = "[enter]";
String CLEAR_STR = "[clear]";
String RESET_STR = "[reset]";
String TAB_STR = "[tab]";
String FWDTAB_STR = "[tab]";
String BACKTAB_STR = "[backtab]";
String BACKSP_STR = "[backspace]";
String DELCHAR_STR = "[delete]";
String INSERT_STR = "[insert]";
String HOME_STR = "[home]";
String NEWLINE_STR = "[newline]";
String ENDLINE_STR = "[eof]";
String ERASEEOF_STR = "[eraseeof]";
String ERASEFLD_STR = "[erasefld]";
String ERASEINPUT_STR = "[erinp]";
String ATTN_STR = "[attn]";
String SYSREQ_STR = "[sysreq]";
String DUP_STR = "[dup]";
String FLDMRK_STR = "[fieldmark]";
String PA1_STR = "[pa1]";
String PA2_STR = "[pa2]";
String PA3_STR = "[pa3]";
String CURUP_STR = "[up]";
String CURDOWN_STR = "[down]";
String CURLEFT_STR = "[left]";
String CURRIGHT_STR = "[right]";
String PAGEUP_STR = "[pageup]";
String PAGEDWN_STR = "[pagedn]";
String COPY_STR = "[copy]";
String PASTE_STR = "[paste]";
String CUT_STR = "[cut]";
String SELECTALL_STR = "[selectall]";
// Function keys F1 - F24
String F1_STR = "[pf1]";
String F2_STR = "[pf2]";
String F3_STR = "[pf3]";
String F4_STR = "[pf4]";
String F5_STR = "[pf5]";
String F6_STR = "[pf6]";
String F7_STR = "[pf7]";
String F8_STR = "[pf8]";
String F9_STR = "[pf9]";
String F10_STR = "[pf10]";
String F11_STR = "[pf11]";
String F12_STR = "[pf12]";
String F13_STR = "[pf13]";
String F14_STR = "[pf14]";
String F15_STR = "[pf15]";
String F16_STR = "[pf16]";
String F17_STR = "[pf17]";
String F18_STR = "[pf18]";
String F19_STR = "[pf19]";
String F20_STR = "[pf20]";
String F21_STR = "[pf21]";
String F22_STR = "[pf22]";
String F23_STR = "[pf23]";
String F24_STR = "[pf24]";
// Shift F1 - F24
String SHIFT_F1_STR = "[Spf1]";
String SHIFT_F2_STR = "[Spf2]";
String SHIFT_F3_STR = "[Spf3]";
String SHIFT_F4_STR = "[Spf4]";
String SHIFT_F5_STR = "[Spf5]";
String SHIFT_F6_STR = "[Spf6]";
String SHIFT_F7_STR = "[Spf7]";
String SHIFT_F8_STR = "[Spf8]";
String SHIFT_F9_STR = "[Spf9]";
String SHIFT_F10_STR = "[Spf10]";
String SHIFT_F11_STR = "[Spf11]";
String SHIFT_F12_STR = "[Spf12]";
String SHIFT_F13_STR = "[Spf13]";
String SHIFT_F14_STR = "[Spf14]";
String SHIFT_F15_STR = "[Spf15]";
String SHIFT_F16_STR = "[Spf16]";
String SHIFT_F17_STR = "[Spf17]";
String SHIFT_F18_STR = "[Spf18]";
String SHIFT_F19_STR = "[Spf19]";
String SHIFT_F20_STR = "[Spf20]";
// Field attributes
short ATTR_PROTECTED = 32;
short ATTR_ALPHANUM = 16;
short ATTR_DISPLAY1 = 8;
short ATTR_DISPLAY2 = 4;
short ATTR_MDT = 1;
// Extended attributes
short EXT_ATTR_3270_UNDERLINE = 192;
short EXT_ATTR_3270_REVERSE = 128;
short EXT_ATTR_3270_BLINK = 64;
short EXT_ATTR_3270_EBCDIC = 0;
// Timeout
int NO_TIMEOUT = -1;
}
@@ -0,0 +1,54 @@
package com.ibm.eNetwork.ECL;
/**
* IBM Host Access Class Library (HACL) ECLErr checked exception facade.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLErr.
*/
public class ECLErr extends haus.nightmare.lib3270j.ecl.ECLErr {
private static final long serialVersionUID = 1L;
public ECLErr() {
super();
}
public ECLErr(String text) {
super(text);
}
public ECLErr(String location, String msgNum) {
super(location, msgNum);
}
public ECLErr(String location, String msgNum, String insert) {
super(location, msgNum, insert);
}
public ECLErr(String location, String msgNum, String insert1, String insert2) {
super(location, msgNum, insert1, insert2);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
super(location, msgNum, insert1, insert2, log);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
super(location, msgNum, insert1, insert2, insert3);
}
public ECLErr(String location, String msgNum, int reasonCode) {
super(location, msgNum, reasonCode);
}
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
super(location, msgNum, insert1, reasonCode);
}
public ECLErr(Throwable cause) {
super(cause);
}
public ECLErr(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,137 @@
package com.ibm.eNetwork.ECL;
/**
* Discrete 3270 field facade matching IBM Host On-Demand ECLField.
*/
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements ECLConstants {
private final haus.nightmare.lib3270j.ecl.ECLField delegate;
public ECLField(haus.nightmare.lib3270j.ecl.ECLField delegate) {
super(delegate != null ? delegate.getPS() : null,
delegate != null ? delegate.getStart() : 0,
delegate != null ? delegate.getDataStart() : 0,
delegate != null ? delegate.getEnd() : 0,
delegate != null ? delegate.getLength() : 0,
(byte) (delegate != null ? delegate.getAttribute() : 0));
this.delegate = delegate;
}
public int GetStart() {
return delegate != null ? delegate.GetStart() + 1 : 1;
}
public int getStart() { return GetStart(); }
public int GetStartRow() {
return delegate != null ? delegate.GetStartRow() + 1 : 1;
}
public int getStartRow() { return GetStartRow(); }
public int GetStartCol() {
return delegate != null ? delegate.GetStartCol() + 1 : 1;
}
public int getStartCol() { return GetStartCol(); }
public int GetEnd() {
return delegate != null ? delegate.GetEnd() + 1 : 1;
}
public int getEnd() { return GetEnd(); }
public int GetEndRow() {
return delegate != null ? delegate.GetEndRow() + 1 : 1;
}
public int getEndRow() { return GetEndRow(); }
public int GetEndCol() {
return delegate != null ? delegate.GetEndCol() + 1 : 1;
}
public int getEndCol() { return GetEndCol(); }
public int GetLength() {
return delegate != null ? delegate.GetLength() : 0;
}
public int getLength() { return GetLength(); }
public String GetString() {
return delegate != null ? delegate.getString() : "";
}
public String getString() { return GetString(); }
public int GetString(char[] buffer, int len) throws ECLErr {
if (buffer == null) throw new ECLErr("ECLField:GetString", "ECL0011", "1");
String text = GetString();
int count = Math.min(len, Math.min(buffer.length, text.length()));
for (int i = 0; i < count; i++) {
buffer[i] = text.charAt(i);
}
return count;
}
public void SetString(String text) throws ECLErr {
if (delegate != null) {
delegate.setText(text);
}
}
public void SetText(String text) throws ECLErr {
SetString(text);
}
public void setText(String text) {
if (delegate != null) {
delegate.setText(text);
}
}
public boolean IsModified() {
return delegate != null && delegate.isModified();
}
public boolean isModified() { return IsModified(); }
public boolean IsProtected() {
return delegate != null && delegate.isProtected();
}
public boolean isProtected() { return IsProtected(); }
public boolean IsNumeric() {
return delegate != null && delegate.isNumeric();
}
public boolean isNumeric() { return IsNumeric(); }
public boolean IsHighIntensity() {
return delegate != null && delegate.isHighIntensity();
}
public boolean isHighIntensity() { return IsHighIntensity(); }
public boolean IsPenDetectable() {
return delegate != null && delegate.isPenDetectable();
}
public boolean isPenDetectable() { return IsPenDetectable(); }
public boolean IsDisplay() {
return delegate != null && delegate.isDisplay();
}
public boolean isDisplay() { return IsDisplay(); }
public short GetAttribute() {
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
}
public short getAttribute() { return GetAttribute(); }
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
}
@@ -0,0 +1,113 @@
package com.ibm.eNetwork.ECL;
import java.util.Vector;
/**
* Collection of 3270 fields matching IBM Host On-Demand ECLFieldList.
*/
public class ECLFieldList extends Vector<ECLField> implements ECLConstants {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLFieldList delegate;
private final ECLPS ps;
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate, ECLPS ps) {
this.delegate = delegate;
this.ps = ps;
refreshFields();
}
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate) {
this(delegate, null);
}
private void refreshFields() {
clear();
if (delegate != null) {
delegate.refresh();
for (haus.nightmare.lib3270j.ecl.ECLField f : delegate.getFields()) {
add(new ECLField(f));
}
}
}
public void Refresh() {
refreshFields();
}
public void Refresh(int planes) throws ECLErr {
refreshFields();
}
public void refresh() {
refreshFields();
}
public int GetFieldCount() {
return size();
}
public int getFieldCount() { return GetFieldCount(); }
public ECLField GetFirstField() {
return GetFirstField(0);
}
public ECLField GetFirstField(int filter) {
for (ECLField f : this) {
if (matchesFilter(f, filter)) return f;
}
return null;
}
public ECLField GetNextField(ECLField current) {
return GetNextField(current, 0);
}
public ECLField GetNextField(ECLField current, int filter) {
if (current == null) return GetFirstField(filter);
int idx = indexOf(current);
if (idx >= 0) {
for (int i = idx + 1; i < size(); i++) {
ECLField f = get(i);
if (matchesFilter(f, filter)) return f;
}
}
return null;
}
public ECLField FindField(int pos) throws ECLErr {
if (ps != null && (pos < 1 || pos > ps.GetSize())) {
throw new ECLErr("ECLFieldList:FindField", "ECL0010", "1", String.valueOf(pos));
}
for (ECLField f : this) {
int start = f.GetStart();
int end = f.GetEnd();
if (start <= end) {
if (pos >= start && pos <= end) return f;
} else {
// Wrapped field
if (pos >= start || pos <= end) return f;
}
}
return null;
}
public ECLField FindField(int row, int col) throws ECLErr {
if (ps == null) return null;
int pos = ps.ConvertRowColToPos(row, col);
return FindField(pos);
}
private boolean matchesFilter(ECLField f, int filter) {
if (filter == 0 || filter == ALL_FIELDS) return true;
if ((filter & MODIFIED_FIELD) != 0 && !f.IsModified()) return false;
if ((filter & PROTECTED_FIELD) != 0 && !f.IsProtected()) return false;
if ((filter & UNPROTECTED_FIELD) != 0 && f.IsProtected()) return false;
if ((filter & NUMERIC_FIELD) != 0 && !f.IsNumeric()) return false;
if ((filter & HILIGHT_FIELD) != 0 && !f.IsHighIntensity()) return false;
if ((filter & DISPLAYABLE_FIELD) != 0 && !f.IsDisplay()) return false;
return true;
}
}
@@ -0,0 +1,39 @@
package com.ibm.eNetwork.ECL;
import java.util.Properties;
/**
* Host Print Session facade matching IBM Host On-Demand ECLHostPrintSession.
*/
public class ECLHostPrintSession extends ECLSession {
public static final String SESSION_PRINT_SESSION_LABEL_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_TYPE_DEFAULT = "5";
public static final String SESSION_PRINT_SESSION_HOST_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_HOST_PORT_DEFAULT = "23";
public static final String SESSION_PRINT_SESSION_LU_NAME_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_AUTOCONNECT_DEFAULT = "true";
public static final String SESSION_PRINT_SESSION_AUTORECONNECT_DEFAULT = "true";
public static final String SESSION_LU_TYPE = "printerLUType";
public ECLHostPrintSession(Properties properties) throws ECLErr {
super(properties);
}
public ECLHostPrintSession() throws ECLErr {
super();
}
public void cancelPrintJob() {
ECLPS ps = GetPS();
if (ps != null) {
try {
ps.SendKeys("[cancelpjob]");
} catch (Exception ignored) {}
}
}
public void CancelPrintJob() {
cancelPrintJob();
}
}
@@ -0,0 +1,130 @@
package com.ibm.eNetwork.ECL;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Operator Information Area facade matching IBM Host On-Demand ECLOIA.
*/
public class ECLOIA implements ECLConstants {
private final haus.nightmare.lib3270j.ecl.ECLOIA delegate;
private final List<ECLOIANotify> oiaNotifies = new CopyOnWriteArrayList<>();
public ECLOIA(haus.nightmare.lib3270j.ecl.ECLOIA delegate) {
this.delegate = delegate;
if (delegate != null) {
delegate.RegisterOIAEvent(new haus.nightmare.lib3270j.ecl.ECLOIAListener() {
@Override
public void OIANotifyEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent event) {
ECLOIAEvent facadeEvt = new ECLOIAEvent(event);
for (ECLOIANotify n : oiaNotifies) {
try {
n.OIANotifyEvent(facadeEvt);
} catch (Exception ignored) {}
}
}
});
}
}
public void RegisterOIAEvent(ECLOIANotify notify) {
if (notify != null && !oiaNotifies.contains(notify)) {
oiaNotifies.add(notify);
}
}
public void registerOIAEvent(ECLOIANotify notify) { RegisterOIAEvent(notify); }
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
if (delegate != null) {
delegate.RegisterOIAEvent(listener);
}
}
public void UnregisterOIAEvent(ECLOIANotify notify) {
if (notify != null) {
oiaNotifies.remove(notify);
}
}
public void unregisterOIAEvent(ECLOIANotify notify) { UnregisterOIAEvent(notify); }
public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
if (delegate != null) {
delegate.UnregisterOIAEvent(listener);
}
}
public boolean IsAlphanumeric() {
return delegate != null && delegate.IsAlphanumeric();
}
public boolean isAlphanumeric() { return IsAlphanumeric(); }
public boolean IsNumeric() {
return delegate != null && delegate.IsNumeric();
}
public boolean isNumeric() { return IsNumeric(); }
public boolean IsDBCS() {
return delegate != null && delegate.IsDBCS();
}
public boolean isDBCS() { return IsDBCS(); }
public boolean IsInsertMode() {
return delegate != null && delegate.IsInsertMode();
}
public boolean isInsertMode() { return IsInsertMode(); }
public boolean IsCommError() {
return delegate != null && delegate.IsCommError();
}
public boolean isCommError() { return IsCommError(); }
public boolean IsKeyLocked() {
return delegate != null && delegate.isKeyLocked();
}
public boolean isKeyLocked() { return IsKeyLocked(); }
public boolean IsMessageWaiting() {
return delegate != null && delegate.IsMessageWaiting();
}
public boolean isMessageWaiting() { return IsMessageWaiting(); }
public int GetInputInhibited() {
return delegate != null ? delegate.getInputInhibited() : 0;
}
public int getInputInhibited() { return GetInputInhibited(); }
public int GetAlphanumericType() {
return delegate != null ? delegate.getAlphanumericType() : 0;
}
public int getAlphanumericType() { return GetAlphanumericType(); }
public boolean WaitForInput(long timeout) {
return delegate != null && delegate.WaitForInput(timeout);
}
public boolean WaitForSystemAvailable(long timeout) {
return delegate != null && delegate.WaitForSystemAvailable(timeout);
}
public boolean WaitForAppAvailable(long timeout) {
return delegate != null && delegate.WaitForAppAvailable(timeout);
}
public boolean WaitForTransition(long timeout) {
return delegate != null && delegate.WaitForTransition(timeout);
}
public haus.nightmare.lib3270j.ecl.ECLOIA getDelegate() { return delegate; }
}
@@ -0,0 +1,80 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Event object dispatched on Operator Information Area (ECLOIA) status changes matching IBM HoD.
*/
public class ECLOIAEvent extends ECLEvent {
public String data;
public long newState;
public long changedMask;
private int inputInhibited = 0;
private int alphanumericType = 0;
private boolean insertMode = false;
public ECLOIAEvent(Object source, long newState, long changedMask, String data) {
super(source);
this.newState = newState;
this.changedMask = changedMask;
this.data = data != null ? data : "";
}
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
boolean insertMode, String statusString) {
super(source);
this.inputInhibited = inputInhibited;
this.alphanumericType = alphanumericType;
this.insertMode = insertMode;
this.data = statusString != null ? statusString : "";
this.newState = inputInhibited;
this.changedMask = eventType;
}
public ECLOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent delegate) {
super(delegate != null ? delegate.getSource() : null);
if (delegate != null) {
this.inputInhibited = delegate.getInputInhibited();
this.alphanumericType = delegate.getAlphanumericType();
this.insertMode = delegate.isInsertMode();
this.data = delegate.getStatusString();
this.newState = delegate.getInputInhibited();
this.changedMask = delegate.getEventType();
}
}
public ECLOIA GetOIA() {
Object src = GetSource();
if (src instanceof ECLOIA) {
return (ECLOIA) src;
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLOIA) {
return new ECLOIA((haus.nightmare.lib3270j.ecl.ECLOIA) src);
}
return null;
}
public ECLOIA getOIA() { return GetOIA(); }
public int GetInputInhibited() { return inputInhibited; }
public int getInputInhibited() { return inputInhibited; }
public int GetAlphanumericType() { return alphanumericType; }
public int getAlphanumericType() { return alphanumericType; }
public boolean IsInsertMode() { return insertMode; }
public boolean isInsertMode() { return insertMode; }
public boolean IsInputInhibited() { return inputInhibited != 0; }
public boolean isInputInhibited() { return IsInputInhibited(); }
public String GetStatusString() { return data; }
public String getStatusString() { return data; }
@Override
public String toString() {
return "ECLOIAEvent: changedMask=0x" + Long.toHexString(this.changedMask) +
", newState=0x" + Long.toHexString(this.newState) + ", data=" + this.data;
}
}
@@ -0,0 +1,17 @@
package com.ibm.eNetwork.ECL;
/**
* Operator Information Area notification interface matching IBM Host On-Demand ECLOIANotify.
*/
public interface ECLOIANotify {
void NotifyEvent(ECLOIA oia, int inhibitReason, int state, String text);
default void NotifyStop(ECLOIA oia, int reason) {}
default void NotifyError(ECLOIA oia, ECLErr err) {}
default void OIANotifyStop(ECLOIA oia, int reason) { NotifyStop(oia, reason); }
default void OIANotifyError(ECLOIA oia, ECLErr err) { NotifyError(oia, err); }
default void OIANotifyEvent(ECLOIAEvent event) {
if (event != null) {
NotifyEvent(event.GetOIA(), event.GetInputInhibited(), 0, "");
}
}
}
@@ -0,0 +1,514 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLPSListener;
import java.io.Serializable;
/**
* Presentation Space facade matching IBM Host On-Demand ECLPS.
* Provides 1-based indexing, method signature parity, and checked ECLErr exception semantics.
*/
public class ECLPS implements ECLConstants, Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLPS delegate;
public ECLPS(haus.nightmare.lib3270j.ecl.ECLPS delegate) {
this.delegate = delegate;
}
public ECLPS(haus.nightmare.lib3270j.ecl.ECLSession session) {
this(session != null ? session.GetPS() : null);
}
public ECLPS(ECLSession session) {
this(session != null ? session.getDelegate() : null);
}
public haus.nightmare.lib3270j.ecl.ECLPS getDelegate() {
return delegate;
}
// ========== Dimensions & Position Conversion (1-based) ==========
public int GetSize() {
return delegate != null ? delegate.GetSize() : 0;
}
public int getSize() { return GetSize(); }
public int GetRows() {
return delegate != null ? delegate.GetRows() : 0;
}
public int getRows() { return GetRows(); }
public int GetCols() {
return delegate != null ? delegate.GetCols() : 0;
}
public int getCols() { return GetCols(); }
public int GetSizeRows() { return GetRows(); }
public int GetSizeCols() { return GetCols(); }
public int ConvertRowColToPos(int row, int col) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertRowColToPos(row, col);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertRowColToPos(int row, int col) throws ECLErr {
return ConvertRowColToPos(row, col);
}
public int ConvertPosToRow(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToRow(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertPosToRow(int pos) throws ECLErr {
return ConvertPosToRow(pos);
}
public int ConvertPosToCol(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToCol(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertPosToCol(int pos) throws ECLErr {
return ConvertPosToCol(pos);
}
public void ConvertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
if (delegate != null) {
try {
delegate.ConvertPosToRowCol(pos, rowCol);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void convertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
ConvertPosToRowCol(pos, rowCol);
}
public int[] ConvertPosToRowCol(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToRowCol(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return new int[]{1, 1};
}
public int[] convertPosToRowCol(int pos) throws ECLErr {
return ConvertPosToRowCol(pos);
}
// ========== Cursor Operations (1-based) ==========
public int GetCursorPos() {
return delegate != null ? delegate.GetCursorPos() : 1;
}
public int getCursorPos() { return GetCursorPos(); }
public int GetCursorRow() {
return delegate != null ? delegate.GetCursorRow() : 1;
}
public int getCursorRow() { return GetCursorRow(); }
public int GetCursorCol() {
return delegate != null ? delegate.GetCursorCol() : 1;
}
public int getCursorCol() { return GetCursorCol(); }
public void SetCursorPos(int pos) throws ECLErr {
if (delegate != null) {
try {
delegate.SetCursorPos(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setCursorPos(int pos) throws ECLErr {
SetCursorPos(pos);
}
public void SetCursorPos(int row, int col) throws ECLErr {
if (delegate != null) {
try {
delegate.SetCursorPos(row, col);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setCursorPos(int row, int col) throws ECLErr {
SetCursorPos(row, col);
}
// ========== GetString & SetString Operations (1-based) ==========
public int GetString(char[] buffer, int len, int pos) throws ECLErr {
return GetString(buffer, len, pos, len);
}
public int GetString(char[] buffer, int len, int pos, int length) throws ECLErr {
if (delegate != null) {
try {
return delegate.GetString(buffer, len, pos, length);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 0;
}
public int GetString(char[] buffer, int len, int row, int col, int length) throws ECLErr {
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
}
public String GetString(int pos, int length) throws ECLErr {
if (delegate != null) {
try {
return delegate.GetString(pos, length);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return "";
}
public String getString(int pos, int length) throws ECLErr {
return GetString(pos, length);
}
public String GetString(int row, int col, int length) throws ECLErr {
return GetString(ConvertRowColToPos(row, col), length);
}
public String getString(int row, int col, int length) throws ECLErr {
return GetString(row, col, length);
}
public void SetString(String text, int pos) throws ECLErr {
if (delegate != null) {
try {
delegate.SetString(text, pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setString(String text, int pos) throws ECLErr {
SetString(text, pos);
}
public void SetString(String text, int row, int col) throws ECLErr {
SetString(text, ConvertRowColToPos(row, col));
}
public void setString(String text, int row, int col) throws ECLErr {
SetString(text, row, col);
}
public void SetString(String text) throws ECLErr {
SetString(text, GetCursorPos());
}
public void setString(String text) throws ECLErr {
SetString(text);
}
public void SetText(String text, int pos) throws ECLErr {
SetString(text, pos);
}
public void SetText(String text, int row, int col) throws ECLErr {
SetString(text, row, col);
}
public void setText(String text, int pos) {
if (delegate != null) delegate.setText(text, pos);
}
public void setText(String text, int row, int col) {
if (delegate != null) delegate.setText(text, row, col);
}
// ========== Search Operations (1-based) ==========
public int SearchText(String text, int dir, int startPos) {
return delegate != null ? delegate.SearchText(text, dir, startPos) : 0;
}
public int SearchText(String text, int dir) {
return delegate != null ? delegate.SearchText(text, dir) : 0;
}
public int SearchText(String text) {
return delegate != null ? delegate.SearchText(text) : 0;
}
public int SearchText(String text, int row, int col, int dir) throws ECLErr {
return SearchText(text, dir, ConvertRowColToPos(row, col));
}
public int SearchString(String text, int dir, int startPos) {
return SearchText(text, dir, startPos);
}
public int SearchString(String text, int dir) {
return SearchText(text, dir);
}
public int SearchString(String text) {
return SearchText(text);
}
public int SearchString(String text, int row, int col, int dir) throws ECLErr {
return SearchText(text, row, col, dir);
}
public int SearchPS(String text) {
return delegate != null ? delegate.SearchPS(text) : 0;
}
public int SearchPS(String text, int startPos) {
return delegate != null ? delegate.SearchPS(text, startPos) : 0;
}
public int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
return delegate != null ? delegate.SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap) : 0;
}
// ========== Keystrokes & Mnemonics ==========
public void SendKeys(String keys) {
if (delegate != null) delegate.SendKeys(keys);
}
public void sendKeys(String keys) { SendKeys(keys); }
public void SendKeys(String keys, int pos) {
if (delegate != null) delegate.SendKeys(keys, pos);
}
public void sendKeys(String keys, int pos) { SendKeys(keys, pos); }
public void SendKeys(String keys, int row, int col) {
if (delegate != null) delegate.SendKeys(keys, row, col);
}
public void sendKeys(String keys, int row, int col) { SendKeys(keys, row, col); }
public void SendCharacters(String keys) {
if (delegate != null) delegate.SendCharacters(keys);
}
public void sendCharacters(String keys) { SendCharacters(keys); }
public void SendCharacters(String keys, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, delayMs);
}
public void sendCharacters(String keys, int delayMs) { SendCharacters(keys, delayMs); }
public void SendCharacters(String keys, int pos, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, pos, delayMs);
}
public void sendCharacters(String keys, int pos, int delayMs) { SendCharacters(keys, pos, delayMs); }
public void SendCharacters(String keys, int row, int col, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, row, col, delayMs);
}
public void sendCharacters(String keys, int row, int col, int delayMs) { SendCharacters(keys, row, col, delayMs); }
public String[] GetSendKeyMnemonics() {
return delegate != null ? delegate.GetSendKeyMnemonics() : new String[0];
}
public String[] getSendKeyMnemonics() { return GetSendKeyMnemonics(); }
// ========== Field Operations ==========
public ECLFieldList GetFieldList() {
haus.nightmare.lib3270j.ecl.ECLFieldList fl = delegate != null ? delegate.GetFieldList() : null;
return new ECLFieldList(fl, this);
}
public ECLFieldList getFieldList() { return GetFieldList(); }
public ECLField GetField(int pos) throws ECLErr {
return GetFieldList().FindField(pos);
}
public ECLField getField(int pos) throws ECLErr { return GetField(pos); }
public ECLField GetField(int row, int col) throws ECLErr {
return GetFieldList().FindField(row, col);
}
public ECLField getField(int row, int col) throws ECLErr { return GetField(row, col); }
public ECLField GetFirstField() {
return GetFieldList().GetFirstField();
}
public ECLField getFirstField() { return GetFirstField(); }
public ECLField GetNextField(ECLField prev) {
return GetFieldList().GetNextField(prev);
}
public ECLField getNextField(ECLField prev) { return GetNextField(prev); }
// ========== Listener Overloads for HoD Interfaces ==========
public void RegisterPSEvent(ECLPSNotify notify) {
if (delegate != null) delegate.RegisterPSEvent(notify);
}
public void registerPSEvent(ECLPSNotify notify) { RegisterPSEvent(notify); }
public void RegisterPSEvent(ECLPSNotify notify, int eventType) {
if (delegate != null) delegate.RegisterPSEvent(notify, eventType);
}
public void registerPSEvent(ECLPSNotify notify, int eventType) { RegisterPSEvent(notify, eventType); }
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc) {
if (delegate != null) delegate.RegisterPSEvent(notify, desc);
}
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { RegisterPSEvent(notify, desc); }
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
if (delegate != null) delegate.RegisterPSEvent(notify, desc, eventType);
}
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { RegisterPSEvent(notify, desc, eventType); }
public void UnregisterPSEvent(ECLPSNotify notify) {
if (delegate != null) delegate.UnregisterPSEvent(notify);
}
public void unregisterPSEvent(ECLPSNotify notify) { UnregisterPSEvent(notify); }
public void RegisterPSEvent(ECLPSListener listener) {
if (delegate != null) delegate.RegisterPSEvent(listener);
}
public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); }
public void UnregisterPSEvent(ECLPSListener listener) {
if (delegate != null) delegate.UnregisterPSEvent(listener);
}
public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); }
// ========== Synchronization & Waits ==========
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
return delegate != null && delegate.WaitForScreen(desc, timeoutMs);
}
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
public boolean WaitForScreen(ECLScreenDesc desc) {
return delegate != null && delegate.WaitForScreen(desc);
}
public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); }
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
return delegate != null && delegate.WaitWhileScreen(desc, timeoutMs);
}
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); }
public boolean WaitWhileScreen(ECLScreenDesc desc) {
return delegate != null && delegate.WaitWhileScreen(desc);
}
public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); }
public boolean WaitForString(String text, long timeoutMs) {
return delegate != null && delegate.WaitForString(text, timeoutMs);
}
public boolean waitForString(String text, long timeoutMs) { return WaitForString(text, timeoutMs); }
public boolean WaitForString(String text) {
return delegate != null && delegate.WaitForString(text);
}
public boolean waitForString(String text) { return WaitForString(text); }
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
return delegate != null && delegate.WaitForString(text, row, col, timeoutMs);
}
public boolean waitForString(String text, int row, int col, long timeoutMs) { return WaitForString(text, row, col, timeoutMs); }
public boolean WaitForCursor(int row, int col, long timeoutMs) {
return delegate != null && delegate.WaitForCursor(row, col, timeoutMs);
}
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
// ========== Screen Update Inquiries ==========
public ECLPSUpdate GetScreen(int startRow, int startCol, int length) throws ECLErr {
return GetScreen(startRow, startCol, length, false);
}
public ECLPSUpdate GetScreen(int startRow, int startCol, int length, boolean fillNulls) throws ECLErr {
int pos = ConvertRowColToPos(startRow, startCol);
char[] buf = new char[length];
GetString(buf, length, pos, length);
if (fillNulls) {
for (int i = 0; i < buf.length; i++) {
if (buf[i] == '\0') buf[i] = ' ';
}
}
int endPos = pos + length - 1;
int endRow = ConvertPosToRow(endPos);
int endCol = ConvertPosToCol(endPos);
return new ECLPSUpdate(this, startRow, startCol, endRow, endCol, new String(buf));
}
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
return delegate.GetScreen(buffer, len, row, col, length, plane);
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.ECL;
/**
* Presentation space notification interface matching IBM Host On-Demand ECLPSNotify.
*/
public interface ECLPSNotify {
void NotifyEvent(ECLPS ps);
default void NotifyStop(ECLPS ps, int reason) {}
default void NotifyError(ECLPS ps, ECLErr err) {}
}
@@ -0,0 +1,90 @@
package com.ibm.eNetwork.ECL;
import java.io.Serializable;
/**
* Conforms to IBM Host On-Demand ECLPSUpdate.
*/
public class ECLPSUpdate implements Serializable {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final int start;
private final int end;
private final boolean fullUpdate;
private final String text;
private final transient ECLPS ps;
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol,
int start, int end, boolean fullUpdate, String text) {
this.ps = ps;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.start = start;
this.end = end;
this.fullUpdate = fullUpdate;
this.text = text != null ? text : "";
this.delegate = null;
}
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, String text) {
this(ps, startRow, startCol, endRow, endCol, 0, 0, false, text);
}
public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, String text) {
this(null, startRow, startCol, endRow, endCol, 0, 0, false, text);
}
public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate) {
this.delegate = delegate;
this.ps = (delegate != null && delegate.getPS() != null) ? new ECLPS(delegate.getPS()) : null;
this.startRow = delegate != null ? delegate.getStartRow() : 0;
this.startCol = delegate != null ? delegate.getStartCol() : 0;
this.endRow = delegate != null ? delegate.getEndRow() : 0;
this.endCol = delegate != null ? delegate.getEndCol() : 0;
this.start = delegate != null ? delegate.getStart() : 0;
this.end = delegate != null ? delegate.getEnd() : 0;
this.fullUpdate = delegate != null && delegate.isFullUpdate();
this.text = delegate != null ? delegate.getText() : "";
}
public ECLPS getPS() { return ps; }
public ECLPS GetPS() { return ps; }
public int getStartRow() { return startRow; }
public int GetStartRow() { return startRow + 1; }
public int getStartCol() { return startCol; }
public int GetStartCol() { return startCol + 1; }
public int getEndRow() { return endRow; }
public int GetEndRow() { return endRow + 1; }
public int getEndCol() { return endCol; }
public int GetEndCol() { return endCol + 1; }
public int getStart() { return start; }
public int GetStart() { return start + 1; }
public int getEnd() { return end; }
public int GetEnd() { return end + 1; }
public boolean isFullUpdate() { return fullUpdate; }
public boolean IsFullUpdate() { return fullUpdate; }
public String getText() { return text; }
public String GetText() { return text; }
@Override
public String toString() {
return String.format("ECLPSUpdate[start=(%d,%d), end=(%d,%d), full=%b]",
startRow, startCol, endRow, endCol, fullUpdate);
}
}
@@ -0,0 +1,21 @@
package com.ibm.eNetwork.ECL;
import java.io.Serializable;
/**
* Screen descriptor matching engine facade matching IBM Host On-Demand ECLScreenDesc.
*/
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc implements Serializable {
private static final long serialVersionUID = 1L;
public ECLScreenDesc() {
super();
}
public boolean Matches(ECLPS ps, ECLOIA oia) {
haus.nightmare.lib3270j.ecl.ECLPS p = (ps != null) ? ps.getDelegate() : null;
haus.nightmare.lib3270j.ecl.ECLOIA o = (oia != null) ? oia.getDelegate() : null;
return super.Matches(p, o);
}
}
@@ -0,0 +1,8 @@
package com.ibm.eNetwork.ECL;
/**
* Listener interface for ECLScreenReco matching notifications matching IBM HoD.
*/
public interface ECLScreenNotify {
void screenMatching(ECLScreenRecoEvent event);
}
@@ -0,0 +1,78 @@
package com.ibm.eNetwork.ECL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Screen recognition facade matching IBM Host On-Demand ECLScreenReco.
*/
public class ECLScreenReco {
private final haus.nightmare.lib3270j.ecl.ECLScreenReco delegate;
private final ECLPS ps;
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
public ECLScreenReco() {
this((ECLPS) null);
}
public ECLScreenReco(ECLSession session) {
this(session != null ? session.GetPS() : null);
}
public ECLScreenReco(ECLPS ps) {
this(ps, (ECLOIA) null);
}
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
this.ps = ps;
this.delegate = new haus.nightmare.lib3270j.ecl.ECLScreenReco(ps != null ? ps.getDelegate() : null,
oia != null ? oia.getDelegate() : null);
}
public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLScreenReco delegate) {
this.delegate = delegate;
this.ps = null;
}
public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
if (desc != null && notify != null) {
notifyMap.put(desc, notify);
if (delegate != null) {
delegate.RegisterScreen(desc, event -> notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps)));
}
}
}
public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
RegisterScreen(desc, notify);
}
public void UnregisterScreen(ECLScreenDesc desc) {
if (desc != null) {
notifyMap.remove(desc);
if (delegate != null) {
delegate.UnregisterScreen(desc);
}
}
}
public void unregisterScreen(ECLScreenDesc desc) {
UnregisterScreen(desc);
}
public boolean IsMatch(ECLScreenDesc desc) {
if (delegate != null) {
return delegate.IsMatch(desc);
}
return false;
}
public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) {
if (ps == null) return false;
if (desc == null) return true;
return desc.Matches(ps, (ECLOIA) null);
}
public haus.nightmare.lib3270j.ecl.ECLScreenReco getDelegate() { return delegate; }
}
@@ -0,0 +1,39 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Event delivered when an ECLScreenDesc matches the Presentation Space.
*/
public class ECLScreenRecoEvent extends ECLEvent {
private final ECLScreenReco source;
private final ECLScreenDesc screenDesc;
private final ECLPS ps;
public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) {
super(source);
this.source = source;
this.screenDesc = screenDesc;
this.ps = ps;
}
public ECLScreenRecoEvent(Object source, ECLPS ps) {
super(source);
this.source = (source instanceof ECLScreenReco) ? (ECLScreenReco) source : null;
this.screenDesc = null;
this.ps = ps;
}
@Override
public Object GetSource() { return source != null ? source : super.GetSource(); }
public ECLScreenReco GetScreenReco() { return source; }
public ECLScreenReco getScreenReco() { return source; }
public ECLScreenDesc GetScreenDesc() { return screenDesc; }
public ECLScreenDesc getScreenDesc() { return screenDesc; }
public ECLPS GetPS() { return ps; }
public ECLPS getPS() { return ps; }
}
@@ -0,0 +1,190 @@
package com.ibm.eNetwork.ECL;
import java.io.IOException;
import java.util.Properties;
/**
* Top-level session facade matching IBM Host On-Demand ECLSession.
* Extends ECLConnection and provides access to Presentation Space (ECLPS),
* Operator Information Area (ECLOIA), File Transfer (ECLXfer), and Screen Recognition (ECLScreenReco).
*/
public class ECLSession extends ECLConnection implements ECLConstants {
public static final String SESSION_NAME = "SESSION_NAME";
public static final String SESSION_LABEL = "SESSION_LABEL";
public static final String SESSION_TYPE = "SESSION_TYPE";
public static final String SESSION_HOST = "SESSION_HOST";
public static final String SESSION_HOST_PORT = "SESSION_HOST_PORT";
public static final String SESSION_PORT = "SESSION_PORT";
public static final String SESSION_PS_SIZE = "SESSION_PS_SIZE";
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
public static final String SESSION_SSL = "SESSION_SSL";
public static final String SESSION_SECURITY_PROTOCOL = "SESSION_SECURITY_PROTOCOL";
public static final String SESSION_TLS_PROTOCOL_VERSION = "tlsProtocolVersion";
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
public static final String SESSION_TN3270E = "SESSION_TN3270E";
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE";
public static final String KEY_KEEPALIVE_TYPE = "keepAliveType";
public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout";
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
public static final String BLOCK_PASTE = "blockPaste";
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
private final haus.nightmare.lib3270j.ecl.ECLSession delegate;
private final ECLPS ps;
private final ECLOIA oia;
private final ECLXfer xfer;
private final ECLScreenReco screenReco;
public ECLSession(Properties properties) throws ECLErr {
super(properties);
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(properties);
this.ps = new ECLPS(delegate.getPS());
this.oia = new ECLOIA(delegate.getOIA());
this.xfer = new ECLXfer(delegate.getXfer());
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLSession() throws ECLErr {
this(new Properties());
}
public ECLSession(String host, int port) throws ECLErr {
super(host, port);
Properties props = new Properties();
if (host != null) props.setProperty(SESSION_HOST, host);
props.setProperty(SESSION_PORT, String.valueOf(port));
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(props);
this.ps = new ECLPS(delegate.getPS());
this.oia = new ECLOIA(delegate.getOIA());
this.xfer = new ECLXfer(delegate.getXfer());
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLSession(haus.nightmare.lib3270j.ecl.ECLSession delegate) {
super(delegate);
this.delegate = delegate;
this.ps = new ECLPS(delegate != null ? delegate.getPS() : null);
this.oia = new ECLOIA(delegate != null ? delegate.getOIA() : null);
this.xfer = new ECLXfer(delegate != null ? delegate.getXfer() : null);
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLPS GetPS() {
return ps;
}
public ECLPS getPS() {
return ps;
}
public ECLOIA GetOIA() {
return oia;
}
public ECLOIA getOIA() {
return oia;
}
public ECLXfer GetXfer() {
return xfer;
}
public ECLXfer getXfer() {
return xfer;
}
public ECLScreenReco GetScreenReco() {
return screenReco;
}
public ECLScreenReco getScreenReco() {
return screenReco;
}
public ECLConnection GetConnection() {
return this;
}
public ECLConnection getConnection() {
return this;
}
public haus.nightmare.lib3270j.ecl.ECLSession getDelegate() {
return delegate;
}
@Override
public void StartCommunication() throws IOException {
if (delegate != null) {
delegate.StartCommunication();
} else {
super.StartCommunication();
}
}
@Override
public void StopCommunication() {
if (delegate != null) {
delegate.StopCommunication();
} else {
super.StopCommunication();
}
}
public boolean StartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null);
}
public boolean startCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout);
}
public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
if (delegate != null) {
try {
return delegate.StartCommunicationWithBlocking(timeout, desc);
} catch (IOException e) {
throw new ECLErr("ECLSession.StartCommunicationWithBlocking", "ECL0001", e.getMessage());
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return false;
}
public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout, desc);
}
public boolean RestartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
if (delegate != null) {
try {
return delegate.RestartCommunicationWithBlocking(timeout);
} catch (IOException e) {
throw new ECLErr("ECLSession.RestartCommunicationWithBlocking", "ECL0001", e.getMessage());
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return false;
}
public boolean restartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return RestartCommunicationWithBlocking(timeout);
}
public void dispose() {
if (delegate != null) {
delegate.dispose();
}
}
}
@@ -0,0 +1,54 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLXferListener;
/**
* File transfer facade matching IBM Host On-Demand ECLXfer.
*/
public class ECLXfer {
private final haus.nightmare.lib3270j.ecl.ECLXfer delegate;
public ECLXfer(ECLSession session, Boolean bidi) {
this.delegate = (session != null && session.getDelegate() != null)
? session.getDelegate().GetXfer() : null;
}
public ECLXfer(haus.nightmare.lib3270j.ecl.ECLXfer delegate) {
this.delegate = delegate;
}
public void SendFile(String localFile, String hostFile, String options) throws ECLErr {
if (delegate != null) {
int rc = delegate.SendFile(localFile, hostFile, options);
if (rc != 0) {
throw new ECLErr("ECLXfer:SendFile", "ECL0150", String.valueOf(rc));
}
}
}
public void ReceiveFile(String localFile, String hostFile, String options) throws ECLErr {
if (delegate != null) {
int rc = delegate.ReceiveFile(localFile, hostFile, options);
if (rc != 0) {
throw new ECLErr("ECLXfer:ReceiveFile", "ECL0150", String.valueOf(rc));
}
}
}
public void Cancel() {
if (delegate != null) {
delegate.Cancel();
}
}
public void RegisterXferEvent(ECLXferListener listener) {
// Can bridge listener if needed
}
public void UnregisterXferEvent(ECLXferListener listener) {
// Can bridge listener if needed
}
public haus.nightmare.lib3270j.ecl.ECLXfer getDelegate() { return delegate; }
}
@@ -0,0 +1,25 @@
package com.ibm.eNetwork.ECL.event;
/**
* Base event class matching IBM Host On-Demand ECLEvent.
*/
public class ECLEvent {
private final Object originator;
public ECLEvent(Object object) {
this.originator = object;
}
public Object GetSource() {
return this.originator;
}
public Object getSource() {
return this.originator;
}
@Override
public String toString() {
return super.toString() + ": " + (this.originator != null ? this.originator.toString() : "null");
}
}
@@ -0,0 +1,126 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLPS;
import com.ibm.eNetwork.ECL.ECLPSUpdate;
/**
* Presentation Space event class matching IBM Host On-Demand ECLPSEvent.
*/
public class ECLPSEvent extends ECLEvent {
public static final int USER_EVENTS = 1;
public static final int HOST_EVENTS = 2;
public static final int ALL_EVENTS = 3;
private int type = 0;
private int start = 1;
private int end = 1;
private int startRow = 1;
private int startCol = 1;
private int endRow = 1;
private int endCol = 1;
private int oldCursorPos = 1;
private int newCursorPos = 1;
private boolean fullUpdate = true;
private boolean cursorVisible = true;
private int ringCounter = 0;
private ECLPSUpdate psUpdate = null;
private haus.nightmare.lib3270j.ecl.ECLPSEvent delegate = null;
public ECLPSEvent(Object source) {
super(source);
if (source instanceof ECLPS) {
ECLPS ps = (ECLPS) source;
this.end = ps.GetSize();
this.endRow = ps.GetRows();
this.endCol = ps.GetCols();
this.newCursorPos = ps.GetCursorPos();
} else if (source instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
haus.nightmare.lib3270j.ecl.ECLPS ps = (haus.nightmare.lib3270j.ecl.ECLPS) source;
this.end = ps.GetSize();
this.endRow = ps.GetRows();
this.endCol = ps.GetCols();
this.newCursorPos = ps.GetCursorPos();
}
}
public ECLPSEvent(Object source, int type) {
this(source);
this.type = type;
}
public ECLPSEvent(haus.nightmare.lib3270j.ecl.ECLPSEvent delegate) {
super(delegate != null ? delegate.getSource() : null);
this.delegate = delegate;
if (delegate != null) {
this.type = delegate.getType();
this.start = delegate.getStart();
this.end = delegate.getEnd();
this.startRow = delegate.getStartRow();
this.startCol = delegate.getStartCol();
this.endRow = delegate.getEndRow();
this.endCol = delegate.getEndCol();
this.oldCursorPos = delegate.getOldCursorAddress() + 1;
this.newCursorPos = delegate.getNewCursorAddress() + 1;
this.fullUpdate = delegate.isFullUpdate();
this.cursorVisible = delegate.isCursorVisible();
this.ringCounter = delegate.getRingCounter();
if (delegate.getPSUpdate() != null) {
this.psUpdate = new ECLPSUpdate(delegate.getPSUpdate());
}
}
}
public int GetType() { return type; }
public int getType() { return type; }
public ECLPS GetPS() {
Object src = GetSource();
if (src instanceof ECLPS) {
return (ECLPS) src;
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
return new ECLPS((haus.nightmare.lib3270j.ecl.ECLPS) src);
}
return null;
}
public void SetStart(int start) { this.start = start; }
public int GetStart() { return start; }
public int getStart() { return start; }
public int GetStartRow() { return startRow; }
public int getStartRow() { return startRow; }
public int GetStartCol() { return startCol; }
public int getStartCol() { return startCol; }
public void SetEnd(int end) { this.end = end; }
public int GetEnd() { return end; }
public int getEnd() { return end; }
public int GetEndRow() { return endRow; }
public int getEndRow() { return endRow; }
public int GetEndCol() { return endCol; }
public int getEndCol() { return endCol; }
public int GetOldCursorPos() { return oldCursorPos; }
public int getOldCursorPos() { return oldCursorPos; }
public int GetNewCursorPos() { return newCursorPos; }
public int getNewCursorPos() { return newCursorPos; }
public boolean IsFullUpdate() { return fullUpdate; }
public boolean isFullUpdate() { return fullUpdate; }
public boolean IsCursorVisible() { return cursorVisible; }
public boolean isCursorVisible() { return cursorVisible; }
public int GetRingCounter() { return ringCounter; }
public int getRingCounter() { return ringCounter; }
public ECLPSUpdate GetPSUpdate() { return psUpdate; }
public ECLPSUpdate getPSUpdate() { return psUpdate; }
public haus.nightmare.lib3270j.ecl.ECLPSEvent getDelegate() { return delegate; }
}
@@ -0,0 +1,13 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLPS;
/**
* Presentation Space listener interface matching IBM Host On-Demand ECLPSListener.
*/
public interface ECLPSListener {
void PSNotifyEvent(ECLPSEvent event);
default void PSNotifyStop(ECLPS ps, int reason) {}
default void PSNotifyError(ECLPS ps, ECLErr err) {}
}
@@ -0,0 +1,48 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLXfer;
/**
* File transfer event matching IBM Host On-Demand ECLXferEvent.
*/
public class ECLXferEvent extends ECLEvent {
private final String sourceFile;
private final String targetFile;
private final boolean receive;
private final long unitsDone;
private final long unitsToDo;
public ECLXferEvent(Object source, long unitsDone, String sourceFile, String targetFile, boolean receive) {
this(source, unitsDone, -1L, sourceFile, targetFile, receive);
}
public ECLXferEvent(Object source, long unitsDone, long unitsToDo, String sourceFile, String targetFile, boolean receive) {
super(source);
this.unitsDone = unitsDone;
this.unitsToDo = unitsToDo;
this.sourceFile = sourceFile;
this.targetFile = targetFile;
this.receive = receive;
}
public ECLXfer GetECLXfer() {
Object src = GetSource();
return (src instanceof ECLXfer) ? (ECLXfer) src : null;
}
public String GetSourceFile() { return sourceFile; }
public String getSourceFile() { return sourceFile; }
public String GetTargetFile() { return targetFile; }
public String getTargetFile() { return targetFile; }
public long GetBytesSent() { return unitsDone; }
public long getBytesSent() { return unitsDone; }
public long GetBytesToSend() { return unitsToDo; }
public long getBytesToSend() { return unitsToDo; }
public boolean IsReceive() { return receive; }
public boolean isReceive() { return receive; }
}
@@ -0,0 +1,13 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLXfer;
/**
* File transfer listener interface matching IBM Host On-Demand ECLXferListener.
*/
public interface ECLXferListener {
void XferNotifyEvent(ECLXferEvent event);
default void XferNotifyStop(ECLXfer xfer, int reason) {}
default void XferNotifyError(ECLXfer xfer, ECLErr err) {}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.ECL.macrovariable;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroExpressionParser.
*/
public class MacroExpressionParser extends haus.nightmare.lib3270j.macro.variable.MacroExpressionParser implements Serializable {
private static final long serialVersionUID = 1L;
public MacroExpressionParser() {
super();
}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.ECL.macrovariable;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroVariables.
*/
public class MacroVariables extends haus.nightmare.lib3270j.macro.variable.MacroVariables implements Serializable {
private static final long serialVersionUID = 1L;
public MacroVariables() {
super();
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDAttrib drop-in facade.
*/
public class ECLSDAttrib extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib {
private static final long serialVersionUID = 1L;
public ECLSDAttrib() {
super();
}
public ECLSDAttrib(char attrib, int row, int col, int plane) {
super(attrib, row, col, plane);
}
public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) {
super(attrib, row, col, plane, optional);
}
}
@@ -0,0 +1,24 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDBlock drop-in facade.
*/
public class ECLSDBlock extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock {
private static final long serialVersionUID = 1L;
public ECLSDBlock() {
super();
}
public ECLSDBlock(int srow, int scol, boolean caseSense, boolean optional) {
super(srow, scol, caseSense, optional);
}
public ECLSDBlock(int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
super(srow, scol, erow, ecol, caseSense, optional);
}
public ECLSDBlock(String[] textLines, int srow, int scol, boolean caseSense) {
super(textLines, srow, scol, caseSense);
}
}
@@ -0,0 +1,16 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDBlockString drop-in facade.
*/
public class ECLSDBlockString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlockString {
private static final long serialVersionUID = 1L;
public ECLSDBlockString() {
super();
}
public ECLSDBlockString(String text) {
super(text);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDChecksum drop-in facade.
*/
public class ECLSDChecksum extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDChecksum {
private static final long serialVersionUID = 1L;
public ECLSDChecksum() {
super();
}
public ECLSDChecksum(int value) {
super(value);
}
public ECLSDChecksum(int value, boolean optional) {
super(value, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDCursor drop-in facade.
*/
public class ECLSDCursor extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDCursor {
private static final long serialVersionUID = 1L;
public ECLSDCursor() {
super();
}
public ECLSDCursor(int row, int col) {
super(row, col);
}
public ECLSDCursor(int row, int col, boolean optional) {
super(row, col, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDFields drop-in facade.
*/
public class ECLSDFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDFields {
private static final long serialVersionUID = 1L;
public ECLSDFields() {
super();
}
public ECLSDFields(int num) {
super(num);
}
public ECLSDFields(int num, boolean optional) {
super(num, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDInputFields drop-in facade.
*/
public class ECLSDInputFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDInputFields {
private static final long serialVersionUID = 1L;
public ECLSDInputFields() {
super();
}
public ECLSDInputFields(int num) {
super(num);
}
public ECLSDInputFields(int num, boolean optional) {
super(num, optional);
}
}

Some files were not shown because too many files have changed in this diff Show More