Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
14bf7ba4b1
|
|||
|
bdfe6eec2a
|
|||
|
c28c097e25
|
|||
|
46a022d86b
|
|||
|
59925c2787
|
|||
|
f8fc0e4b22
|
|||
|
dbcb0aa1b6
|
|||
|
50a4279446
|
|||
|
6331e0108b
|
|||
|
79d493c24d
|
|||
|
74e30d1ead
|
|||
|
cbb310b889
|
+18
-10
@@ -9,8 +9,12 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build JAR & Run Tests
|
||||
name: Build JAR & Run Tests (Java ${{ matrix.java }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java: [ '11', '17', '21' ]
|
||||
|
||||
steps:
|
||||
- name: Configure Dynamic DNS Resolvers
|
||||
@@ -23,21 +27,21 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Java JDK (Temurin 17)
|
||||
- name: Set up Java JDK (Temurin ${{ matrix.java }})
|
||||
uses: actions/setup-java@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Ensure Java Available
|
||||
run: |
|
||||
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
|
||||
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
|
||||
apk add --no-cache openjdk17 || true
|
||||
apk add --no-cache openjdk${{ matrix.java }} || true
|
||||
fi
|
||||
fi
|
||||
echo "Java compiler:"
|
||||
@@ -53,23 +57,27 @@ jobs:
|
||||
run: |
|
||||
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
|
||||
uses: actions/upload-artifact@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: j3270-executable-jar
|
||||
path: build/j3270.jar
|
||||
name: j3270-java${{ matrix.java }}-jar
|
||||
path: build/j3270-java${{ matrix.java }}.jar
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-reports
|
||||
name: test-reports-java${{ matrix.java }}
|
||||
path: build/reports/tests/
|
||||
|
||||
- name: Report Build Status
|
||||
if: failure()
|
||||
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
|
||||
|
||||
@@ -9,8 +9,12 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build JAR & Run Tests
|
||||
name: Build JAR & Run Tests (Java ${{ matrix.java }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java: [ '11', '17', '21' ]
|
||||
|
||||
steps:
|
||||
- name: Configure Dynamic DNS Resolvers
|
||||
@@ -23,21 +27,21 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Java JDK (Temurin 17)
|
||||
- name: Set up Java JDK (Temurin ${{ matrix.java }})
|
||||
uses: actions/setup-java@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Ensure Java Available
|
||||
run: |
|
||||
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
|
||||
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
|
||||
apk add --no-cache openjdk17 || true
|
||||
apk add --no-cache openjdk${{ matrix.java }} || true
|
||||
fi
|
||||
fi
|
||||
echo "Java compiler:"
|
||||
@@ -53,23 +57,27 @@ jobs:
|
||||
run: |
|
||||
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
|
||||
uses: actions/upload-artifact@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: j3270-executable-jar
|
||||
path: build/j3270.jar
|
||||
name: j3270-java${{ matrix.java }}-jar
|
||||
path: build/j3270-java${{ matrix.java }}.jar
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-reports
|
||||
name: test-reports-java${{ matrix.java }}
|
||||
path: build/reports/tests/
|
||||
|
||||
- name: Report Build Status
|
||||
if: failure()
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
j3270.log.*
|
||||
*.bin
|
||||
*.txt
|
||||
*.py
|
||||
*.pcap
|
||||
@@ -8,3 +9,4 @@ build/*
|
||||
*.jar
|
||||
build
|
||||
.gradle
|
||||
*.txt
|
||||
|
||||
@@ -2,29 +2,43 @@
|
||||
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/actions)
|
||||
[](https://git.hugfreevikings.wtf/rudi/j3270/releases)
|
||||
[](https://adoptium.net)
|
||||
[-blue.svg)](https://adoptium.net)
|
||||
[]()
|
||||
[](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
|
||||
|
||||
### Latest Standalone JAR
|
||||
You can download the ready-to-run executable JAR directly from the releases or CI builds:
|
||||
### Latest Standalone JARs
|
||||
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)**
|
||||
- **[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`
|
||||
Requires **Java 11 or higher** (Java 11, 17, 21+):
|
||||
|
||||
```bash
|
||||
# Launch interactive GUI
|
||||
# Launch interactive Swing GUI
|
||||
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
|
||||
# Or using standard x3270 L: prefix
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
@@ -48,39 +69,108 @@ java -jar j3270.jar -c config.ini
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, plain TN3270 fallback, and SSL/TLS encryption.
|
||||
- **SSL/TLS Security**:
|
||||
- Encrypted TN3270 over TLS connections on standard port `992` or custom ports.
|
||||
- Interactive certificate verification prompt for self-signed or untrusted certificates with fingerprint, subject, issuer, and validity inspection.
|
||||
- Optional unverified/insecure mode for testing and headless scripting.
|
||||
- **IND$FILE File Transfer**: Full support for both **CUT** and **DFT (DDM)** high-speed structured field transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS.
|
||||
- **z/VM & Line-Mode Support**: Proper SSCP-LU and unformatted line handling (`CP TERM CONMODE 3270` supported).
|
||||
- **APL & Graphic Escape**: Comprehensive box-drawing and math symbol character rendering.
|
||||
- **Terminal Models**:
|
||||
### 🌐 Protocol & Telnet Engine
|
||||
- **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.
|
||||
- **SSCP-LU & Unformatted Line Mode**: Native z/VM line-mode support (`CP TERM CONMODE 3270`).
|
||||
- **Terminal Models & Dynamic Geometry**:
|
||||
- Model 2 (24x80)
|
||||
- Model 3 (32x80)
|
||||
- Model 4 (43x80)
|
||||
- Model 5 (27x132)
|
||||
- **Zero-Dependency Core**: Modular architecture separated into:
|
||||
- `lib3270j`: Reusable Java 3270 protocol, data stream parser, charset translation, and telnet state machine.
|
||||
- `j3270`: Desktop Swing user interface and session management.
|
||||
- `IBM-DYNAMIC` (Model 0 dynamic geometry auto-negotiation via Usable Area Query Reply).
|
||||
- **Enterprise Proxy Support**: SOCKS5 and HTTP CONNECT proxy tunneling with optional authentication.
|
||||
|
||||
### 🔒 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
|
||||
|
||||
### Standalone Build (No Gradle Required)
|
||||
The project includes self-contained, portable build scripts:
|
||||
### Quick Setup & Build for End Users
|
||||
|
||||
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
|
||||
# Build executable JAR in 2 seconds:
|
||||
sh ./build_all.sh
|
||||
# Setup and build standalone executable JAR:
|
||||
./setup.sh
|
||||
|
||||
# Run all 57 automated unit tests in ~500ms:
|
||||
sh ./test_all.sh
|
||||
# Build and immediately launch:
|
||||
./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)
|
||||
```bash
|
||||
@@ -88,6 +178,9 @@ The resulting standalone JAR is created at `build/j3270.jar`.
|
||||
./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
|
||||
|
||||
@@ -17,6 +17,7 @@ subprojects {
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jvmArgs '-Djava.awt.headless=true'
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed"
|
||||
}
|
||||
@@ -24,5 +25,6 @@ subprojects {
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
}
|
||||
|
||||
+23
-7
@@ -14,16 +14,32 @@ if [ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/javac" ] && [ -x "$JAVA_HOME/bin/
|
||||
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||
JAR_BIN="$JAVA_HOME/bin/jar"
|
||||
elif command -v javac >/dev/null 2>&1 && command -v jar >/dev/null 2>&1; then
|
||||
JAVAC_BIN="$(command -v javac)"
|
||||
JAR_BIN="$(command -v jar)"
|
||||
else
|
||||
# Check common SDKMAN / macOS / Linux JDK locations
|
||||
for h in "$HOME/.sdkman/candidates/java/current" \
|
||||
if javac -version >/dev/null 2>&1; then
|
||||
JAVAC_BIN="$(command -v javac)"
|
||||
JAR_BIN="$(command -v jar)"
|
||||
fi
|
||||
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-17-openjdk* \
|
||||
/usr/lib/jvm/java-11-openjdk* \
|
||||
/usr/lib/jvm/default-java \
|
||||
/Library/Java/JavaVirtualMachines/*/Contents/Home; do
|
||||
/Library/Java/JavaVirtualMachines/*/Contents/Home \
|
||||
/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
|
||||
export JAVA_HOME="$h"
|
||||
JAVAC_BIN="$JAVA_HOME/bin/javac"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Gitea Actions Runner Configuration for j3270
|
||||
|
||||
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`.
|
||||
2. Build the standalone `j3270.jar` executable via `build_all.sh`.
|
||||
3. Upload `j3270.jar` as a downloadable artifact.
|
||||
4. Report test results and diagnostics.
|
||||
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 standalone executables for each LTS version via `build_all.sh`.
|
||||
3. Upload `j3270-java11.jar`, `j3270-java17.jar`, and `j3270-java21.jar` as downloadable artifacts.
|
||||
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:
|
||||
|
||||
```bash
|
||||
# Compile and run all 25 unit tests (130ms):
|
||||
# Compile and run all 539 unit tests (~7s):
|
||||
sh ./test_all.sh
|
||||
|
||||
# Build executable standalone JAR:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,2 +1,249 @@
|
||||
#!/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
@@ -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
|
||||
@@ -45,6 +45,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
private FieldInspectorDialog fieldInspectorDialog;
|
||||
private PrinterSessionDialog printerSessionDialog;
|
||||
|
||||
// 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() {
|
||||
super("j3270 — Java TN3270 Terminal Emulator");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
@@ -59,12 +70,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
setLocationRelativeTo(null);
|
||||
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 -> {
|
||||
if (client != null) {
|
||||
statusBar.updateStatus();
|
||||
if (isActive() && !terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
syncModeMenuItems();
|
||||
if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
|
||||
if (!terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -84,14 +98,38 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
if (!isAnyChildDialogActive()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
|
||||
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
|
||||
for (Window owned : getOwnedWindows()) {
|
||||
if (owned != null && owned.isVisible()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
terminalPanel = new TerminalPanel();
|
||||
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().setBackground(Color.BLACK);
|
||||
@@ -99,6 +137,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
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) {
|
||||
buildMenuBar();
|
||||
statusBar.applyTheme(theme);
|
||||
@@ -178,6 +227,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
viewMenu.add(themeMenu);
|
||||
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));
|
||||
@@ -259,7 +314,77 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
|
||||
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");
|
||||
actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
@@ -587,6 +712,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
return terminalPanel;
|
||||
}
|
||||
|
||||
public Telnet3270Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
void connect(ConnectionConfig config) {
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
@@ -639,12 +768,26 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
client.disconnect();
|
||||
Telnet3270Client c = client;
|
||||
client = null;
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null, terminalPanel);
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
new Thread(() -> {
|
||||
try {
|
||||
c.disconnect();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,6 +798,27 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
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 ==========
|
||||
|
||||
@Override
|
||||
@@ -664,7 +828,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
statusBar.updateStatus();
|
||||
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.requestFocusInWindow();
|
||||
}
|
||||
@@ -703,6 +876,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
syncModeMenuItems();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -720,6 +894,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 ==========
|
||||
|
||||
private void showKeyMappings() {
|
||||
@@ -735,7 +922,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
"End — Erase to end of field\n" +
|
||||
"Delete — Delete character\n" +
|
||||
"Backspace — Backspace\n" +
|
||||
"Insert — Toggle insert mode\n" +
|
||||
"Insert / Help / Alt+I — Toggle insert mode\n" +
|
||||
"Escape / Alt+R — Reset\n" +
|
||||
"PageUp/Down — PF7/PF8\n" +
|
||||
"Alt+C / Alt+K — Clear\n" +
|
||||
@@ -746,7 +933,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
"Alt+L — Toggle Light Pen\n" +
|
||||
"Alt+T — File Transfer\n" +
|
||||
"Alt+Shift+R — Crosshair Ruler\n" +
|
||||
"Alt+B — Toggle Status Bar\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+G / F3— Find Next\n" +
|
||||
"Cmd/Ctrl+D — Disconnect\n" +
|
||||
@@ -811,13 +1002,78 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
"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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Main ==========
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean debug = false;
|
||||
boolean cliTls = false;
|
||||
boolean cliNoVerifyCert = false;
|
||||
Boolean cliTn3270e = null;
|
||||
Boolean cliAutoSysUnlock = null;
|
||||
GraphicsMode cliGraphicsMode = null;
|
||||
String configFile = null;
|
||||
|
||||
@@ -834,6 +1090,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
cliTn3270e = false;
|
||||
} else if ("--tn3270e".equals(arg)) {
|
||||
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=")) {
|
||||
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
|
||||
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||
@@ -849,42 +1109,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) {
|
||||
try {
|
||||
@@ -910,6 +1136,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
final boolean finalTls = cliTls;
|
||||
final boolean finalNoVerify = cliNoVerifyCert;
|
||||
final Boolean finalTn3270e = cliTn3270e;
|
||||
final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
|
||||
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
@@ -946,6 +1173,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
if (finalTn3270e != null) {
|
||||
config.setTn3270eEnabled(finalTn3270e);
|
||||
}
|
||||
if (finalAutoSysUnlock != null) {
|
||||
config.setAutoSysUnlock(finalAutoSysUnlock);
|
||||
} else {
|
||||
config.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||
}
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
@@ -969,6 +1201,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
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 {
|
||||
|
||||
@@ -33,6 +33,7 @@ public class Settings {
|
||||
|
||||
public static void setFontFamily(String family) {
|
||||
prefs.put("fontFamily", family);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static int getFontSize() {
|
||||
@@ -41,6 +42,14 @@ public class Settings {
|
||||
|
||||
public static void setFontSize(int size) {
|
||||
prefs.putInt("fontSize", size);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
private static void flushPrefs() {
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static StartupBehavior getStartupBehavior() {
|
||||
@@ -96,6 +105,85 @@ public class Settings {
|
||||
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() {
|
||||
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
|
||||
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||
@@ -155,8 +243,61 @@ public class Settings {
|
||||
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) {
|
||||
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) {
|
||||
@@ -249,6 +390,26 @@ public class Settings {
|
||||
prefs.putBoolean("blockSelectMode", block);
|
||||
}
|
||||
|
||||
// ========== 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();
|
||||
}
|
||||
|
||||
// ========== Crosshair Ruler ==========
|
||||
|
||||
public static boolean getCrosshairRuler() {
|
||||
@@ -259,6 +420,16 @@ public class Settings {
|
||||
prefs.putBoolean("crosshairRuler", enabled);
|
||||
}
|
||||
|
||||
// ========== 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() {
|
||||
@@ -269,6 +440,101 @@ public class Settings {
|
||||
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) {
|
||||
switch (section) {
|
||||
case "appearance":
|
||||
@@ -284,6 +550,12 @@ public class Settings {
|
||||
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;
|
||||
@@ -336,11 +608,62 @@ public class Settings {
|
||||
setDynamicCols(Integer.parseInt(value));
|
||||
break;
|
||||
case "blockSelectMode": setBlockSelectMode(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 "enablePasteFromExcel":
|
||||
case "pasteFromExcel":
|
||||
case "excelPaste":
|
||||
setEnablePasteFromExcel(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteStopAtProtectedLine":
|
||||
case "stopAtProtected":
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown behavior/connection key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "clipboard":
|
||||
switch (key) {
|
||||
case "enablePasteFromExcel":
|
||||
case "pasteFromExcel":
|
||||
case "excelPaste":
|
||||
setEnablePasteFromExcel(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteStopAtProtectedLine":
|
||||
case "stopAtProtected":
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown clipboard key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "colors":
|
||||
if (key.startsWith("color_")) {
|
||||
int index = Integer.parseInt(key.substring(6));
|
||||
@@ -365,6 +688,55 @@ public class Settings {
|
||||
}
|
||||
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:
|
||||
// Allow bare keys outside any section — treat as raw prefs
|
||||
log.fine("Setting raw preference: " + key + " = " + value);
|
||||
@@ -392,6 +764,7 @@ public class Settings {
|
||||
w.println("fontFamily = " + getFontFamily());
|
||||
w.println("fontSize = " + getFontSize());
|
||||
w.println("crosshairRuler = " + getCrosshairRuler());
|
||||
w.println("statusBarVisible = " + getStatusBarVisible());
|
||||
w.println("cursorStyle = " + getCursorStyle());
|
||||
w.println();
|
||||
|
||||
@@ -410,6 +783,32 @@ public class Settings {
|
||||
w.println("dynamicRows = " + getDynamicRows());
|
||||
w.println("dynamicCols = " + getDynamicCols());
|
||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||
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();
|
||||
|
||||
// [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();
|
||||
|
||||
// [graphics]
|
||||
@@ -438,9 +837,10 @@ public class Settings {
|
||||
|
||||
// [keybindings]
|
||||
w.println("[keybindings]");
|
||||
// Navigation keys
|
||||
// Navigation & clipboard keys
|
||||
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) {
|
||||
String val = prefs.get("key_" + act, null);
|
||||
if (val != null) {
|
||||
|
||||
@@ -12,6 +12,50 @@ import java.io.File;
|
||||
|
||||
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 Frame owner;
|
||||
|
||||
@@ -66,6 +110,16 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
// Host Type
|
||||
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));
|
||||
|
||||
// Direction
|
||||
@@ -88,6 +142,15 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
// Local File
|
||||
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...");
|
||||
ThemeManager.styleButton(browseLocalButton, ThemeManager.ButtonVariant.DEFAULT);
|
||||
browseLocalButton.addActionListener(e -> browseLocalFile());
|
||||
@@ -211,13 +274,47 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
setContentPane(mainPanel);
|
||||
|
||||
prefillFromLastTransfer();
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
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() {
|
||||
boolean isSend = sendRadio.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);
|
||||
overwriteCheck.setEnabled(!isSend);
|
||||
@@ -225,10 +322,11 @@ public class FileTransferDialog extends JDialog {
|
||||
crCheck.setEnabled(isAscii);
|
||||
remapCheck.setEnabled(isAscii);
|
||||
|
||||
recfmField.setEnabled(isSend);
|
||||
lreclField.setEnabled(isSend);
|
||||
blksizeField.setEnabled(isSend);
|
||||
spaceField.setEnabled(isSend);
|
||||
recfmField.setEnabled(isSend && isTso);
|
||||
lreclField.setEnabled(isSend && isTso);
|
||||
blksizeField.setEnabled(isSend && isTso);
|
||||
spaceField.setEnabled(isSend && isTso);
|
||||
optionsField.setEnabled(isCms);
|
||||
}
|
||||
|
||||
private JPanel createRow(String labelText, Component comp) {
|
||||
@@ -245,9 +343,13 @@ public class FileTransferDialog extends JDialog {
|
||||
private void browseLocalFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
|
||||
if (hostFileField.getText().trim().isEmpty()) {
|
||||
hostFileField.setText(chooser.getSelectedFile().getName());
|
||||
File selected = chooser.getSelectedFile();
|
||||
localFileField.setText(selected.getAbsolutePath());
|
||||
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 +362,21 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
||||
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("\"", "");
|
||||
int lastDot = cleanName.lastIndexOf('.');
|
||||
if (lastDot > 0) cleanName = cleanName.substring(lastDot + 1);
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -298,6 +411,8 @@ public class FileTransferDialog extends JDialog {
|
||||
config.setSpace(spaceField.getText().trim());
|
||||
config.setOptions(optionsField.getText().trim());
|
||||
|
||||
recordSessionTransfer();
|
||||
|
||||
String error = coordinator.startTransfer(config);
|
||||
if (error != null) {
|
||||
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
|
||||
@@ -306,6 +421,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) {
|
||||
Color fg = new Color(200, 200, 200);
|
||||
Color bg = new Color(40, 40, 40);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ public class ConnectDialog extends JDialog {
|
||||
private JCheckBox tlsCheckBox;
|
||||
private JCheckBox verifyCertCheckBox;
|
||||
private JCheckBox tn3270eCheckBox;
|
||||
private JCheckBox keepAliveCheckBox;
|
||||
private JCheckBox autoReconnectCheckBox;
|
||||
private boolean confirmed;
|
||||
private ConnectionConfig result;
|
||||
|
||||
@@ -246,6 +248,24 @@ public class ConnectDialog extends JDialog {
|
||||
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
||||
mainPanel.add(tn3270eCheckBox, gbc);
|
||||
|
||||
// Keep-Alive Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 10;
|
||||
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
|
||||
ThemeManager.styleCheckBox(keepAliveCheckBox);
|
||||
keepAliveCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
|
||||
mainPanel.add(keepAliveCheckBox, gbc);
|
||||
|
||||
// Auto-Reconnect Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 11;
|
||||
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
|
||||
ThemeManager.styleCheckBox(autoReconnectCheckBox);
|
||||
autoReconnectCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
|
||||
mainPanel.add(autoReconnectCheckBox, gbc);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||
|
||||
@@ -266,7 +286,7 @@ public class ConnectDialog extends JDialog {
|
||||
buttonPanel.add(connectBtn);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 10;
|
||||
gbc.gridy = 12;
|
||||
gbc.gridwidth = 2;
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
|
||||
@@ -328,6 +348,18 @@ public class ConnectDialog extends JDialog {
|
||||
result.setUseTls(tlsCheckBox.isSelected());
|
||||
result.setTlsVerifyCert(verifyCertCheckBox.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);
|
||||
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
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 INI-style KEY<n>=action formats 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);
|
||||
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
|
||||
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = line.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;
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import haus.nightmare.j3270.config.Settings;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
@@ -30,9 +31,27 @@ public class SettingsDialog extends JDialog {
|
||||
private JPanel autoConnectPanel;
|
||||
private JTextField hostField;
|
||||
private JTextField portField;
|
||||
private JCheckBox autoReconnectCheck;
|
||||
private JCheckBox inputMaskCheck;
|
||||
private JTextField inputMaskCharField;
|
||||
private JCheckBox blockSelectCheck;
|
||||
private JSpinner dynamicRowsSpinner;
|
||||
private JSpinner dynamicColsSpinner;
|
||||
private JCheckBox enablePasteFromExcelCheck;
|
||||
private JCheckBox pasteStopAtProtectedCheck;
|
||||
|
||||
// Entry Assist & Modes tab
|
||||
private JCheckBox docModeCheck;
|
||||
private JCheckBox wordWrapCheck;
|
||||
private JSpinner startColSpinner;
|
||||
private JSpinner endColSpinner;
|
||||
private JCheckBox bellCheck;
|
||||
private JSpinner bellColSpinner;
|
||||
private JTextField tabStopsField;
|
||||
private JCheckBox insertOffOnAidCheck;
|
||||
private JCheckBox fourColorOverrideCheck;
|
||||
private JCheckBox numericFieldLockCheck;
|
||||
private JCheckBox autoSkipCheck;
|
||||
|
||||
// Advanced tab state tracking
|
||||
private final Color[] tempHostColors = new Color[16];
|
||||
@@ -45,7 +64,7 @@ public class SettingsDialog extends JDialog {
|
||||
this.parentApp = parent;
|
||||
|
||||
initComponents();
|
||||
setSize(560, 480);
|
||||
setSize(600, 520);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
@@ -55,6 +74,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
tabbedPane.addTab("Appearance", createAppearancePanel());
|
||||
tabbedPane.addTab("Behavior", createBehaviorPanel());
|
||||
tabbedPane.addTab("Entry Assist & Modes", createEntryAssistPanel());
|
||||
tabbedPane.addTab("Advanced", createAdvancedPanel());
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
|
||||
@@ -133,11 +153,31 @@ public class SettingsDialog extends JDialog {
|
||||
panel.add(fontLabel, gbc);
|
||||
|
||||
String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
|
||||
fontBox = new JComboBox<>(fonts);
|
||||
// Find default
|
||||
java.util.List<String> fontList = new java.util.ArrayList<>();
|
||||
boolean hasMonospaced = false;
|
||||
String currentFont = Settings.getFontFamily();
|
||||
for (int i = 0; i < fonts.length; i++) {
|
||||
if (fonts[i].equalsIgnoreCase(currentFont)) {
|
||||
for (String f : fonts) {
|
||||
if ("Monospaced".equalsIgnoreCase(f)) {
|
||||
hasMonospaced = true;
|
||||
}
|
||||
fontList.add(f);
|
||||
}
|
||||
if (!hasMonospaced) {
|
||||
fontList.add(0, "Monospaced");
|
||||
}
|
||||
boolean hasCurrent = false;
|
||||
for (String f : fontList) {
|
||||
if (f.equalsIgnoreCase(currentFont)) {
|
||||
hasCurrent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasCurrent && currentFont != null && !currentFont.trim().isEmpty()) {
|
||||
fontList.add(0, currentFont);
|
||||
}
|
||||
fontBox = new JComboBox<>(fontList.toArray(new String[0]));
|
||||
for (int i = 0; i < fontBox.getItemCount(); i++) {
|
||||
if (fontBox.getItemAt(i).equalsIgnoreCase(currentFont)) {
|
||||
fontBox.setSelectedIndex(i);
|
||||
break;
|
||||
}
|
||||
@@ -231,14 +271,45 @@ public class SettingsDialog extends JDialog {
|
||||
});
|
||||
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
|
||||
|
||||
// Block select mode checkbox
|
||||
// Auto-Reconnect checkbox
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.gridwidth = 2;
|
||||
autoReconnectCheck = new JCheckBox("Auto-Reconnect on Disconnect", Settings.getAutoConnectAutoReconnect());
|
||||
ThemeManager.styleCheckBox(autoReconnectCheck);
|
||||
panel.add(autoReconnectCheck, gbc);
|
||||
|
||||
// Input Mask feature control
|
||||
gbc.gridy = 3;
|
||||
gbc.gridwidth = 2;
|
||||
JPanel inputMaskPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
|
||||
inputMaskPanel.setOpaque(false);
|
||||
inputMaskCheck = new JCheckBox("Enable Input Mask (Password Masking)", Settings.getInputMask());
|
||||
ThemeManager.styleCheckBox(inputMaskCheck);
|
||||
inputMaskPanel.add(inputMaskCheck);
|
||||
|
||||
JLabel maskCharLabel = new JLabel("Mask Character:");
|
||||
inputMaskCharField = new JTextField(Settings.getInputMaskChar(), 2);
|
||||
ThemeManager.styleTextField(inputMaskCharField);
|
||||
inputMaskCharField.setEnabled(inputMaskCheck.isSelected());
|
||||
maskCharLabel.setEnabled(inputMaskCheck.isSelected());
|
||||
inputMaskCheck.addActionListener(e -> {
|
||||
boolean sel = inputMaskCheck.isSelected();
|
||||
inputMaskCharField.setEnabled(sel);
|
||||
maskCharLabel.setEnabled(sel);
|
||||
});
|
||||
inputMaskPanel.add(maskCharLabel);
|
||||
inputMaskPanel.add(inputMaskCharField);
|
||||
panel.add(inputMaskPanel, gbc);
|
||||
|
||||
// Block select mode checkbox
|
||||
gbc.gridy = 4;
|
||||
gbc.gridwidth = 2;
|
||||
blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode());
|
||||
panel.add(blockSelectCheck, gbc);
|
||||
|
||||
// Default Dynamic Screen Size
|
||||
gbc.gridy = 3;
|
||||
gbc.gridy = 5;
|
||||
gbc.gridwidth = 1;
|
||||
gbc.gridx = 0;
|
||||
panel.add(new JLabel("Default Dynamic Screen:"), gbc);
|
||||
@@ -257,16 +328,123 @@ public class SettingsDialog extends JDialog {
|
||||
gbc.gridx = 1;
|
||||
panel.add(dynDimPanel, gbc);
|
||||
|
||||
// Placeholder for potentially more behavior options below
|
||||
// Clipboard & Tabular Paste options
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridy = 6;
|
||||
gbc.gridwidth = 2;
|
||||
enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel());
|
||||
panel.add(enablePasteFromExcelCheck, gbc);
|
||||
|
||||
gbc.gridy = 7;
|
||||
pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine());
|
||||
panel.add(pasteStopAtProtectedCheck, gbc);
|
||||
|
||||
gbc.gridy = 8;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(Box.createGlue(), gbc);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createEntryAssistPanel() {
|
||||
JPanel panel = new JPanel();
|
||||
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
|
||||
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// Group 1: Entry Assist / Document Mode
|
||||
JPanel eaGroup = new JPanel(new GridBagLayout());
|
||||
eaGroup.setBorder(BorderFactory.createTitledBorder("Entry Assist (Document Mode)"));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(4, 6, 4, 6);
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
|
||||
docModeCheck = new JCheckBox("Enable Document Mode (DOC)", Settings.getEntryAssistDocMode());
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
gbc.gridwidth = 2;
|
||||
eaGroup.add(docModeCheck, gbc);
|
||||
|
||||
wordWrapCheck = new JCheckBox("Enable Word Wrap (V)", Settings.getEntryAssistWordWrap());
|
||||
gbc.gridy = 1;
|
||||
eaGroup.add(wordWrapCheck, gbc);
|
||||
|
||||
// Margins
|
||||
gbc.gridy = 2;
|
||||
gbc.gridwidth = 1;
|
||||
eaGroup.add(new JLabel("Margins:"), gbc);
|
||||
|
||||
JPanel marginPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
|
||||
marginPanel.setOpaque(false);
|
||||
marginPanel.add(new JLabel("Left:"));
|
||||
startColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistStartCol(), 1, 80, 1));
|
||||
ThemeManager.styleSpinner(startColSpinner);
|
||||
marginPanel.add(startColSpinner);
|
||||
|
||||
marginPanel.add(new JLabel("Right:"));
|
||||
endColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistEndCol(), 1, 80, 1));
|
||||
ThemeManager.styleSpinner(endColSpinner);
|
||||
marginPanel.add(endColSpinner);
|
||||
|
||||
gbc.gridx = 1;
|
||||
eaGroup.add(marginPanel, gbc);
|
||||
|
||||
// End-of-Line Bell
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 3;
|
||||
bellCheck = new JCheckBox("Audible EOL Bell at Col:", Settings.getEntryAssistBell());
|
||||
eaGroup.add(bellCheck, gbc);
|
||||
|
||||
bellColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistBellCol(), 1, 80, 1));
|
||||
ThemeManager.styleSpinner(bellColSpinner);
|
||||
gbc.gridx = 1;
|
||||
eaGroup.add(bellColSpinner, gbc);
|
||||
|
||||
// Tab Stops
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
eaGroup.add(new JLabel("Tab Stops:"), gbc);
|
||||
|
||||
tabStopsField = new JTextField(Settings.getEntryAssistTabStops(), 20);
|
||||
tabStopsField.setToolTipText("Comma-separated column numbers (1-80, e.g. 1,9,17,25,33,41,49,57,65,73)");
|
||||
gbc.gridx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
eaGroup.add(tabStopsField, gbc);
|
||||
|
||||
panel.add(eaGroup);
|
||||
panel.add(Box.createVerticalStrut(10));
|
||||
|
||||
// Group 2: Terminal Operational Modes
|
||||
JPanel modesGroup = new JPanel(new GridBagLayout());
|
||||
modesGroup.setBorder(BorderFactory.createTitledBorder("Terminal Operational Modes"));
|
||||
GridBagConstraints gbcM = new GridBagConstraints();
|
||||
gbcM.insets = new Insets(4, 6, 4, 6);
|
||||
gbcM.anchor = GridBagConstraints.WEST;
|
||||
gbcM.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbcM.gridx = 0;
|
||||
gbcM.weightx = 1.0;
|
||||
|
||||
insertOffOnAidCheck = new JCheckBox("Reset Insert mode on AID key (Enter, PF, PA, Clear)", Settings.getInsertOffOnAid());
|
||||
gbcM.gridy = 0;
|
||||
modesGroup.add(insertOffOnAidCheck, gbcM);
|
||||
|
||||
fourColorOverrideCheck = new JCheckBox("Base 4-Color Override mode (3279 green/white/red/turquoise)", Settings.getFourColorOverride());
|
||||
gbcM.gridy = 1;
|
||||
modesGroup.add(fourColorOverrideCheck, gbcM);
|
||||
|
||||
numericFieldLockCheck = new JCheckBox("Lock keyboard on non-numeric input in numeric fields (-NUMERIC)", Settings.getNumericFieldLock());
|
||||
gbcM.gridy = 2;
|
||||
modesGroup.add(numericFieldLockCheck, gbcM);
|
||||
|
||||
autoSkipCheck = new JCheckBox("Auto-Skip to next unprotected field when field is filled", Settings.getAutoSkipEnabled());
|
||||
gbcM.gridy = 3;
|
||||
modesGroup.add(autoSkipCheck, gbcM);
|
||||
|
||||
panel.add(modesGroup);
|
||||
panel.add(Box.createVerticalGlue());
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createAdvancedPanel() {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
|
||||
@@ -369,7 +547,9 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
|
||||
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR",
|
||||
"ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL"};
|
||||
"STATUS_BAR",
|
||||
"ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL",
|
||||
"COPY", "PASTE", "SELECTALL"};
|
||||
|
||||
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
|
||||
@Override
|
||||
@@ -380,15 +560,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Populate table from Settings or Defaults
|
||||
for(String act : actions) {
|
||||
String def = act;
|
||||
if (def.equals("ERASE_INPUT")) def = "alt E";
|
||||
else if (def.equals("NEWLINE")) def = "shift ENTER";
|
||||
else if (def.equals("DUP")) def = "alt D";
|
||||
else if (def.equals("FIELD_MARK")) def = "alt M";
|
||||
else if (def.equals("ATTN")) def = "alt A";
|
||||
else if (def.equals("SYSREQ")) def = "alt S";
|
||||
else if (def.equals("CURSEL")) def = "alt Q";
|
||||
else if (def.equals("CLEAR")) def = "alt C";
|
||||
String def = getDefaultBinding(act);
|
||||
String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def);
|
||||
tempKeyBindings.put(act, current);
|
||||
keymapModel.addRow(new Object[]{act, current});
|
||||
@@ -526,6 +698,45 @@ public class SettingsDialog extends JDialog {
|
||||
});
|
||||
btnPanel.add(btnReset);
|
||||
|
||||
// Import Keymap — import from .kmp file
|
||||
JButton btnImportKmp = new JButton("Import Keymap...");
|
||||
ThemeManager.styleButton(btnImportKmp, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnImportKmp.addActionListener(e -> {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Import Keymap Profile");
|
||||
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("Keyboard Map Profile (*.kmp)", "kmp"));
|
||||
int ret = chooser.showOpenDialog(this);
|
||||
if (ret == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
try {
|
||||
Map<String, String> imported = KeyBindings.parseKmp(f);
|
||||
int updatedCount = 0;
|
||||
for (Map.Entry<String, String> entry : imported.entrySet()) {
|
||||
String action = entry.getKey();
|
||||
String binding = entry.getValue();
|
||||
tempKeyBindings.put(action, binding);
|
||||
for (int row = 0; row < keymapModel.getRowCount(); row++) {
|
||||
if (action.equalsIgnoreCase((String) keymapModel.getValueAt(row, 0))) {
|
||||
keymapModel.setValueAt(binding, row, 1);
|
||||
updatedCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Successfully imported " + updatedCount + " key bindings from " + f.getName(),
|
||||
"Keymap Imported",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Failed to import keymap: " + ex.getMessage(),
|
||||
"Import Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
btnPanel.add(btnImportKmp);
|
||||
|
||||
main.add(new JScrollPane(table), BorderLayout.CENTER);
|
||||
main.add(btnPanel, BorderLayout.SOUTH);
|
||||
|
||||
@@ -534,13 +745,12 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
/** Returns the factory-default binding for a given action name. */
|
||||
private String getDefaultBinding(String action) {
|
||||
if (action.startsWith("PF")) {
|
||||
if (action.matches("PF[0-9]+")) {
|
||||
int n = Integer.parseInt(action.substring(2));
|
||||
return n <= 12 ? "F" + n : "shift F" + (n - 12);
|
||||
}
|
||||
if (action.startsWith("PA")) return "alt " + action.substring(2);
|
||||
if (action.equals("CLEAR")) return "alt C";
|
||||
return action; // nav keys default to their own name
|
||||
if (action.matches("PA[1-3]")) return "alt " + action.substring(2);
|
||||
return haus.nightmare.j3270.config.Settings.getDefaultBinding(action);
|
||||
}
|
||||
|
||||
// ========== Apply Settings ==========
|
||||
@@ -576,15 +786,57 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Reconnect
|
||||
if (autoReconnectCheck != null) {
|
||||
boolean ar = autoReconnectCheck.isSelected();
|
||||
Settings.setAutoConnectAutoReconnect(ar);
|
||||
if (parentApp != null && parentApp.getClient() != null && parentApp.getClient().getConfig() != null) {
|
||||
parentApp.getClient().getConfig().setAutoReconnect(ar);
|
||||
}
|
||||
}
|
||||
|
||||
// Input Mask
|
||||
if (inputMaskCheck != null) {
|
||||
Settings.setInputMask(inputMaskCheck.isSelected());
|
||||
}
|
||||
if (inputMaskCharField != null) {
|
||||
String charText = inputMaskCharField.getText().trim();
|
||||
Settings.setInputMaskChar(charText.isEmpty() ? "*" : charText.substring(0, 1));
|
||||
}
|
||||
|
||||
// Block select mode
|
||||
Settings.setBlockSelectMode(blockSelectCheck.isSelected());
|
||||
|
||||
// Clipboard & Tabular Paste options
|
||||
if (enablePasteFromExcelCheck != null) {
|
||||
Settings.setEnablePasteFromExcel(enablePasteFromExcelCheck.isSelected());
|
||||
}
|
||||
if (pasteStopAtProtectedCheck != null) {
|
||||
Settings.setPasteStopAtProtectedLine(pasteStopAtProtectedCheck.isSelected());
|
||||
}
|
||||
|
||||
// Default Dynamic screen dimensions
|
||||
if (dynamicRowsSpinner != null && dynamicColsSpinner != null) {
|
||||
Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue());
|
||||
Settings.setDynamicCols((Integer) dynamicColsSpinner.getValue());
|
||||
}
|
||||
|
||||
// Entry Assist & Modes
|
||||
if (docModeCheck != null) {
|
||||
Settings.setEntryAssistDocMode(docModeCheck.isSelected());
|
||||
Settings.setEntryAssistWordWrap(wordWrapCheck.isSelected());
|
||||
Settings.setEntryAssistStartCol((Integer) startColSpinner.getValue());
|
||||
Settings.setEntryAssistEndCol((Integer) endColSpinner.getValue());
|
||||
Settings.setEntryAssistBell(bellCheck.isSelected());
|
||||
Settings.setEntryAssistBellCol((Integer) bellColSpinner.getValue());
|
||||
Settings.setEntryAssistTabStops(tabStopsField.getText().trim());
|
||||
|
||||
Settings.setInsertOffOnAid(insertOffOnAidCheck.isSelected());
|
||||
Settings.setFourColorOverride(fourColorOverrideCheck.isSelected());
|
||||
Settings.setNumericFieldLock(numericFieldLockCheck.isSelected());
|
||||
Settings.setAutoSkipEnabled(autoSkipCheck.isSelected());
|
||||
}
|
||||
|
||||
// Propagate visual changes to the app
|
||||
// Save Colors
|
||||
for (int i=0; i<16; i++) {
|
||||
@@ -600,6 +852,9 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
|
||||
parentApp.getTerminalPanel().reloadSettings();
|
||||
if (parentApp != null && (parentApp.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) {
|
||||
parentApp.getTerminalPanel().guardedPack();
|
||||
}
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -18,7 +18,11 @@ public class StatusBar extends JPanel {
|
||||
private final JLabel tlsStatus;
|
||||
private final JLabel luName;
|
||||
private final JLabel lockStatus;
|
||||
private final JLabel insertStatus;
|
||||
private final JLabel aplStatus;
|
||||
private final JLabel fieldTypeStatus;
|
||||
private final JLabel docModeStatus;
|
||||
private final JLabel wordWrapStatus;
|
||||
private final JLabel codePageInfo;
|
||||
private final JLabel modelInfo;
|
||||
private final JLabel cursorPosition;
|
||||
@@ -37,8 +41,40 @@ public class StatusBar extends JPanel {
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
|
||||
tlsStatus = 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());
|
||||
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());
|
||||
modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
|
||||
cursorPosition = createLabel("001/001 [0000]", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
@@ -51,9 +87,17 @@ public class StatusBar extends JPanel {
|
||||
add(luName);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(lockStatus);
|
||||
add(Box.createHorizontalStrut(8));
|
||||
add(insertStatus);
|
||||
add(Box.createHorizontalStrut(8));
|
||||
add(aplStatus);
|
||||
add(Box.createHorizontalStrut(10));
|
||||
add(fieldTypeStatus);
|
||||
add(Box.createHorizontalGlue());
|
||||
add(docModeStatus);
|
||||
add(Box.createHorizontalStrut(8));
|
||||
add(wordWrapStatus);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(codePageInfo);
|
||||
add(Box.createHorizontalStrut(12));
|
||||
add(modelInfo);
|
||||
@@ -69,6 +113,23 @@ public class StatusBar extends JPanel {
|
||||
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) {
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
@@ -76,12 +137,20 @@ public class StatusBar extends JPanel {
|
||||
}
|
||||
|
||||
public void applyTheme(UITheme theme) {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(() -> applyTheme(theme));
|
||||
return;
|
||||
}
|
||||
setBackground(ThemeManager.getStatusBarBg(theme));
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(this::updateStatus);
|
||||
return;
|
||||
}
|
||||
UITheme theme = ThemeManager.getTheme();
|
||||
if (client == null) {
|
||||
connectionStatus.setText("Not Connected");
|
||||
@@ -89,10 +158,16 @@ public class StatusBar extends JPanel {
|
||||
tlsStatus.setText("");
|
||||
luName.setText("");
|
||||
lockStatus.setText("");
|
||||
lockStatus.setForeground(ThemeManager.getOiaInputInhibited(theme));
|
||||
insertStatus.setText("");
|
||||
aplStatus.setText("");
|
||||
fieldTypeStatus.setText("");
|
||||
docModeStatus.setText("");
|
||||
wordWrapStatus.setText("");
|
||||
codePageInfo.setText("");
|
||||
modelInfo.setText("");
|
||||
cursorPosition.setText("001/001");
|
||||
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,6 +178,10 @@ public class StatusBar extends JPanel {
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
break;
|
||||
case RECONNECTING:
|
||||
connectionStatus.setText("Reconnecting...");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
|
||||
break;
|
||||
case TCP_PENDING:
|
||||
case TELNET_PENDING:
|
||||
connectionStatus.setText("Connecting...");
|
||||
@@ -110,25 +189,25 @@ public class StatusBar extends JPanel {
|
||||
break;
|
||||
case CONNECTED_3270:
|
||||
connectionStatus.setText("TN3270");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
break;
|
||||
case CONNECTED_TN3270E:
|
||||
connectionStatus.setText("TN3270E");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
break;
|
||||
case CONNECTED_SSCP:
|
||||
connectionStatus.setText("SSCP-LU");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
break;
|
||||
case CONNECTED_NVT:
|
||||
case CONNECTED_NVT_CHAR:
|
||||
case CONNECTED_E_NVT:
|
||||
connectionStatus.setText("NVT");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
break;
|
||||
case CONNECTED_UNBOUND:
|
||||
connectionStatus.setText("Unbound");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaAttention());
|
||||
connectionStatus.setForeground(ThemeManager.getOiaAttention(theme));
|
||||
break;
|
||||
default:
|
||||
connectionStatus.setText(state.name());
|
||||
@@ -144,11 +223,11 @@ public class StatusBar extends JPanel {
|
||||
String protocol = session != null ? session.getProtocol() : "TLS";
|
||||
if (verified) {
|
||||
tlsStatus.setText("🔒 TLS");
|
||||
tlsStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
|
||||
tlsStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
|
||||
} else {
|
||||
tlsStatus.setText("🔓 TLS (Unverified)");
|
||||
tlsStatus.setForeground(ThemeManager.getOiaAttention());
|
||||
tlsStatus.setForeground(ThemeManager.getOiaAttention(theme));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
|
||||
}
|
||||
} else {
|
||||
@@ -164,50 +243,85 @@ public class StatusBar extends JPanel {
|
||||
lu = "LU:" + client.getConfig().getLuName();
|
||||
}
|
||||
luName.setText(lu);
|
||||
luName.setForeground(ThemeManager.getOiaStatusSysAvail());
|
||||
luName.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
|
||||
// Lock / Inhibit status
|
||||
int inhibit = client.getOIA().getInputInhibited();
|
||||
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
|
||||
Color lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
|
||||
Color lockFg = ThemeManager.getOiaInputInhibited(theme);
|
||||
switch (inhibit) {
|
||||
case ECLConstants.INHIBIT_SYSTEM_LOCK:
|
||||
lockStatus.setText("X SYSTEM");
|
||||
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
|
||||
lockFg = ThemeManager.getOiaInputInhibited(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_COMM_CHECK:
|
||||
lockStatus.setText("X COMM");
|
||||
lockFg = ThemeManager.getOiaCommCheck(); // Red (oEI)
|
||||
lockFg = ThemeManager.getOiaCommCheck(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_NUMERIC_ONLY:
|
||||
lockStatus.setText("X NUM");
|
||||
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
|
||||
lockFg = ThemeManager.getOiaAttention(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_PROTECTED_FIELD:
|
||||
lockStatus.setText("X PROT");
|
||||
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
|
||||
lockFg = ThemeManager.getOiaInputInhibited(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_OVERFLOW:
|
||||
lockStatus.setText("X >");
|
||||
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
|
||||
lockFg = ThemeManager.getOiaAttention(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_OPERATOR_DUE:
|
||||
lockStatus.setText("X OP");
|
||||
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI)
|
||||
lockFg = ThemeManager.getOiaAttention(theme);
|
||||
break;
|
||||
default:
|
||||
lockStatus.setText("X LOCKED");
|
||||
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII)
|
||||
lockFg = ThemeManager.getOiaInputInhibited(theme);
|
||||
break;
|
||||
}
|
||||
lockStatus.setForeground(lockFg);
|
||||
} else if (client.getInputProcessor().isInsertMode()) {
|
||||
lockStatus.setText("INSERT");
|
||||
lockStatus.setForeground(ThemeManager.getOiaStatusSysAvail());
|
||||
} else {
|
||||
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
|
||||
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
|
||||
if (client.getOIA().isNumeric()) {
|
||||
|
||||
@@ -65,6 +65,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
|
||||
private boolean crosshairRulerEnabled = false;
|
||||
private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC)
|
||||
private boolean inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
|
||||
private String inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
|
||||
private boolean textBlinkVisible = true;
|
||||
private Image wallpaperImage = null;
|
||||
private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null;
|
||||
@@ -84,6 +86,33 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
private long lastGraphicsUpdateCount = -1;
|
||||
private java.awt.image.BufferedImage cachedGraphicsImage = null;
|
||||
private boolean resizeGuard = false;
|
||||
private StatusBar statusBar = null;
|
||||
|
||||
// ========== Mode and Listener State ==========
|
||||
private boolean insertMode = false;
|
||||
|
||||
public interface ModeChangeListener {
|
||||
void onModeChanged();
|
||||
}
|
||||
private final java.util.List<ModeChangeListener> modeChangeListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
|
||||
public void addModeChangeListener(ModeChangeListener listener) {
|
||||
if (listener != null && !modeChangeListeners.contains(listener)) {
|
||||
modeChangeListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeModeChangeListener(ModeChangeListener listener) {
|
||||
modeChangeListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void fireModeChanged() {
|
||||
for (ModeChangeListener l : modeChangeListeners) {
|
||||
try {
|
||||
l.onModeChanged();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the horizontal render offset to center the grid within the panel.
|
||||
@@ -331,12 +360,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
addMouseListener(mouseHandler);
|
||||
addMouseMotionListener(mouseHandler);
|
||||
|
||||
// Handle resize: auto-fit font to window size
|
||||
// Handle resize: keep user configured font persistent without auto-fit reset
|
||||
addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
if (resizeGuard) return;
|
||||
autoFitFont();
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -461,13 +490,27 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
try {
|
||||
String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
.getData(DataFlavor.stringFlavor);
|
||||
if (text != null) {
|
||||
for (char ch : text.toCharArray()) {
|
||||
if (ch == '\n' || ch == '\r') {
|
||||
continue;
|
||||
}
|
||||
if (ch >= 0x20 && ch != 0x7F) {
|
||||
client.typeCharacter(ch);
|
||||
if (text != null && !text.isEmpty()) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int curPos = sb != null ? sb.getDisplayCursorAddress() : 0;
|
||||
int cols = sb != null ? sb.getDisplayCols() : 80;
|
||||
int curRow = cols > 0 ? curPos / cols : 0;
|
||||
int curCol = cols > 0 ? curPos % cols : 0;
|
||||
boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel();
|
||||
boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine();
|
||||
|
||||
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
|
||||
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected);
|
||||
} else if (client.getPS() != null) {
|
||||
client.getPS().pasteString(text, curRow, curCol);
|
||||
} else {
|
||||
for (char ch : text.toCharArray()) {
|
||||
if (ch == '\n' || ch == '\r') {
|
||||
continue;
|
||||
}
|
||||
if (ch >= 0x20 && ch != 0x7F) {
|
||||
client.typeCharacter(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshScreen();
|
||||
@@ -564,53 +607,20 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Font auto-resize ==========
|
||||
|
||||
private void autoFitFont() {
|
||||
int panelW = getWidth();
|
||||
int panelH = getHeight();
|
||||
if (panelW <= 0 || panelH <= 0) return;
|
||||
|
||||
int termCols = 80;
|
||||
int termRows = 24;
|
||||
if (client != null) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
termCols = sb.getDisplayCols();
|
||||
termRows = sb.getDisplayRows();
|
||||
}
|
||||
|
||||
int availW = panelW - 2 * padding;
|
||||
int availH = panelH - 2 * padding;
|
||||
if (availW <= 0 || availH <= 0) return;
|
||||
|
||||
int bestSize = 8;
|
||||
for (int testSize = 8; testSize <= 72; testSize++) {
|
||||
Font testFont = new Font(terminalFont.getFamily(), Font.PLAIN, testSize);
|
||||
FontMetrics fm = getFontMetrics(testFont);
|
||||
int testCellW = fm.charWidth('M');
|
||||
int testCellH = fm.getHeight();
|
||||
|
||||
if (testCellW * termCols <= availW && testCellH * termRows <= availH) {
|
||||
bestSize = testSize;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestSize != currentFontSize) {
|
||||
currentFontSize = bestSize;
|
||||
haus.nightmare.j3270.config.Settings.setFontSize(bestSize);
|
||||
terminalFont = new Font(terminalFont.getFamily(), Font.PLAIN, bestSize);
|
||||
updateCellSize();
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void guardedPack() {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(this::guardedPack);
|
||||
return;
|
||||
}
|
||||
resizeGuard = true;
|
||||
revalidate();
|
||||
Container top = getTopLevelAncestor();
|
||||
if (top instanceof java.awt.Window) {
|
||||
if (top instanceof java.awt.Frame) {
|
||||
if ((((java.awt.Frame) top).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) {
|
||||
((java.awt.Window) top).pack();
|
||||
}
|
||||
} else if (top instanceof java.awt.Window) {
|
||||
((java.awt.Window) top).pack();
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> resizeGuard = false);
|
||||
@@ -643,7 +653,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
|
||||
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
|
||||
for (String key : navKeys) {
|
||||
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding(key, key);
|
||||
String defBinding = haus.nightmare.j3270.config.Settings.getDefaultBinding(key);
|
||||
String binding = haus.nightmare.j3270.config.Settings.getKeyBinding(key, defBinding);
|
||||
bindKeyToMap(im, key, binding);
|
||||
}
|
||||
|
||||
@@ -671,18 +682,31 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
bindKeyToMap(im, "SYSREQ", haus.nightmare.j3270.config.Settings.getKeyBinding("SYSREQ", "alt S"));
|
||||
bindKeyToMap(im, "CURSEL", haus.nightmare.j3270.config.Settings.getKeyBinding("CURSEL", "alt Q"));
|
||||
|
||||
// Operational Mode keybindings
|
||||
bindKeyToMap(im, "DOCMODE", haus.nightmare.j3270.config.Settings.getKeyBinding("DOCMODE", "alt F1"));
|
||||
bindKeyToMap(im, "WORDWRAP", haus.nightmare.j3270.config.Settings.getKeyBinding("WORDWRAP", "alt F2"));
|
||||
bindKeyToMap(im, "APL", haus.nightmare.j3270.config.Settings.getKeyBinding("APL", "alt F3"));
|
||||
|
||||
// Copy/Paste/Lightpen bindings
|
||||
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, shortcutMask), "j3270-SELECTALL");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN");
|
||||
String defCopy = haus.nightmare.j3270.config.Settings.getDefaultBinding("COPY");
|
||||
String defPaste = haus.nightmare.j3270.config.Settings.getDefaultBinding("PASTE");
|
||||
String defSelectAll = haus.nightmare.j3270.config.Settings.getDefaultBinding("SELECTALL");
|
||||
bindKeyToMap(im, "COPY", haus.nightmare.j3270.config.Settings.getKeyBinding("COPY", defCopy));
|
||||
bindKeyToMap(im, "PASTE", haus.nightmare.j3270.config.Settings.getKeyBinding("PASTE", defPaste));
|
||||
bindKeyToMap(im, "SELECTALL", haus.nightmare.j3270.config.Settings.getKeyBinding("SELECTALL", defSelectAll));
|
||||
bindKeyToMap(im, "LIGHTPEN", haus.nightmare.j3270.config.Settings.getKeyBinding("LIGHTPEN", "alt L"));
|
||||
|
||||
String defStatusBar = haus.nightmare.j3270.config.Settings.getDefaultBinding("STATUS_BAR");
|
||||
bindKeyToMap(im, "STATUS_BAR", haus.nightmare.j3270.config.Settings.getKeyBinding("STATUS_BAR", defStatusBar));
|
||||
|
||||
// Action map implementations
|
||||
am.put("j3270-ENTER", createAction(this::handleEnter));
|
||||
am.put("j3270-ESCAPE", createAction(this::handleReset));
|
||||
am.put("j3270-TAB", createAction(() -> handleTab(false)));
|
||||
am.put("j3270-shift TAB", createAction(() -> handleTab(true)));
|
||||
am.put("j3270-DOCMODE", createAction(this::toggleDocMode));
|
||||
am.put("j3270-WORDWRAP", createAction(this::toggleWordWrap));
|
||||
am.put("j3270-APL", createAction(this::toggleAplMode));
|
||||
am.put("j3270-UP", createAction(() -> handleCursor("up")));
|
||||
am.put("j3270-DOWN", createAction(() -> handleCursor("down")));
|
||||
am.put("j3270-LEFT", createAction(() -> handleCursor("left")));
|
||||
@@ -693,7 +717,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
am.put("j3270-END", createAction(this::handleEraseEOF));
|
||||
am.put("j3270-DELETE", createAction(this::handleDelete));
|
||||
am.put("j3270-BACK_SPACE", createAction(this::handleBackspace));
|
||||
am.put("j3270-INSERT", createAction(this::handleInsert));
|
||||
am.put("j3270-INSERT", createAction(this::toggleInsertMode));
|
||||
am.put("j3270-CLEAR", createAction(this::handleClear));
|
||||
am.put("j3270-ERASE_INPUT", createAction(this::handleEraseInput));
|
||||
am.put("j3270-NEWLINE", createAction(this::handleNewline));
|
||||
@@ -707,6 +731,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||
am.put("j3270-SELECTALL", createAction(this::selectAll));
|
||||
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
|
||||
am.put("j3270-STATUS_BAR", createAction(this::toggleStatusBar));
|
||||
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
final int pf = i;
|
||||
@@ -821,6 +846,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
} else if (state.isFullSession()) {
|
||||
client.sendEnter();
|
||||
refreshScreen();
|
||||
fireModeChanged();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -837,6 +864,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
clearSelection();
|
||||
client.reset();
|
||||
refreshScreen();
|
||||
fireModeChanged();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,6 +934,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
client.sendPF(n);
|
||||
refreshScreen();
|
||||
fireModeChanged();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -930,6 +961,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
client.sendPA(n);
|
||||
refreshScreen();
|
||||
fireModeChanged();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -973,12 +1006,65 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInsert() {
|
||||
if (client != null) {
|
||||
public void toggleInsertMode() {
|
||||
if (client != null && client.getInputProcessor() != null) {
|
||||
var ip = client.getInputProcessor();
|
||||
ip.setInsertMode(!ip.isInsertMode());
|
||||
refreshScreen();
|
||||
insertMode = ip.isInsertMode();
|
||||
} else {
|
||||
insertMode = !insertMode;
|
||||
}
|
||||
refreshScreen();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
fireModeChanged();
|
||||
}
|
||||
|
||||
public boolean isInsertMode() {
|
||||
if (client != null && client.getInputProcessor() != null) {
|
||||
return client.getInputProcessor().isInsertMode();
|
||||
}
|
||||
return insertMode;
|
||||
}
|
||||
|
||||
public void toggleDocMode() {
|
||||
if (client != null) {
|
||||
client.toggleDocMode();
|
||||
haus.nightmare.j3270.config.Settings.setEntryAssistDocMode(client.isDocMode());
|
||||
refreshScreen();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
fireModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleWordWrap() {
|
||||
if (client != null) {
|
||||
client.toggleWordWrap();
|
||||
haus.nightmare.j3270.config.Settings.setEntryAssistWordWrap(client.isWordWrap());
|
||||
refreshScreen();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
fireModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleAplMode() {
|
||||
if (client != null) {
|
||||
client.toggleAplMode();
|
||||
refreshScreen();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
fireModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDocMode() {
|
||||
return client != null && client.isDocMode();
|
||||
}
|
||||
|
||||
public boolean isWordWrap() {
|
||||
return client != null && client.isWordWrap();
|
||||
}
|
||||
|
||||
public boolean isAplMode() {
|
||||
return client != null && client.isAplMode();
|
||||
}
|
||||
|
||||
private void handleClear() {
|
||||
@@ -991,6 +1077,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
client.sendClear();
|
||||
refreshScreen();
|
||||
fireModeChanged();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,11 +1134,73 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
public void setStatusBar(StatusBar statusBar) {
|
||||
this.statusBar = statusBar;
|
||||
}
|
||||
|
||||
public StatusBar getStatusBar() {
|
||||
return statusBar;
|
||||
}
|
||||
|
||||
private Runnable statusBarToggleCallback;
|
||||
|
||||
public void setStatusBarToggleCallback(Runnable callback) {
|
||||
this.statusBarToggleCallback = callback;
|
||||
}
|
||||
|
||||
public void toggleStatusBar() {
|
||||
if (statusBarToggleCallback != null) {
|
||||
statusBarToggleCallback.run();
|
||||
} else if (statusBar != null) {
|
||||
boolean visible = !statusBar.isVisible();
|
||||
statusBar.setVisible(visible);
|
||||
haus.nightmare.j3270.config.Settings.setStatusBarVisible(visible);
|
||||
guardedPack();
|
||||
}
|
||||
}
|
||||
|
||||
public void applyModeSettings() {
|
||||
if (client != null) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
if (sb != null) {
|
||||
sb.setEntryAssistDOCmode(haus.nightmare.j3270.config.Settings.getEntryAssistDocMode());
|
||||
sb.setEntryAssistWordWrap(haus.nightmare.j3270.config.Settings.getEntryAssistWordWrap());
|
||||
sb.setLeftMargin(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistStartCol() - 1));
|
||||
sb.setRightMargin(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistEndCol() - 1));
|
||||
sb.setWordTabPositions(haus.nightmare.j3270.config.Settings.getEntryAssistTabStopsArray());
|
||||
}
|
||||
haus.nightmare.lib3270j.input.InputProcessor ip = client.getInputProcessor();
|
||||
if (ip != null) {
|
||||
ip.setBellEnabled(haus.nightmare.j3270.config.Settings.getEntryAssistBell());
|
||||
ip.setBellColumn(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistBellCol() - 1));
|
||||
if (ip.getBellListener() == null) {
|
||||
ip.setBellListener(() -> {
|
||||
SwingUtilities.invokeLater(() -> Toolkit.getDefaultToolkit().beep());
|
||||
});
|
||||
}
|
||||
ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
|
||||
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
|
||||
ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
|
||||
}
|
||||
if (client.getPS() != null) {
|
||||
client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel());
|
||||
client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine());
|
||||
}
|
||||
}
|
||||
if (statusBar != null) {
|
||||
statusBar.updateStatus();
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void refreshScreen() {
|
||||
if (client != null) {
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
}
|
||||
repaint();
|
||||
if (statusBar != null) {
|
||||
statusBar.updateStatus();
|
||||
}
|
||||
Container parent = getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof JFrame) {
|
||||
@@ -1088,14 +1238,35 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
setupColors();
|
||||
setupKeyBindings();
|
||||
setupFont();
|
||||
applyModeSettings();
|
||||
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
|
||||
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
|
||||
inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
|
||||
inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
|
||||
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
|
||||
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
public boolean isInputMaskEnabled() {
|
||||
return inputMaskEnabled;
|
||||
}
|
||||
|
||||
public void setInputMaskEnabled(boolean enabled) {
|
||||
this.inputMaskEnabled = enabled;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public String getInputMaskChar() {
|
||||
return inputMaskChar;
|
||||
}
|
||||
|
||||
public void setInputMaskChar(String maskChar) {
|
||||
this.inputMaskChar = maskChar;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setFontSize(int size) {
|
||||
currentFontSize = size;
|
||||
haus.nightmare.j3270.config.Settings.setFontSize(size);
|
||||
@@ -1137,6 +1308,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (client != null) {
|
||||
setupGraphicsPlaneRenderer();
|
||||
updateCellSize();
|
||||
applyModeSettings();
|
||||
|
||||
if (client.getInputProcessor() != null) {
|
||||
client.getInputProcessor().setInsertMode(insertMode);
|
||||
client.getInputProcessor().setBellListener(() -> {
|
||||
SwingUtilities.invokeLater(() -> Toolkit.getDefaultToolkit().beep());
|
||||
});
|
||||
}
|
||||
|
||||
client.setNvtClipboardHandler(new haus.nightmare.lib3270j.nvt.NvtProcessor.ClipboardHandler() {
|
||||
@Override
|
||||
@@ -1169,6 +1348,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
});
|
||||
});
|
||||
}
|
||||
fireModeChanged();
|
||||
if (statusBar != null) statusBar.updateStatus();
|
||||
}
|
||||
|
||||
private void setupGraphicsPlaneRenderer() {
|
||||
@@ -1290,8 +1471,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
||||
long currentUpdateCount = client.getGraphicsPlane().getUpdateCount();
|
||||
if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) {
|
||||
cachedGraphicsImage = new java.awt.image.BufferedImage(gWidth, gHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
cachedGraphicsImage.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
|
||||
cachedGraphicsImage = AwtPixelBufferBridge.toBufferedImage(client.getGraphicsPlane());
|
||||
lastGraphicsUpdateCount = currentUpdateCount;
|
||||
}
|
||||
if (gWidth == gridW && gHeight == gridH) {
|
||||
@@ -1369,12 +1549,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
// Password fields
|
||||
if (faIsZero(currentFA & 0xFF)) {
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? boldTerminalFont : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
g2.drawString("*", x, y + fontAscent);
|
||||
if (inputMaskEnabled) {
|
||||
char ch = ea.ucs4;
|
||||
if (ch > 0x20 && ch != 0xFF) {
|
||||
Font f = bold ? boldTerminalFont : terminalFont;
|
||||
g2.setFont(f);
|
||||
g2.setColor(fgColor);
|
||||
String mask = (inputMaskChar != null && !inputMaskChar.isEmpty()) ? inputMaskChar : "*";
|
||||
g2.drawString(mask, x, y + fontAscent);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCellSelected(row, col)) {
|
||||
@@ -1406,7 +1589,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
|
||||
if (slot != null) {
|
||||
int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0;
|
||||
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), symBg);
|
||||
haus.nightmare.lib3270j.graphics.PixelBuffer symPb = slot.getScaledPixelBuffer(cellWidth, cellHeight, fgColor.getRGB(), symBg);
|
||||
java.awt.image.BufferedImage img = AwtPixelBufferBridge.toBufferedImage(symPb);
|
||||
if (img != null) {
|
||||
g2.drawImage(img, x, y, null);
|
||||
drawnAsPs = true;
|
||||
@@ -1549,7 +1733,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
public void setHodWallpaper(haus.nightmare.lib3270j.graphics.HODWallpaper wallpaper) {
|
||||
this.hodWallpaper = wallpaper;
|
||||
if (wallpaper != null) {
|
||||
this.wallpaperImage = wallpaper.getHODImage();
|
||||
Object hodImg = wallpaper.getHODImage();
|
||||
if (hodImg instanceof Image) {
|
||||
this.wallpaperImage = (Image) hodImg;
|
||||
} else if (hodImg instanceof haus.nightmare.lib3270j.graphics.PixelBuffer) {
|
||||
this.wallpaperImage = AwtPixelBufferBridge.toBufferedImage((haus.nightmare.lib3270j.graphics.PixelBuffer) hodImg);
|
||||
}
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
@@ -1564,10 +1753,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
|
||||
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
|
||||
int fg = ea.fg != 0 ? (ea.fg & 0xFF)
|
||||
: (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
|
||||
if (fg >= 0xf0 && fg <= 0xff) {
|
||||
return hostColors[fg - 0xf0];
|
||||
if (!haus.nightmare.j3270.config.Settings.getFourColorOverride()) {
|
||||
int fg = ea.fg != 0 ? (ea.fg & 0xFF)
|
||||
: (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
|
||||
if (fg >= 0xf0 && fg <= 0xff) {
|
||||
return hostColors[fg - 0xf0];
|
||||
}
|
||||
}
|
||||
if (faIsProtected(currentFA & 0xFF)) {
|
||||
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_TURQUOISE];
|
||||
|
||||
@@ -251,11 +251,30 @@ public final class ThemeManager {
|
||||
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 HOD_OIA_STATUS_SYS_AVAIL; }
|
||||
public static Color getOiaInputInhibited() { return HOD_OIA_INPUT_INHIBITED; }
|
||||
public static Color getOiaAttention() { return HOD_OIA_ATTENTION_WARN; }
|
||||
public static Color getOiaCommCheck() { return HOD_OIA_COMM_CHECK_ERROR; }
|
||||
public static Color getOiaBackground() { return HOD_OIA_BG_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
|
||||
@@ -647,6 +666,16 @@ public final class ThemeManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof StatusBar) {
|
||||
((StatusBar) comp).applyTheme(theme);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof TerminalPanel) {
|
||||
comp.repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp instanceof JMenuBar) {
|
||||
styleMenuBar((JMenuBar) comp);
|
||||
for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) {
|
||||
|
||||
@@ -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,176 @@
|
||||
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;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
|
||||
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
|
||||
origInputMask = Settings.getInputMask();
|
||||
origInputMaskChar = Settings.getInputMaskChar();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
|
||||
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
|
||||
Settings.setInputMask(origInputMask);
|
||||
Settings.setInputMaskChar(origInputMaskChar);
|
||||
}
|
||||
|
||||
@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("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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
originalFontSize = Settings.getFontSize();
|
||||
originalFontFamily = Settings.getFontFamily();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setFontSize(originalFontSize);
|
||||
Settings.setFontFamily(originalFontFamily);
|
||||
}
|
||||
|
||||
@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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ public class MenuBarShortcutsTest {
|
||||
assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
|
||||
assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
|
||||
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");
|
||||
|
||||
@@ -37,6 +37,7 @@ public class Phase1UiOverlayTest {
|
||||
@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());
|
||||
|
||||
@@ -51,6 +52,15 @@ public class Phase1UiOverlayTest {
|
||||
|
||||
// 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
|
||||
|
||||
@@ -62,8 +62,33 @@ public class ThemeManagerTest {
|
||||
// Verify Status Bar colors
|
||||
Color sbBg = ThemeManager.getStatusBarBg(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(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
|
||||
Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme);
|
||||
@@ -218,4 +243,23 @@ public class ThemeManagerTest {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,17 @@ public class ConnectionConfig {
|
||||
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
|
||||
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
|
||||
|
||||
// IBM HoD autoSysUnlock parity
|
||||
private boolean autoSysUnlock = true;
|
||||
|
||||
// Phase 11: Enterprise Connection Resilience & Heartbeat
|
||||
private boolean keepAliveEnabled = true;
|
||||
private int keepAliveIntervalSeconds = 120;
|
||||
private String keepAliveType = "NOP";
|
||||
private boolean autoReconnect = false;
|
||||
private int reconnectMaxRetries = 5;
|
||||
private int tcpUserTimeoutMs = 0;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
@@ -280,6 +291,27 @@ public class ConnectionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() { return autoSysUnlock; }
|
||||
public void setAutoSysUnlock(boolean autoSysUnlock) { this.autoSysUnlock = autoSysUnlock; }
|
||||
|
||||
public boolean isKeepAliveEnabled() { return keepAliveEnabled; }
|
||||
public void setKeepAliveEnabled(boolean enabled) { this.keepAliveEnabled = enabled; }
|
||||
|
||||
public int getKeepAliveIntervalSeconds() { return keepAliveIntervalSeconds; }
|
||||
public void setKeepAliveIntervalSeconds(int seconds) { this.keepAliveIntervalSeconds = seconds; }
|
||||
|
||||
public String getKeepAliveType() { return keepAliveType; }
|
||||
public void setKeepAliveType(String type) { this.keepAliveType = (type != null) ? type.trim().toUpperCase() : "NOP"; }
|
||||
|
||||
public boolean isAutoReconnect() { return autoReconnect; }
|
||||
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
|
||||
|
||||
public int getReconnectMaxRetries() { return reconnectMaxRetries; }
|
||||
public void setReconnectMaxRetries(int retries) { this.reconnectMaxRetries = Math.max(0, retries); }
|
||||
|
||||
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
|
||||
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
|
||||
@@ -303,10 +335,21 @@ public class ConnectionConfig {
|
||||
String pUser = null;
|
||||
String pPass = null;
|
||||
|
||||
boolean keepAlive = true;
|
||||
boolean autoReconnect = false;
|
||||
|
||||
String[] tokens = s.split("\\s+");
|
||||
StringBuilder remaining = new StringBuilder();
|
||||
for (String tok : tokens) {
|
||||
if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
|
||||
if (tok.equalsIgnoreCase("--keepalive") || tok.equalsIgnoreCase("-keepalive")) {
|
||||
keepAlive = true;
|
||||
} else if (tok.equalsIgnoreCase("--no-keepalive") || tok.equalsIgnoreCase("-no-keepalive")) {
|
||||
keepAlive = false;
|
||||
} else if (tok.equalsIgnoreCase("--autoreconnect") || tok.equalsIgnoreCase("-autoreconnect")) {
|
||||
autoReconnect = true;
|
||||
} else if (tok.equalsIgnoreCase("--no-autoreconnect") || tok.equalsIgnoreCase("-no-autoreconnect")) {
|
||||
autoReconnect = false;
|
||||
} else if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
|
||||
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
|
||||
try {
|
||||
java.net.URI uri = new java.net.URI(proxyUrl);
|
||||
@@ -392,6 +435,14 @@ public class ConnectionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse [lu@]host[:port] (e.g. "00C2@mvs.host.com:1023")
|
||||
String parsedLu = null;
|
||||
int atIdx = s.indexOf('@');
|
||||
if (atIdx > 0 && atIdx < s.length() - 1) {
|
||||
parsedLu = s.substring(0, atIdx).trim();
|
||||
s = s.substring(atIdx + 1).trim();
|
||||
}
|
||||
|
||||
String host = s;
|
||||
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||
|
||||
@@ -415,8 +466,13 @@ public class ConnectionConfig {
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
if (parsedLu != null && !parsedLu.isEmpty()) {
|
||||
config.setLuName(parsedLu);
|
||||
}
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
config.setKeepAliveEnabled(keepAlive);
|
||||
config.setAutoReconnect(autoReconnect);
|
||||
if (dynamic) {
|
||||
config.setDynamicDimensions(dynRows, dynCols);
|
||||
}
|
||||
@@ -434,7 +490,7 @@ public class ConnectionConfig {
|
||||
return terminalName;
|
||||
}
|
||||
if (isDynamicModel()) {
|
||||
return extendedDataStream ? "IBM-DYNAMIC-E" : "IBM-DYNAMIC";
|
||||
return "IBM-DYNAMIC";
|
||||
}
|
||||
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ public class Telnet3270Client {
|
||||
private final haus.nightmare.lib3270j.ecl.ECLXfer xfer;
|
||||
private TelnetConnection connection;
|
||||
|
||||
private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
private volatile Thread reconnectThread;
|
||||
|
||||
public Telnet3270Client(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
this.translator = new EbcdicTranslator(config.getCodePage());
|
||||
@@ -58,6 +61,7 @@ public class Telnet3270Client {
|
||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||
}
|
||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||
this.dsProcessor.setAutoSysUnlock(config.isAutoSysUnlock());
|
||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||
@@ -87,6 +91,33 @@ public class Telnet3270Client {
|
||||
@Override public void onSoundAlarm() {
|
||||
ps.notifyAlarm();
|
||||
}
|
||||
@Override public void onKeyboardUnlocked() {
|
||||
ps.notifyKeyUnlocked();
|
||||
}
|
||||
});
|
||||
|
||||
// Phase 11: Auto-reconnection and OIA status coordinator
|
||||
fsm.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
if (newState == ConnectionState.RECONNECTING) {
|
||||
if (oia != null) {
|
||||
oia.writeToOIA("X RECONNECT");
|
||||
}
|
||||
initiateAutoReconnect();
|
||||
} else if (newState == ConnectionState.NOT_CONNECTED) {
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(haus.nightmare.lib3270j.ecl.ECLOIA.INHIBIT_COMMCHECK);
|
||||
}
|
||||
} else if (newState.isFullSession()) {
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionError(String message) {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,11 +190,146 @@ public class Telnet3270Client {
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
cancelAutoReconnect();
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
fsm.onDisconnect();
|
||||
fsm.onDisconnect(false);
|
||||
}
|
||||
|
||||
public void cancelAutoReconnect() {
|
||||
reconnecting.set(false);
|
||||
if (reconnectThread != null) {
|
||||
reconnectThread.interrupt();
|
||||
reconnectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void initiateAutoReconnect() {
|
||||
if (config == null || !config.isAutoReconnect()) {
|
||||
return;
|
||||
}
|
||||
if (!reconnecting.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
reconnectThread = new Thread(this::runAutoReconnect, "TN3270-AutoReconnect");
|
||||
reconnectThread.setDaemon(true);
|
||||
reconnectThread.start();
|
||||
}
|
||||
|
||||
private void runAutoReconnect() {
|
||||
int maxRetries = (config != null) ? config.getReconnectMaxRetries() : 5;
|
||||
log.info("Starting automatic reconnection loop (maxRetries=" + maxRetries + ")");
|
||||
try {
|
||||
for (int attempt = 1; attempt <= maxRetries && reconnecting.get(); attempt++) {
|
||||
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s
|
||||
long delaySeconds = Math.min(30, (long) Math.pow(2, attempt - 1));
|
||||
log.info("Auto-reconnect attempt " + attempt + "/" + maxRetries + " scheduled in " + delaySeconds + "s");
|
||||
|
||||
for (int s = 0; s < delaySeconds * 10; s++) {
|
||||
if (!reconnecting.get() || Thread.currentThread().isInterrupted()) {
|
||||
log.info("Auto-reconnect cancelled during backoff delay");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
log.info("Auto-reconnect thread interrupted");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!reconnecting.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Executing auto-reconnect attempt " + attempt + "/" + maxRetries + " to " + config.getHost() + ":" + config.getPort());
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
fsm.resetSessionState();
|
||||
screenBuffer.erase(false);
|
||||
|
||||
connection = new TelnetConnection(config, fsm);
|
||||
fsm.setConnection(connection);
|
||||
connection.connect();
|
||||
fsm.onConnected();
|
||||
|
||||
log.info("Auto-reconnect successful on attempt " + attempt);
|
||||
reconnecting.set(false);
|
||||
fsm.notifyScreenUpdate();
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
log.warning("Auto-reconnect attempt " + attempt + " failed: " + e.getMessage());
|
||||
if (attempt < maxRetries && reconnecting.get()) {
|
||||
fsm.setConnectionState(ConnectionState.RECONNECTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All retries failed
|
||||
log.warning("All " + maxRetries + " automatic reconnection attempts failed");
|
||||
reconnecting.set(false);
|
||||
fsm.onDisconnect(false);
|
||||
fsm.onError("Automatic reconnection failed after " + maxRetries + " attempts");
|
||||
} finally {
|
||||
reconnecting.set(false);
|
||||
reconnectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAutoReconnect() {
|
||||
return (config != null) && config.isAutoReconnect();
|
||||
}
|
||||
|
||||
public void setAutoReconnect(boolean autoReconnect) {
|
||||
if (config != null) {
|
||||
config.setAutoReconnect(autoReconnect);
|
||||
}
|
||||
}
|
||||
|
||||
public int getReconnectMaxRetries() {
|
||||
return (config != null) ? config.getReconnectMaxRetries() : 5;
|
||||
}
|
||||
|
||||
public void setReconnectMaxRetries(int retries) {
|
||||
if (config != null) {
|
||||
config.setReconnectMaxRetries(retries);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isReconnecting() {
|
||||
return reconnecting.get() || fsm.getConnectionState() == ConnectionState.RECONNECTING;
|
||||
}
|
||||
|
||||
public boolean isKeepAliveEnabled() {
|
||||
return (config != null) && config.isKeepAliveEnabled();
|
||||
}
|
||||
|
||||
public void setKeepAliveEnabled(boolean enabled) {
|
||||
if (config != null) {
|
||||
config.setKeepAliveEnabled(enabled);
|
||||
}
|
||||
if (connection != null) {
|
||||
if (enabled) connection.startKeepAlive();
|
||||
else connection.stopKeepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
public int getKeepAliveIntervalSeconds() {
|
||||
return (config != null) ? config.getKeepAliveIntervalSeconds() : 120;
|
||||
}
|
||||
|
||||
public void setKeepAliveIntervalSeconds(int seconds) {
|
||||
if (config != null) {
|
||||
config.setKeepAliveIntervalSeconds(seconds);
|
||||
}
|
||||
if (connection != null && config != null && config.isKeepAliveEnabled()) {
|
||||
connection.startKeepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,6 +460,23 @@ public class Telnet3270Client {
|
||||
return fsm;
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() {
|
||||
return (config != null) ? config.isAutoSysUnlock() : true;
|
||||
}
|
||||
|
||||
public void setAutoSysUnlock(boolean autoSysUnlock) {
|
||||
if (config != null) {
|
||||
config.setAutoSysUnlock(autoSysUnlock);
|
||||
}
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.setAutoSysUnlock(autoSysUnlock);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isContentionResolution() {
|
||||
return fsm != null && fsm.isContentionResolutionNegotiated();
|
||||
}
|
||||
|
||||
/** Send an NVT ASCII character in NVT mode. */
|
||||
public void sendNVTChar(char c) throws IOException {
|
||||
fsm.sendNVTChar(c);
|
||||
@@ -462,8 +645,23 @@ public class Telnet3270Client {
|
||||
public void processFieldMark() { inputProcessor.processFieldMark(); }
|
||||
|
||||
/** Toggle Insert Mode. */
|
||||
public void toggleInsert() { inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
|
||||
public void processToggleInsert() { inputProcessor.processToggleInsert(); }
|
||||
public boolean isInsertMode() { return inputProcessor != null && inputProcessor.isInsertMode(); }
|
||||
public void toggleInsert() { if (inputProcessor != null) inputProcessor.setInsertMode(!inputProcessor.isInsertMode()); }
|
||||
public void processToggleInsert() { if (inputProcessor != null) inputProcessor.processToggleInsert(); }
|
||||
|
||||
/** Document Mode (Entry Assist) operations. */
|
||||
public boolean isDocMode() { return screenBuffer != null && screenBuffer.isEntryAssistDOCmode(); }
|
||||
public void setDocMode(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistDOCmode(b); if (oia != null) oia.notifyOIAChanged(); }
|
||||
public void toggleDocMode() { setDocMode(!isDocMode()); }
|
||||
|
||||
public boolean isWordWrap() { return screenBuffer != null && screenBuffer.isEntryAssistWordWrap(); }
|
||||
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
|
||||
public void toggleWordWrap() { setWordWrap(!isWordWrap()); }
|
||||
|
||||
/** APL Keyboard Mode operations. */
|
||||
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
|
||||
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
|
||||
public void toggleAplMode() { if (inputProcessor != null) inputProcessor.toggleAplKeyboardMode(); }
|
||||
|
||||
/** Move word left. */
|
||||
public void processWordLeft() { inputProcessor.processWordLeft(); }
|
||||
|
||||
@@ -46,11 +46,11 @@ public enum TerminalModel {
|
||||
/**
|
||||
* Returns the terminal type string for TN3270E negotiation.
|
||||
* e.g., "IBM-3279-4-E" for a color model 4 with extended data stream,
|
||||
* or "IBM-DYNAMIC-E" for dynamic model.
|
||||
* or "IBM-DYNAMIC" for dynamic model.
|
||||
*/
|
||||
public String getTerminalType() {
|
||||
if (modelNumber == 0) {
|
||||
return "IBM-DYNAMIC-E";
|
||||
return "IBM-DYNAMIC";
|
||||
}
|
||||
return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* EBCDIC ↔ Unicode character translator conforming to IBM Host On-Demand (HoD v14).
|
||||
* EBCDIC ↔ Unicode character translator conforming to Host On-Demand (HoD v14).
|
||||
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
|
||||
* Supports custom per-instance character translation override tables and complete
|
||||
* IBM 3270 APL / Graphic Escape (GA23-0059) character mappings.
|
||||
* 3270 APL / Graphic Escape (GA23-0059) character mappings.
|
||||
* Default: Code Page 037 (US/Canada EBCDIC).
|
||||
*/
|
||||
public class EbcdicTranslator {
|
||||
@@ -210,9 +210,11 @@ public class EbcdicTranslator {
|
||||
*/
|
||||
public char ebcdicToUnicode(int ebc) {
|
||||
int b = ebc & 0xFF;
|
||||
Character custom = customEbcdicToUnicode.get(b);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
if (!customEbcdicToUnicode.isEmpty()) {
|
||||
Character custom = customEbcdicToUnicode.get(b);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
}
|
||||
return activeCodePage.ebcdicToUnicode(b);
|
||||
}
|
||||
@@ -236,9 +238,11 @@ public class EbcdicTranslator {
|
||||
* Returns -1 if the character cannot be mapped.
|
||||
*/
|
||||
public int unicodeToEbcdic(char unicode) {
|
||||
Integer custom = customUnicodeToEbcdic.get(unicode);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
if (!customUnicodeToEbcdic.isEmpty()) {
|
||||
Integer custom = customUnicodeToEbcdic.get(unicode);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
}
|
||||
return activeCodePage.unicodeToEbcdic(unicode);
|
||||
}
|
||||
@@ -296,8 +300,8 @@ public class EbcdicTranslator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
|
||||
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification.
|
||||
* Map a 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
|
||||
* Conforms to 3270 APL / Text character set and GA23-0059 specification.
|
||||
*/
|
||||
public char mapAPL(int ebcdicCodePoint) {
|
||||
switch (ebcdicCodePoint & 0xFF) {
|
||||
@@ -331,7 +335,7 @@ public class EbcdicTranslator {
|
||||
case 0xBF: return '\u00B5'; // Micro 'µ'
|
||||
case 0x5F: return '\u00AC'; // Not sign '¬'
|
||||
|
||||
// IBM 3270 APL Operational & Structural Glyphs
|
||||
// 3270 APL Operational & Structural Glyphs
|
||||
case 0x80: return '\u22C4'; // Diamond '⋄'
|
||||
case 0x81: return '\u237A'; // APL Alpha '⍺'
|
||||
case 0x82: return '\u22A5'; // Up Tack / Decode '⊥'
|
||||
@@ -383,7 +387,7 @@ public class EbcdicTranslator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode.
|
||||
* Translate a 3270 Graphic Escape (GE) / APL character code to Unicode.
|
||||
*/
|
||||
public char getAplGraphic(int ec) {
|
||||
return mapAPL(ec);
|
||||
|
||||
+80
-19
@@ -47,6 +47,19 @@ public class DataStreamProcessor {
|
||||
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||
private int currentGocaSubtype = 0;
|
||||
|
||||
// Phase 10: Auto-Unlock & Contention Resolution State
|
||||
private boolean autoSysUnlock = true;
|
||||
private boolean contentionResolution = false;
|
||||
private boolean unlockPending = false;
|
||||
private boolean unlockSysPending = false;
|
||||
private boolean rcvdRead = false;
|
||||
|
||||
// Modal SA (set attribute) character attributes
|
||||
private byte currentFg = 0;
|
||||
private byte currentBg = 0;
|
||||
private byte currentGr = 0;
|
||||
private byte currentCs = 0;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
@@ -131,6 +144,21 @@ public class DataStreamProcessor {
|
||||
screenListeners.remove(l);
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() { return autoSysUnlock; }
|
||||
public void setAutoSysUnlock(boolean autoSysUnlock) { this.autoSysUnlock = autoSysUnlock; }
|
||||
|
||||
public boolean isContentionResolution() { return contentionResolution; }
|
||||
public void setContentionResolution(boolean cr) { this.contentionResolution = cr; }
|
||||
|
||||
public boolean isUnlockPending() { return unlockPending; }
|
||||
public void setUnlockPending(boolean pending) { this.unlockPending = pending; }
|
||||
|
||||
public boolean isUnlockSysPending() { return unlockSysPending; }
|
||||
public void setUnlockSysPending(boolean pending) { this.unlockSysPending = pending; }
|
||||
|
||||
public boolean isRcvdRead() { return rcvdRead; }
|
||||
public void setRcvdRead(boolean rcvdRead) { this.rcvdRead = rcvdRead; }
|
||||
|
||||
/**
|
||||
* Process a 3270 data stream record.
|
||||
*
|
||||
@@ -197,16 +225,19 @@ public class DataStreamProcessor {
|
||||
break;
|
||||
case CMD_RB:
|
||||
case SNA_CMD_RB:
|
||||
rcvdRead = true;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadBuffer();
|
||||
break;
|
||||
case CMD_RM:
|
||||
case SNA_CMD_RM:
|
||||
rcvdRead = true;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadModified(false);
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
rcvdRead = true;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadModified(true);
|
||||
break;
|
||||
@@ -215,6 +246,10 @@ public class DataStreamProcessor {
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
@@ -235,7 +270,10 @@ public class DataStreamProcessor {
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
if (keyboardRestore && inputProcessor != null) {
|
||||
boolean isWriteCmd = (cmd == CMD_W || cmd == SNA_CMD_W ||
|
||||
cmd == CMD_EW || cmd == SNA_CMD_EW ||
|
||||
cmd == CMD_EWA || cmd == SNA_CMD_EWA);
|
||||
if (!isWriteCmd && keyboardRestore && inputProcessor != null && !contentionResolution) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
|
||||
@@ -333,10 +371,13 @@ public class DataStreamProcessor {
|
||||
log.fine("WCC: " + String.format("0x%02x", wcc) +
|
||||
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
|
||||
|
||||
if (kbdRestore || inputProcessor != null) {
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
if (kbdRestore) {
|
||||
unlockPending = true;
|
||||
unlockSysPending = true;
|
||||
}
|
||||
|
||||
if (!contentionResolution && kbdRestore && inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
|
||||
if (resetMdt) {
|
||||
@@ -347,11 +388,19 @@ public class DataStreamProcessor {
|
||||
if (wccReset(wcc)) {
|
||||
// Reset all character attributes to defaults
|
||||
log.fine("WCC reset: clearing default attributes");
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
}
|
||||
|
||||
if (eraseFirst) {
|
||||
screen.clear();
|
||||
log.fine("Cleared screen for Erase/Write");
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
}
|
||||
|
||||
// Process orders and data starting at byte 2
|
||||
@@ -359,9 +408,6 @@ public class DataStreamProcessor {
|
||||
int end = offset + length;
|
||||
int baddr = screen.getBufferAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
|
||||
// Current SA (set attribute) values for character-mode
|
||||
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
|
||||
boolean lastWasOrder = false;
|
||||
|
||||
while (pos < end) {
|
||||
@@ -396,10 +442,6 @@ public class DataStreamProcessor {
|
||||
// FA position is a display position that shows as blank
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = ' ';
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
screen.setFormatted(true);
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
@@ -424,10 +466,6 @@ public class DataStreamProcessor {
|
||||
ea.clear();
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = ' ';
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
|
||||
for (int i = 0; i < nPairs; i++) {
|
||||
int attrType = data[pos + 2 + i * 2] & 0xFF;
|
||||
@@ -454,6 +492,12 @@ public class DataStreamProcessor {
|
||||
int attrType = data[pos + 1] & 0xFF;
|
||||
int attrValue = data[pos + 2] & 0xFF;
|
||||
switch (attrType) {
|
||||
case XA_ALL:
|
||||
currentFg = 0;
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
break;
|
||||
case XA_FOREGROUND:
|
||||
currentFg = (byte) attrValue;
|
||||
break;
|
||||
@@ -953,7 +997,13 @@ public class DataStreamProcessor {
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||
if (fieldLen > 3) {
|
||||
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
int targetW = screen.getCols() * qrBuilder.getCharWidth();
|
||||
int targetH = screen.getRows() * qrBuilder.getCharHeight();
|
||||
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||
graphicsPlane.resize(targetW, targetH);
|
||||
}
|
||||
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
@@ -1101,6 +1151,9 @@ public class DataStreamProcessor {
|
||||
}
|
||||
|
||||
private void notifyScreenUpdated() {
|
||||
if (screen != null) {
|
||||
screen.notifyScreenUpdate();
|
||||
}
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
@@ -1264,9 +1317,10 @@ public class DataStreamProcessor {
|
||||
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
|
||||
int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
|
||||
|
||||
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
|
||||
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
|
||||
int targetW = screen.getCols() * 9;
|
||||
int targetH = screen.getRows() * 16;
|
||||
int targetW = screen.getCols() * qrBuilder.getCharWidth();
|
||||
int targetH = screen.getRows() * qrBuilder.getCharHeight();
|
||||
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
|
||||
graphicsPlane.resize(targetW, targetH);
|
||||
}
|
||||
@@ -1288,6 +1342,9 @@ public class DataStreamProcessor {
|
||||
byte[] fullStream = gocaAccumulator.toByteArray();
|
||||
gocaAccumulator.reset();
|
||||
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
|
||||
try {
|
||||
java.nio.file.Files.write(java.nio.file.Paths.get("/Users/rudi/Projects/j3270/captured_goca.bin"), fullStream);
|
||||
} catch (Exception ignored) {}
|
||||
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
|
||||
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
|
||||
} else {
|
||||
@@ -1546,7 +1603,11 @@ public class DataStreamProcessor {
|
||||
log.fine("processWCC: " + String.format("0x%02x", wcc) +
|
||||
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
|
||||
|
||||
if (kbdRestore && inputProcessor != null) {
|
||||
if (kbdRestore) {
|
||||
unlockPending = true;
|
||||
unlockSysPending = true;
|
||||
}
|
||||
if (!contentionResolution && kbdRestore && inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
if (resetMdt) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Reusable, resizable byte buffer that provides zero-copy access to its internal
|
||||
* array and supports ByteBuffer slicing to minimize heap allocations during
|
||||
* network stream processing.
|
||||
*/
|
||||
public class FastByteBuffer extends OutputStream {
|
||||
|
||||
private byte[] buf;
|
||||
private int count;
|
||||
|
||||
public FastByteBuffer() {
|
||||
this(32768);
|
||||
}
|
||||
|
||||
public FastByteBuffer(int initialCapacity) {
|
||||
this.buf = new byte[Math.max(32, initialCapacity)];
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
private void ensureCapacity(int minCapacity) {
|
||||
if (minCapacity > buf.length) {
|
||||
int newCap = Math.max(buf.length << 1, minCapacity);
|
||||
buf = Arrays.copyOf(buf, newCap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int b) {
|
||||
ensureCapacity(count + 1);
|
||||
buf[count++] = (byte) b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] b, int off, int len) {
|
||||
if (b == null || len <= 0) return;
|
||||
ensureCapacity(count + len);
|
||||
System.arraycopy(b, off, buf, count, len);
|
||||
count += len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct reference to internal buffer array.
|
||||
* Use {@link #size()} to determine active length.
|
||||
*/
|
||||
public synchronized byte[] buffer() {
|
||||
return buf;
|
||||
}
|
||||
|
||||
public synchronized int size() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public synchronized void reset() {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read-only ByteBuffer view wrapping active bytes without copying.
|
||||
*/
|
||||
public synchronized ByteBuffer asByteBuffer() {
|
||||
return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read-only ByteBuffer slice for a sub-range without copying.
|
||||
*/
|
||||
public synchronized ByteBuffer slice(int offset, int length) {
|
||||
if (offset < 0 || length < 0 || offset + length > count) {
|
||||
throw new IndexOutOfBoundsException("offset=" + offset + " length=" + length + " size=" + count);
|
||||
}
|
||||
return ByteBuffer.wrap(buf, offset, length).slice().asReadOnlyBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a copied byte array if an isolated copy is explicitly required.
|
||||
*/
|
||||
public synchronized byte[] toByteArray() {
|
||||
return Arrays.copyOf(buf, count);
|
||||
}
|
||||
}
|
||||
+154
-138
@@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
* Builds Query Reply structured fields in response to host Read Partition queries.
|
||||
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
|
||||
* Matches 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
|
||||
*/
|
||||
public class QueryReplyBuilder {
|
||||
|
||||
@@ -18,10 +18,102 @@ public class QueryReplyBuilder {
|
||||
private static final int SW_3279_2 = 0x09;
|
||||
private static final int SH_3279_2 = 0x0c;
|
||||
|
||||
// Usable Area physical dimensions matching IBM Host On-Demand DS3270.java (Inches, 96 dpi: 0x00010060)
|
||||
// Usable Area physical dimensions matching HOD DS3270.java (Inches, 96 dpi: 0x00010060)
|
||||
private static final int Xr_HOD = 0x00010060;
|
||||
private static final int Yr_HOD = 0x00010060;
|
||||
|
||||
// Pre-computed static query reply payloads to eliminate allocation churn
|
||||
private static final byte[] STATIC_QR_COLOR = new byte[] {
|
||||
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||
(byte) 0xF1, (byte) 0xF1, // Blue
|
||||
(byte) 0xF2, (byte) 0xF2, // Red
|
||||
(byte) 0xF3, (byte) 0xF3, // Pink
|
||||
(byte) 0xF4, (byte) 0xF4, // Green
|
||||
(byte) 0xF5, (byte) 0xF5, // Turquoise
|
||||
(byte) 0xF6, (byte) 0xF6, // Yellow
|
||||
(byte) 0xF7, (byte) 0xF7 // Neutral/White
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_HIGHLIGHTING = new byte[] {
|
||||
0x04, 0x00, (byte) 0xF0,
|
||||
(byte) 0xF1, (byte) 0xF1,
|
||||
(byte) 0xF2, (byte) 0xF2,
|
||||
(byte) 0xF4, (byte) 0xF4
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_REPLY_MODES = new byte[] {
|
||||
SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_OUTLINING = new byte[] {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_DBCS_ASIA = new byte[] {
|
||||
0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_AUXDA = new byte[] {
|
||||
0x00, 0x00
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_TRANSPARENCY = new byte[] {
|
||||
0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_SEGMENT = new byte[] {
|
||||
(byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_PROCEDURE = new byte[] {
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
|
||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_QR_LINETYPE = new byte[] {
|
||||
0x00, 0x09, 0x00, 0x07,
|
||||
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||
};
|
||||
|
||||
private static final byte[] STATIC_PORT_BLOCKS;
|
||||
static {
|
||||
ByteArrayOutputStream pOut = new ByteArrayOutputStream(64);
|
||||
byte[][] data = {
|
||||
{ 0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
|
||||
{ 0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02 },
|
||||
{ 0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
|
||||
{ 0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C }
|
||||
};
|
||||
for (byte[] d : data) {
|
||||
int len = 4 + d.length;
|
||||
pOut.write((len >> 8) & 0xFF);
|
||||
pOut.write(len & 0xFF);
|
||||
pOut.write(SFID_QREPLY);
|
||||
pOut.write(QR_PORT);
|
||||
pOut.write(d, 0, d.length);
|
||||
}
|
||||
STATIC_PORT_BLOCKS = pOut.toByteArray();
|
||||
}
|
||||
|
||||
private static final byte[] STATIC_QR_GRCOLOR;
|
||||
static {
|
||||
ByteArrayOutputStream gOut = new ByteArrayOutputStream(110);
|
||||
gOut.write(0x00); gOut.write(0x04); gOut.write(0x00); gOut.write(0xFF); gOut.write(0xFF);
|
||||
gOut.write(0x00); gOut.write(0x10); gOut.write(0x00); gOut.write(0x10);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
gOut.write(0x00);
|
||||
gOut.write(i);
|
||||
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||
gOut.write((argb >> 16) & 0xFF);
|
||||
gOut.write((argb >> 8) & 0xFF);
|
||||
gOut.write(argb & 0xFF);
|
||||
gOut.write(0x00);
|
||||
}
|
||||
STATIC_QR_GRCOLOR = gOut.toByteArray();
|
||||
}
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
|
||||
|
||||
@@ -101,27 +193,27 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
|
||||
// Color (0x86)
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
|
||||
|
||||
// Highlighting (0x87)
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
|
||||
|
||||
// Reply Modes (0x88)
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
|
||||
|
||||
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||
if (isDbcs) {
|
||||
// Outlining (0x8C)
|
||||
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||
appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
|
||||
// DBCS Asia (0x91)
|
||||
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||
appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
|
||||
}
|
||||
|
||||
// Distributed Data Management (0x95)
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
|
||||
// Auxiliary Devices (0x99)
|
||||
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
|
||||
|
||||
// Implicit Partition (0xA6)
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
@@ -141,27 +233,27 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
|
||||
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
|
||||
appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
|
||||
|
||||
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||
appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
|
||||
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
|
||||
if (isDbcs) {
|
||||
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||
appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
|
||||
}
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8
|
||||
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0
|
||||
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1
|
||||
appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2
|
||||
appendPort(out); // 0xB3
|
||||
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4
|
||||
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
|
||||
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
|
||||
appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
|
||||
appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
|
||||
appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
|
||||
appendPort(out); // 0xB3
|
||||
appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR); // 0xB4
|
||||
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
|
||||
}
|
||||
|
||||
log.info("Built " + out.size() + " bytes of complete query replies (graphicsMode=" + graphicsMode + ")");
|
||||
@@ -197,20 +289,20 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_CHARSETS, buildCharsets());
|
||||
break;
|
||||
case QR_COLOR:
|
||||
appendQueryReply(out, QR_COLOR, buildColor());
|
||||
appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
|
||||
break;
|
||||
case QR_HIGHLIGHTING:
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
|
||||
appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
|
||||
break;
|
||||
case QR_REPLY_MODES:
|
||||
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
|
||||
appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
|
||||
break;
|
||||
case QR_OUTLINING: // 0x8C
|
||||
appendQueryReply(out, QR_OUTLINING, buildOutlining());
|
||||
appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
|
||||
break;
|
||||
case QR_DBCS_ASIA: // 0x91
|
||||
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia());
|
||||
appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
@@ -219,35 +311,35 @@ public class QueryReplyBuilder {
|
||||
appendQueryReply(out, QR_DDM, buildDdm(4096));
|
||||
break;
|
||||
case QR_AUXDA: // 0x99
|
||||
appendQueryReply(out, QR_AUXDA, buildAuxDa());
|
||||
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
|
||||
break;
|
||||
case QR_IMP_PART: // 0xA6
|
||||
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
|
||||
break;
|
||||
case QR_TRANSPARENCY: // 0xA8
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
|
||||
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_SEGMENT: // 0xB0
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows));
|
||||
appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_PROCEDURE: // 0xB1
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows));
|
||||
appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_LINETYPE: // 0xB2
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_LINETYPE, buildLineType());
|
||||
appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
@@ -261,7 +353,7 @@ public class QueryReplyBuilder {
|
||||
break;
|
||||
case QR_GRCOLOR: // 0xB4
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GRCOLOR, buildGrColor());
|
||||
appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR);
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
@@ -321,19 +413,19 @@ public class QueryReplyBuilder {
|
||||
out.write(maxCols & 0xFF); // usable width low
|
||||
out.write((maxRows >> 8) & 0xFF); // usable height high
|
||||
out.write(maxRows & 0xFF); // usable height low
|
||||
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING)
|
||||
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
|
||||
out.write(0x00); // units (0x00 = inches, matching HOD QR_USEAREA_STRING)
|
||||
// Xr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
|
||||
out.write((Xr_HOD >> 24) & 0xFF);
|
||||
out.write((Xr_HOD >> 16) & 0xFF);
|
||||
out.write((Xr_HOD >> 8) & 0xFF);
|
||||
out.write(Xr_HOD & 0xFF);
|
||||
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI)
|
||||
// Yr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
|
||||
out.write((Yr_HOD >> 24) & 0xFF);
|
||||
out.write((Yr_HOD >> 16) & 0xFF);
|
||||
out.write((Yr_HOD >> 8) & 0xFF);
|
||||
out.write(Yr_HOD & 0xFF);
|
||||
int charW = getCharWidth();
|
||||
int charH = getCharHeight();
|
||||
int charW = getCharWidth(maxRows);
|
||||
int charH = getCharHeight(maxRows);
|
||||
out.write(charW); // AW
|
||||
out.write(charH); // AH
|
||||
int buf = maxCols * maxRows;
|
||||
@@ -343,6 +435,11 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public int getCharWidth() {
|
||||
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return getCharWidth(rows);
|
||||
}
|
||||
|
||||
public int getCharWidth(int rows) {
|
||||
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||
return 12;
|
||||
}
|
||||
@@ -350,18 +447,11 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public int getCharHeight() {
|
||||
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES:
|
||||
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations:
|
||||
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes
|
||||
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
|
||||
// GDDM then places the top menu bar at Row 1 (gy = 187..200).
|
||||
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
|
||||
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
|
||||
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
|
||||
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
|
||||
//
|
||||
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
|
||||
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
|
||||
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return getCharHeight(rows);
|
||||
}
|
||||
|
||||
public int getCharHeight(int rows) {
|
||||
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
|
||||
}
|
||||
|
||||
@@ -442,31 +532,15 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildColor() {
|
||||
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total)
|
||||
return new byte[] {
|
||||
0x00, 0x08, 0x00, (byte) 0xF4,
|
||||
(byte) 0xF1, (byte) 0xF1, // Blue
|
||||
(byte) 0xF2, (byte) 0xF2, // Red
|
||||
(byte) 0xF3, (byte) 0xF3, // Pink
|
||||
(byte) 0xF4, (byte) 0xF4, // Green
|
||||
(byte) 0xF5, (byte) 0xF5, // Turquoise
|
||||
(byte) 0xF6, (byte) 0xF6, // Yellow
|
||||
(byte) 0xF7, (byte) 0xF7 // Neutral/White
|
||||
};
|
||||
return STATIC_QR_COLOR.clone();
|
||||
}
|
||||
|
||||
public byte[] buildHighlighting() {
|
||||
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total)
|
||||
return new byte[] {
|
||||
0x04, 0x00, (byte) 0xF0,
|
||||
(byte) 0xF1, (byte) 0xF1,
|
||||
(byte) 0xF2, (byte) 0xF2,
|
||||
(byte) 0xF4, (byte) 0xF4
|
||||
};
|
||||
return STATIC_QR_HIGHLIGHTING.clone();
|
||||
}
|
||||
|
||||
public byte[] buildReplyModes() {
|
||||
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
|
||||
return STATIC_QR_REPLY_MODES.clone();
|
||||
}
|
||||
|
||||
public byte[] buildDdm() {
|
||||
@@ -517,39 +591,27 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildOutlining() {
|
||||
// HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000")
|
||||
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
return STATIC_QR_OUTLINING.clone();
|
||||
}
|
||||
|
||||
public byte[] buildDbcsAsia() {
|
||||
// HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001")
|
||||
return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 };
|
||||
return STATIC_QR_DBCS_ASIA.clone();
|
||||
}
|
||||
|
||||
public byte[] buildAuxDa() {
|
||||
// HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000")
|
||||
return new byte[]{ 0x00, 0x00 };
|
||||
return STATIC_QR_AUXDA.clone();
|
||||
}
|
||||
|
||||
public byte[] buildTransparency() {
|
||||
// HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff")
|
||||
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
|
||||
return STATIC_QR_TRANSPARENCY.clone();
|
||||
}
|
||||
|
||||
public byte[] buildSegment() {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildSegment(maxCols, maxRows);
|
||||
return STATIC_QR_SEGMENT.clone();
|
||||
}
|
||||
|
||||
public byte[] buildSegment(int maxCols, int maxRows) {
|
||||
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000")
|
||||
return new byte[]{
|
||||
(byte) 0x80, 0x02,
|
||||
0x00, 0x00,
|
||||
0x00, (byte) 0xFC,
|
||||
0x00
|
||||
};
|
||||
return STATIC_QR_SEGMENT.clone();
|
||||
}
|
||||
|
||||
public byte[] buildGraphics() {
|
||||
@@ -561,21 +623,11 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildProcedure() {
|
||||
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS;
|
||||
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
|
||||
return buildProcedure(maxCols, maxRows);
|
||||
return STATIC_QR_PROCEDURE.clone();
|
||||
}
|
||||
|
||||
public byte[] buildProcedure(int maxCols, int maxRows) {
|
||||
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0")
|
||||
return new byte[]{
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, (byte) 0xFC,
|
||||
0x00,
|
||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||
};
|
||||
return STATIC_QR_PROCEDURE.clone();
|
||||
}
|
||||
|
||||
public byte[] buildGImage() {
|
||||
@@ -587,12 +639,7 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildLineType() {
|
||||
// HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b")
|
||||
return new byte[]{
|
||||
0x00, 0x09, 0x00, 0x07,
|
||||
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
|
||||
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
|
||||
};
|
||||
return STATIC_QR_LINETYPE.clone();
|
||||
}
|
||||
|
||||
public byte[] buildAuxDev() {
|
||||
@@ -604,19 +651,7 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public void appendPort(ByteArrayOutputStream out) {
|
||||
// HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total)
|
||||
appendQueryReply(out, QR_PORT, new byte[]{
|
||||
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_PORT, new byte[]{
|
||||
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
|
||||
});
|
||||
appendQueryReply(out, QR_PORT, new byte[]{
|
||||
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
|
||||
});
|
||||
appendQueryReply(out, QR_PORT, new byte[]{
|
||||
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
|
||||
});
|
||||
out.write(STATIC_PORT_BLOCKS, 0, STATIC_PORT_BLOCKS.length);
|
||||
}
|
||||
|
||||
public void appendOemFmt(ByteArrayOutputStream out) {
|
||||
@@ -624,9 +659,7 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildPort() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(70);
|
||||
appendPort(out);
|
||||
return out.toByteArray();
|
||||
return STATIC_PORT_BLOCKS.clone();
|
||||
}
|
||||
|
||||
public byte[] buildOemFormat() {
|
||||
@@ -634,24 +667,7 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
public byte[] buildGrColor() {
|
||||
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
|
||||
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
|
||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
out.write(0x00);
|
||||
out.write(i);
|
||||
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
|
||||
int r = (argb >> 16) & 0xFF;
|
||||
int g = (argb >> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
out.write(r);
|
||||
out.write(g);
|
||||
out.write(b);
|
||||
out.write(0x00); // 6th byte in HOD color table
|
||||
}
|
||||
return out.toByteArray();
|
||||
return STATIC_QR_GRCOLOR.clone();
|
||||
}
|
||||
|
||||
public byte[] buildGraphicColor() {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* Thread-safe memory pool providing reusable byte buffers for high-throughput
|
||||
* 3270 stream operations, minimizing garbage collector pressure.
|
||||
*/
|
||||
public final class ReusableByteBufferPool {
|
||||
|
||||
public static final int SIZE_SMALL = 512;
|
||||
public static final int SIZE_MEDIUM = 4096;
|
||||
public static final int SIZE_LARGE = 32768;
|
||||
|
||||
private static final int MAX_POOLED_PER_TIER = 32;
|
||||
|
||||
private static final Queue<byte[]> smallPool = new ConcurrentLinkedQueue<>();
|
||||
private static final Queue<byte[]> mediumPool = new ConcurrentLinkedQueue<>();
|
||||
private static final Queue<byte[]> largePool = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private ReusableByteBufferPool() {}
|
||||
|
||||
/**
|
||||
* Acquires a pooled byte array with at least the specified capacity.
|
||||
*/
|
||||
public static byte[] acquire(int minCapacity) {
|
||||
if (minCapacity <= SIZE_SMALL) {
|
||||
byte[] b = smallPool.poll();
|
||||
return (b != null) ? b : new byte[SIZE_SMALL];
|
||||
} else if (minCapacity <= SIZE_MEDIUM) {
|
||||
byte[] b = mediumPool.poll();
|
||||
return (b != null) ? b : new byte[SIZE_MEDIUM];
|
||||
} else if (minCapacity <= SIZE_LARGE) {
|
||||
byte[] b = largePool.poll();
|
||||
return (b != null) ? b : new byte[SIZE_LARGE];
|
||||
}
|
||||
return new byte[minCapacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a ByteBuffer wrapping a pooled array up to minCapacity.
|
||||
*/
|
||||
public static ByteBuffer acquireByteBuffer(int minCapacity) {
|
||||
byte[] b = acquire(minCapacity);
|
||||
return ByteBuffer.wrap(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a buffer to the pool for reuse if it matches a standard tier.
|
||||
*/
|
||||
public static void release(byte[] buffer) {
|
||||
if (buffer == null) return;
|
||||
if (buffer.length == SIZE_SMALL && smallPool.size() < MAX_POOLED_PER_TIER) {
|
||||
smallPool.offer(buffer);
|
||||
} else if (buffer.length == SIZE_MEDIUM && mediumPool.size() < MAX_POOLED_PER_TIER) {
|
||||
mediumPool.offer(buffer);
|
||||
} else if (buffer.length == SIZE_LARGE && largePool.size() < MAX_POOLED_PER_TIER) {
|
||||
largePool.offer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all pools to release held memory.
|
||||
*/
|
||||
public static void clear() {
|
||||
smallPool.clear();
|
||||
mediumPool.clear();
|
||||
largePool.clear();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.event;
|
||||
|
||||
import java.awt.Image;
|
||||
import java.awt.Rectangle;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for ECLPSGraphicsEvent.
|
||||
@@ -12,11 +11,11 @@ public class ECLPSGraphicsEvent extends haus.nightmare.lib3270j.ecl.ECLPSGraphic
|
||||
super(source, id);
|
||||
}
|
||||
|
||||
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Image image) {
|
||||
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Object image) {
|
||||
super(source, id, image);
|
||||
}
|
||||
|
||||
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Image image, Rectangle rectangle) {
|
||||
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Object image, Rectangle rectangle) {
|
||||
super(source, id, image, rectangle);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import haus.nightmare.lib3270j.graphics.Color;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for FillArea.
|
||||
|
||||
+1
-3
@@ -1,12 +1,10 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||
|
||||
import java.awt.Component;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for HODBitImage.
|
||||
*/
|
||||
public class HODBitImage extends haus.nightmare.lib3270j.graphics.HODBitImage {
|
||||
public HODBitImage(Component comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
|
||||
public HODBitImage(Object comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
|
||||
super(comp, width, height, data, baseColor, depth, useGraphicColors);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-6
@@ -1,8 +1,7 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Rectangle;
|
||||
import haus.nightmare.lib3270j.graphics.Dimension;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for HODPart.
|
||||
@@ -12,15 +11,15 @@ public class HODPart extends haus.nightmare.lib3270j.graphics.HODPart {
|
||||
super();
|
||||
}
|
||||
|
||||
public HODPart(Component component) {
|
||||
public HODPart(Object component) {
|
||||
super(component);
|
||||
}
|
||||
|
||||
public HODPart(Component component, Dimension dimension) {
|
||||
public HODPart(Object component, Dimension dimension) {
|
||||
super(component, dimension);
|
||||
}
|
||||
|
||||
public HODPart(Component component, Rectangle rectangle) {
|
||||
public HODPart(Object component, Rectangle rectangle) {
|
||||
super(component, rectangle);
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -1,7 +1,5 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||
|
||||
import java.awt.Image;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for HODWallpaper.
|
||||
*/
|
||||
@@ -14,7 +12,7 @@ public class HODWallpaper extends haus.nightmare.lib3270j.graphics.HODWallpaper
|
||||
super(displayMode);
|
||||
}
|
||||
|
||||
public HODWallpaper(Image image, int displayMode) {
|
||||
public HODWallpaper(Object image, int displayMode) {
|
||||
super(image, displayMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import haus.nightmare.lib3270j.graphics.Color;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Standard implementation of ECLPSGraphicsServices conforming to IBM Host On-Demand ECL.
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
|
||||
|
||||
private final ECLPS ps;
|
||||
private Component visualComponent;
|
||||
private Object visualComponent;
|
||||
private Color[] colors;
|
||||
private final List<ECLPSGraphicsListener> listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
@@ -20,11 +20,11 @@ public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisualComponent(Component comp) {
|
||||
public void setVisualComponent(Object comp) {
|
||||
this.visualComponent = comp;
|
||||
}
|
||||
|
||||
public Component getVisualComponent() {
|
||||
public Object getVisualComponent() {
|
||||
return visualComponent;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ public class ECLConnection {
|
||||
private String luName;
|
||||
private String workstationId = "";
|
||||
private boolean ssl = false;
|
||||
private boolean autoSysUnlock = true;
|
||||
private boolean keepAlive = true;
|
||||
private int keepAliveTimeout = 120;
|
||||
private String keepAliveType = "NOP";
|
||||
private boolean autoReconnect = false;
|
||||
private int maxRetry = 5;
|
||||
private boolean contentionResolution = false;
|
||||
private boolean luluSession = false;
|
||||
private boolean isNegCR = false;
|
||||
@@ -71,12 +77,49 @@ public class ECLConnection {
|
||||
if (props != null) {
|
||||
this.properties.putAll(props);
|
||||
convertData(this.properties);
|
||||
String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
|
||||
if (asu != null) {
|
||||
this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu);
|
||||
}
|
||||
String ka = this.properties.getProperty(ECLSession.SESSION_KEEPALIVE);
|
||||
if (ka != null) {
|
||||
this.keepAlive = "true".equalsIgnoreCase(ka) || "1".equals(ka);
|
||||
}
|
||||
String kat = this.properties.getProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT);
|
||||
if (kat != null) {
|
||||
try { this.keepAliveTimeout = Integer.parseInt(kat.trim()); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
String katyp = this.properties.getProperty(ECLSession.KEY_KEEPALIVE_TYPE);
|
||||
if (katyp != null) {
|
||||
this.keepAliveType = katyp;
|
||||
}
|
||||
String ar = this.properties.getProperty(ECLSession.SESSION_AUTORECONNECT);
|
||||
if (ar != null) {
|
||||
this.autoReconnect = "true".equalsIgnoreCase(ar) || "1".equals(ar);
|
||||
}
|
||||
String mr = this.properties.getProperty(ECLSession.SESSION_RECONNECT_RETRIES);
|
||||
if (mr != null) {
|
||||
try { this.maxRetry = Integer.parseInt(mr.trim()); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ECLConnection(ECLSession session, Telnet3270Client client) {
|
||||
this.session = session;
|
||||
this.client = client;
|
||||
if (client != null && client.getConfig() != null) {
|
||||
this.autoSysUnlock = client.getConfig().isAutoSysUnlock();
|
||||
this.keepAlive = client.getConfig().isKeepAliveEnabled();
|
||||
this.keepAliveTimeout = client.getConfig().getKeepAliveIntervalSeconds();
|
||||
this.keepAliveType = client.getConfig().getKeepAliveType();
|
||||
this.autoReconnect = client.getConfig().isAutoReconnect();
|
||||
this.maxRetry = client.getConfig().getReconnectMaxRetries();
|
||||
} else if (session != null) {
|
||||
this.autoSysUnlock = session.isAutoSysUnlock();
|
||||
this.keepAlive = session.isKeepAlive();
|
||||
this.keepAliveTimeout = session.getKeepAliveTimeout();
|
||||
this.autoReconnect = session.isAutoReconnect();
|
||||
}
|
||||
|
||||
if (client != null) {
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@@ -114,6 +157,13 @@ public class ECLConnection {
|
||||
state, state, "TN3270E Negotiated", deviceType, deviceName);
|
||||
notifyCommEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTN3270EFunctionsNegotiated(boolean[] functions) {
|
||||
if (functions != null && functions.length > haus.nightmare.lib3270j.protocol.TN3270EConstants.FUNC_CONTENTION_RESOLUTION) {
|
||||
setContentionResolution(functions[haus.nightmare.lib3270j.protocol.TN3270EConstants.FUNC_CONTENTION_RESOLUTION]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -398,17 +448,138 @@ public class ECLConnection {
|
||||
}
|
||||
public void setSSL(boolean ssl) { SetSSL(ssl); }
|
||||
|
||||
public void setContentionResolution(boolean bl) { this.contentionResolution = bl; }
|
||||
public void setContentionResolution(boolean bl) {
|
||||
this.contentionResolution = bl;
|
||||
if (client != null && client.getTelnetFSM() != null) {
|
||||
client.getTelnetFSM().setContentionResolutionNegotiated(bl);
|
||||
}
|
||||
}
|
||||
public void SetContentionResolution(boolean bl) { setContentionResolution(bl); }
|
||||
public boolean getContentionResolution() { return contentionResolution; }
|
||||
public boolean isContentionResolution() { return contentionResolution; }
|
||||
public boolean getContentionResolution() {
|
||||
if (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().isTn3270eNegotiated()) {
|
||||
return client.getTelnetFSM().isContentionResolutionNegotiated();
|
||||
}
|
||||
return contentionResolution;
|
||||
}
|
||||
public boolean isContentionResolution() { return getContentionResolution(); }
|
||||
public boolean GetContentionResolution() { return getContentionResolution(); }
|
||||
public boolean IsContentionResolution() { return getContentionResolution(); }
|
||||
|
||||
public boolean isAutoSysUnlock() {
|
||||
if (client != null && client.getConfig() != null) {
|
||||
return client.getConfig().isAutoSysUnlock();
|
||||
}
|
||||
String s = properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
|
||||
if (s != null) {
|
||||
return "true".equalsIgnoreCase(s) || "1".equals(s);
|
||||
}
|
||||
return autoSysUnlock;
|
||||
}
|
||||
public boolean IsAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
public boolean getAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
public boolean GetAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
|
||||
public void setAutoSysUnlock(boolean unlock) {
|
||||
this.autoSysUnlock = unlock;
|
||||
properties.setProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK, String.valueOf(unlock));
|
||||
if (client != null) {
|
||||
client.setAutoSysUnlock(unlock);
|
||||
}
|
||||
}
|
||||
public void SetAutoSysUnlock(boolean unlock) { setAutoSysUnlock(unlock); }
|
||||
|
||||
public boolean isKeepAlive() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().isKeepAliveEnabled();
|
||||
return keepAlive;
|
||||
}
|
||||
public boolean getKeepAlive() { return isKeepAlive(); }
|
||||
public boolean IsKeepAlive() { return isKeepAlive(); }
|
||||
public boolean GetKeepAlive() { return isKeepAlive(); }
|
||||
public void setKeepAlive(boolean ka) {
|
||||
this.keepAlive = ka;
|
||||
this.properties.setProperty(ECLSession.SESSION_KEEPALIVE, String.valueOf(ka));
|
||||
if (client != null) {
|
||||
client.setKeepAliveEnabled(ka);
|
||||
}
|
||||
}
|
||||
public void SetKeepAlive(boolean ka) { setKeepAlive(ka); }
|
||||
|
||||
public int getKeepAliveTimeout() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().getKeepAliveIntervalSeconds();
|
||||
return keepAliveTimeout;
|
||||
}
|
||||
public int GetKeepAliveTimeout() { return getKeepAliveTimeout(); }
|
||||
public void setKeepAliveTimeout(int timeout) {
|
||||
this.keepAliveTimeout = timeout;
|
||||
this.properties.setProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT, String.valueOf(timeout));
|
||||
if (client != null) {
|
||||
client.setKeepAliveIntervalSeconds(timeout);
|
||||
}
|
||||
}
|
||||
public void SetKeepAliveTimeout(int timeout) { setKeepAliveTimeout(timeout); }
|
||||
|
||||
public String getKeepAliveType() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().getKeepAliveType();
|
||||
return keepAliveType;
|
||||
}
|
||||
public String GetKeepAliveType() { return getKeepAliveType(); }
|
||||
public void setKeepAliveType(String type) {
|
||||
this.keepAliveType = type;
|
||||
this.properties.setProperty(ECLSession.KEY_KEEPALIVE_TYPE, type != null ? type : "");
|
||||
if (client != null && client.getConfig() != null) {
|
||||
client.getConfig().setKeepAliveType(type);
|
||||
}
|
||||
}
|
||||
public void SetKeepAliveType(String type) { setKeepAliveType(type); }
|
||||
|
||||
public boolean isAutoReconnect() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().isAutoReconnect();
|
||||
return autoReconnect;
|
||||
}
|
||||
public boolean getAutoReconnect() { return isAutoReconnect(); }
|
||||
public boolean IsAutoReconnect() { return isAutoReconnect(); }
|
||||
public boolean GetAutoReconnect() { return isAutoReconnect(); }
|
||||
public void setAutoReconnect(boolean ar) {
|
||||
this.autoReconnect = ar;
|
||||
this.properties.setProperty(ECLSession.SESSION_AUTORECONNECT, String.valueOf(ar));
|
||||
if (client != null) {
|
||||
client.setAutoReconnect(ar);
|
||||
}
|
||||
}
|
||||
public void SetAutoReconnect(boolean ar) { setAutoReconnect(ar); }
|
||||
|
||||
public int getMaxRetry() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().getReconnectMaxRetries();
|
||||
return maxRetry;
|
||||
}
|
||||
public int GetMaxRetry() { return getMaxRetry(); }
|
||||
public void setMaxRetry(int retries) {
|
||||
this.maxRetry = retries;
|
||||
this.properties.setProperty(ECLSession.SESSION_RECONNECT_RETRIES, String.valueOf(retries));
|
||||
if (client != null && client.getConfig() != null) {
|
||||
client.getConfig().setReconnectMaxRetries(retries);
|
||||
}
|
||||
}
|
||||
public void SetMaxRetry(int retries) { setMaxRetry(retries); }
|
||||
public int getReconnectMaxRetries() { return getMaxRetry(); }
|
||||
public void setReconnectMaxRetries(int retries) { setMaxRetry(retries); }
|
||||
|
||||
public void set_LULU_Session(boolean bl) { this.luluSession = bl; }
|
||||
public boolean is_LULU_Session() { return luluSession; }
|
||||
public boolean get_LULU_Session() { return luluSession; }
|
||||
|
||||
public boolean isNegotiateCResolution() { return isNegCR; }
|
||||
public void setNegotiatedCResolution(boolean bl) { this.isNegCR = bl; }
|
||||
public boolean isNegotiateCResolution() {
|
||||
if (client != null && client.getTelnetFSM() != null) {
|
||||
return client.getTelnetFSM().isNegotiateContentionResolution();
|
||||
}
|
||||
return isNegCR;
|
||||
}
|
||||
public void setNegotiatedCResolution(boolean bl) {
|
||||
this.isNegCR = bl;
|
||||
if (client != null && client.getTelnetFSM() != null) {
|
||||
client.getTelnetFSM().setNegotiateContentionResolution(bl);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBIND7FArchitectureViolation() { return isBIND7FArchitectureViolation; }
|
||||
public void setBIND7FArchitectureViolation(boolean bl) { this.isBIND7FArchitectureViolation = bl; }
|
||||
|
||||
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
@@ -178,7 +181,20 @@ public class ECLOIA implements ECLConstants {
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void notifyOIAChanged() {
|
||||
private final ReentrantLock oiaLock = new ReentrantLock();
|
||||
private final Condition oiaCondition = oiaLock.newCondition();
|
||||
|
||||
public void signalWaiters() {
|
||||
oiaLock.lock();
|
||||
try {
|
||||
oiaCondition.signalAll();
|
||||
} finally {
|
||||
oiaLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void notifyOIAChanged() {
|
||||
signalWaiters();
|
||||
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
|
||||
getAlphanumericType(), isInsertMode(), getStatusString());
|
||||
for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) {
|
||||
@@ -413,19 +429,28 @@ public class ECLOIA implements ECLConstants {
|
||||
* @return true if keyboard unlocked, false if timeout occurred.
|
||||
*/
|
||||
public boolean waitForInput(long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
|
||||
return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
|
||||
return true;
|
||||
}
|
||||
oiaLock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (getInputInhibited() != INHIBIT_NOT_INHIBITED) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = oiaCondition.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
oiaLock.unlock();
|
||||
}
|
||||
return getInputInhibited() == INHIBIT_NOT_INHIBITED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -458,20 +483,26 @@ public class ECLOIA implements ECLConstants {
|
||||
* Block until any OIA transition occurs.
|
||||
*/
|
||||
public boolean waitForTransition(long timeoutMs) {
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
int initialInhibit = getInputInhibited();
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (getInputInhibited() != initialInhibit) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
oiaLock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (getInputInhibited() == initialInhibit) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = oiaCondition.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
oiaLock.unlock();
|
||||
}
|
||||
return getInputInhibited() != initialInhibit;
|
||||
}
|
||||
|
||||
public boolean WaitForTransition(long timeoutMs) {
|
||||
@@ -484,6 +515,7 @@ public class ECLOIA implements ECLConstants {
|
||||
if (isNumeric()) s |= STATE_NUMFIELD;
|
||||
if (screen != null && screen.isEntryAssistDOCmode()) s |= STATE_DOC_MODE;
|
||||
if (screen != null && screen.isEntryAssistWordWrap()) s |= STATE_WORDWRAP;
|
||||
if (isApl()) s |= STATE_APL;
|
||||
if (isXSystem()) s |= STATE_SYS_LOCK;
|
||||
if (isXComm()) s |= STATE_COMM_CHECK;
|
||||
return s;
|
||||
@@ -494,6 +526,21 @@ public class ECLOIA implements ECLConstants {
|
||||
public int getStatusFlags() { return GetStatusFlags(); }
|
||||
public long getStatusFlagsEx() { return GetStatusFlagsEx(); }
|
||||
|
||||
public boolean isApl() {
|
||||
return inputProcessor != null && inputProcessor.isAplKeyboardMode();
|
||||
}
|
||||
public boolean IsApl() { return isApl(); }
|
||||
|
||||
public boolean isDocMode() {
|
||||
return screen != null && screen.isEntryAssistDOCmode();
|
||||
}
|
||||
public boolean IsDocMode() { return isDocMode(); }
|
||||
|
||||
public boolean isWordWrap() {
|
||||
return screen != null && screen.isEntryAssistWordWrap();
|
||||
}
|
||||
public boolean IsWordWrap() { return isWordWrap(); }
|
||||
|
||||
public synchronized void setBitmaskState(long flag, boolean on) {
|
||||
this.previousState = this.state;
|
||||
if (on) {
|
||||
|
||||
@@ -2,8 +2,12 @@ package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
@@ -37,6 +41,31 @@ public class ECLPS implements ECLConstants {
|
||||
private final java.util.Map<ECLPSListener, ECLScreenDesc> descriptorListeners = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private final java.util.Map<ECLPSListener, Integer> listenerEventTypes = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
// Synchronization primitives for reactive automation waits (Phase 12)
|
||||
private final ReentrantLock fallbackLock = new ReentrantLock();
|
||||
private final Condition fallbackCondition = fallbackLock.newCondition();
|
||||
|
||||
public ReentrantLock getSyncLock() {
|
||||
return (screen != null) ? screen.getSyncLock() : fallbackLock;
|
||||
}
|
||||
|
||||
public Condition getSyncCondition() {
|
||||
return (screen != null) ? screen.getSyncCondition() : fallbackCondition;
|
||||
}
|
||||
|
||||
public void signalWaiters() {
|
||||
if (screen != null) {
|
||||
screen.signalWaiters();
|
||||
} else {
|
||||
fallbackLock.lock();
|
||||
try {
|
||||
fallbackCondition.signalAll();
|
||||
} finally {
|
||||
fallbackLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
|
||||
this.screen = screen;
|
||||
this.inputProcessor = inputProcessor;
|
||||
@@ -46,6 +75,27 @@ public class ECLPS implements ECLConstants {
|
||||
this.bidiServices = new DefaultPSBIDIServices(this);
|
||||
this.hindiServices = new DefaultPSHindiServices(this);
|
||||
this.thaiServices = new DefaultPSTHAIServices(this);
|
||||
|
||||
if (this.screen != null) {
|
||||
this.screen.addUpdateListener(new ScreenUpdateListener() {
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
signalWaiters();
|
||||
}
|
||||
@Override
|
||||
public void onCursorMoved(int oldAddress, int newAddress) {
|
||||
signalWaiters();
|
||||
}
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
signalWaiters();
|
||||
}
|
||||
@Override
|
||||
public void onKeyboardUnlocked() {
|
||||
signalWaiters();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ECLPS(ECLSession session) {
|
||||
@@ -494,11 +544,71 @@ public class ECLPS implements ECLConstants {
|
||||
return copyString(sRow, sCol, eRow, eCol);
|
||||
}
|
||||
|
||||
private boolean enablePasteFromExcel = true;
|
||||
private boolean pasteStopAtProtectedLine = false;
|
||||
|
||||
public boolean isEnablePasteFromExcel() {
|
||||
if (session != null && session.getProperties() != null) {
|
||||
String p = session.getProperties().getProperty(ECLSession.ENABLE_PASTE_FROM_EXCEL);
|
||||
if (p != null) return Boolean.parseBoolean(p);
|
||||
}
|
||||
return enablePasteFromExcel;
|
||||
}
|
||||
|
||||
public boolean IsEnablePasteFromExcel() { return isEnablePasteFromExcel(); }
|
||||
|
||||
public void setEnablePasteFromExcel(boolean val) {
|
||||
this.enablePasteFromExcel = val;
|
||||
if (session != null && session.getProperties() != null) {
|
||||
session.getProperties().setProperty(ECLSession.ENABLE_PASTE_FROM_EXCEL, String.valueOf(val));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetEnablePasteFromExcel(boolean val) { setEnablePasteFromExcel(val); }
|
||||
|
||||
public boolean isPasteStopAtProtectedLine() {
|
||||
if (session != null && session.getProperties() != null) {
|
||||
String p = session.getProperties().getProperty(ECLSession.PASTE_STOP_AT_PROTECTED_LINE);
|
||||
if (p != null) return Boolean.parseBoolean(p);
|
||||
}
|
||||
return pasteStopAtProtectedLine;
|
||||
}
|
||||
|
||||
public boolean IsPasteStopAtProtectedLine() { return isPasteStopAtProtectedLine(); }
|
||||
|
||||
public void setPasteStopAtProtectedLine(boolean val) {
|
||||
this.pasteStopAtProtectedLine = val;
|
||||
if (session != null && session.getProperties() != null) {
|
||||
session.getProperties().setProperty(ECLSession.PASTE_STOP_AT_PROTECTED_LINE, String.valueOf(val));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPasteStopAtProtectedLine(boolean val) { setPasteStopAtProtectedLine(val); }
|
||||
|
||||
public synchronized int pasteFromExcel(String text, int row, int col) {
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
if (row >= 0 && col >= 0) {
|
||||
setCursorPos(row, col);
|
||||
}
|
||||
if (inputProcessor != null) {
|
||||
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int PasteFromExcel(String text, int row, int col) {
|
||||
return pasteFromExcel(text, row, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste a multi-line rectangular block of text starting at (row, col).
|
||||
*/
|
||||
public synchronized int pasteString(String text, int row, int col) {
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine())) {
|
||||
setCursorPos(row, col);
|
||||
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine());
|
||||
}
|
||||
int rows = screen.getRows();
|
||||
int cols = screen.getCols();
|
||||
if (rows <= 0 || cols <= 0) return 0;
|
||||
@@ -532,7 +642,33 @@ public class ECLPS implements ECLConstants {
|
||||
}
|
||||
|
||||
public int pasteRectangular(String text, int row, int col) {
|
||||
return pasteString(text, row, col);
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
int rows = screen.getRows();
|
||||
int cols = screen.getCols();
|
||||
if (rows <= 0 || cols <= 0) return 0;
|
||||
|
||||
String[] lines = text.split("\r?\n");
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
int targetRow = (row + i) % rows;
|
||||
String line = lines[i];
|
||||
for (int c = 0; c < line.length() && (col + c) < cols; c++) {
|
||||
int pos = targetRow * cols + (col + c);
|
||||
if (screen.isFormatted()) {
|
||||
byte fa = screen.getFieldAttributeAt(pos);
|
||||
if (faIsProtected(fa & 0xFF) || screen.getCell(pos).isFieldAttribute()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
setCursorPos(pos);
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.typeCharacter(line.charAt(c));
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public int PasteRectangular(String text, int row, int col) {
|
||||
@@ -848,6 +984,20 @@ public class ECLPS implements ECLConstants {
|
||||
UnregisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void dispatchEvent(ECLPSEvent event) {
|
||||
notifyPSEvent(event);
|
||||
}
|
||||
|
||||
public void notifyKeyUnlocked() {
|
||||
int r = (screen != null) ? screen.getRows() : 0;
|
||||
int c = (screen != null) ? screen.getCols() : 0;
|
||||
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.EVENT_KEY_UNLOCKED, HOST_EVENTS, 0, 0,
|
||||
Math.max(0, r - 1), Math.max(0, c - 1), cur, cur, r, c, false, cursorVisible, ring, false, null));
|
||||
}
|
||||
|
||||
public void notifyPSEvent(ECLPSEvent event) {
|
||||
for (ECLPSListener l : psListeners) {
|
||||
ECLScreenDesc desc = descriptorListeners.get(l);
|
||||
@@ -899,6 +1049,7 @@ public class ECLPS implements ECLConstants {
|
||||
int c = (screen != null) ? screen.getCols() : 0;
|
||||
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
ECLPSEvent evt = new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, type, startRow, startCol, endRow, endCol,
|
||||
cur, cur, r, c, full, cursorVisible, ring, startPrinter, null);
|
||||
notifyPSEvent(evt);
|
||||
@@ -910,12 +1061,14 @@ public class ECLPS implements ECLConstants {
|
||||
int row = (c > 0) ? newAddress / c : 0;
|
||||
int col = (c > 0) ? newAddress % c : 0;
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, USER_EVENTS, row, col, row, col,
|
||||
oldAddress, newAddress, r, c, false, cursorVisible, ring, false, null));
|
||||
}
|
||||
|
||||
public void notifyAlarm() {
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM, HOST_EVENTS, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, false, cursorVisible, ring, false, null));
|
||||
}
|
||||
@@ -923,6 +1076,7 @@ public class ECLPS implements ECLConstants {
|
||||
public void notifyScreenResized(int rows, int cols) {
|
||||
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, HOST_EVENTS, 0, 0, rows - 1, cols - 1,
|
||||
cur, cur, rows, cols, true, cursorVisible, ring, false, null));
|
||||
}
|
||||
@@ -1067,20 +1221,30 @@ public class ECLPS implements ECLConstants {
|
||||
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
if (desc == null) return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
long start = System.currentTimeMillis();
|
||||
ECLOIA oia = (session != null) ? session.GetOIA() : null;
|
||||
while (System.currentTimeMillis() - start < limit) {
|
||||
if (desc.Matches(this, oia)) {
|
||||
return true;
|
||||
if (desc.Matches(this, oia)) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (!desc.Matches(this, oia)) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return desc.Matches(this, oia);
|
||||
}
|
||||
|
||||
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
@@ -1097,20 +1261,30 @@ public class ECLPS implements ECLConstants {
|
||||
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
if (desc == null) return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
long start = System.currentTimeMillis();
|
||||
ECLOIA oia = (session != null) ? session.GetOIA() : null;
|
||||
while (System.currentTimeMillis() - start < limit) {
|
||||
if (!desc.Matches(this, oia)) {
|
||||
return true;
|
||||
if (!desc.Matches(this, oia)) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (desc.Matches(this, oia)) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return !desc.Matches(this, oia);
|
||||
}
|
||||
|
||||
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
@@ -1121,69 +1295,178 @@ public class ECLPS implements ECLConstants {
|
||||
* Block until the specified text appears anywhere on the presentation space.
|
||||
*/
|
||||
public boolean waitForScreen(String text, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (searchString(text) >= 0) {
|
||||
return true;
|
||||
if (text == null || text.isEmpty()) return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
if (searchString(text) >= 0) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (searchString(text) < 0) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return searchString(text) >= 0;
|
||||
}
|
||||
|
||||
public boolean WaitForScreen(String text, long timeoutMs) {
|
||||
return waitForScreen(text, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text) {
|
||||
return waitForScreen(text, -1L);
|
||||
}
|
||||
|
||||
public boolean WaitForString(String text) {
|
||||
return waitForScreen(text, -1L);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, long timeoutMs) {
|
||||
return waitForScreen(text, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean WaitForString(String text, long timeoutMs) {
|
||||
return waitForScreen(text, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the specified text appears at the given (row, col) coordinate.
|
||||
*/
|
||||
public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
String onScreen = getString(row, col, text.length());
|
||||
if (text.equals(onScreen)) {
|
||||
return true;
|
||||
if (text == null) return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
if (text.equals(getString(row, col, text.length()))) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (!text.equals(getString(row, col, text.length()))) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return text.equals(getString(row, col, text.length()));
|
||||
}
|
||||
|
||||
public boolean WaitForScreen(String text, int row, int col, long timeoutMs) {
|
||||
return waitForScreen(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, int row, int col, long timeoutMs) {
|
||||
return waitForScreen(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
|
||||
return waitForScreen(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the cursor moves to (row, col).
|
||||
*/
|
||||
public boolean waitForCursor(int row, int col, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (getCursorRow() == row && getCursorCol() == col) {
|
||||
return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
if (getCursorRow() == row && getCursorCol() == col) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (getCursorRow() != row || getCursorCol() != col) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return getCursorRow() == row && getCursorCol() == col;
|
||||
}
|
||||
|
||||
public boolean WaitForCursor(int row, int col, long timeoutMs) {
|
||||
return waitForCursor(row, col, timeoutMs);
|
||||
}
|
||||
|
||||
protected boolean locked_SYSLOCK = false;
|
||||
protected boolean locked_TWAIT = false;
|
||||
|
||||
public void lockKeyboard() {
|
||||
lockKeyboard(8);
|
||||
}
|
||||
|
||||
public void lockKeyboard(int reason) {
|
||||
if (reason == 7) locked_TWAIT = true;
|
||||
if (reason == 8) {
|
||||
locked_SYSLOCK = true;
|
||||
if (session != null && session.getOIA() != null) {
|
||||
session.getOIA().setDoNotEnter(8, 0);
|
||||
}
|
||||
}
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void unlockKeyboard() {
|
||||
unlockKeyboard(8);
|
||||
}
|
||||
|
||||
public void unlockKeyboard(int reason) {
|
||||
if (reason == 7) locked_TWAIT = false;
|
||||
if (reason == 8) {
|
||||
locked_SYSLOCK = false;
|
||||
if (session != null && session.getOIA() != null) {
|
||||
session.getOIA().clearDoNotEnter();
|
||||
}
|
||||
}
|
||||
if (!locked_TWAIT && !locked_SYSLOCK) {
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean islocked_TWAIT() {
|
||||
return locked_TWAIT;
|
||||
}
|
||||
|
||||
public boolean islocked_SYSLOCK() {
|
||||
if (locked_SYSLOCK) return true;
|
||||
if (session != null && session.getOIA() != null && session.getOIA().isXSystem()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public class ECLPSEvent extends EventObject {
|
||||
public static final int EVENT_ALARM = PS_ALARM;
|
||||
public static final int EVENT_RESIZE = PS_RESIZE;
|
||||
public static final int EVENT_CLOSE = PS_CLOSE;
|
||||
public static final int EVENT_KEY_UNLOCKED = PS_UPDATE; // HoD event type 1 for keyboard unlock / update
|
||||
|
||||
private final int eventType;
|
||||
private final int type;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.awt.Image;
|
||||
import java.awt.Rectangle;
|
||||
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
|
||||
/**
|
||||
* Conforms to IBM Host On-Demand ECLPSGraphicsEvent.
|
||||
* Decoupled from java.awt.
|
||||
*/
|
||||
public class ECLPSGraphicsEvent {
|
||||
public static final int GRAPHICS_CURSOR_ON = 1;
|
||||
@@ -14,7 +15,7 @@ public class ECLPSGraphicsEvent {
|
||||
public static final int GRAPHICS_UPDATED = 5;
|
||||
|
||||
private int id;
|
||||
private Image image;
|
||||
private Object image;
|
||||
private Rectangle rect;
|
||||
private ECLPS source;
|
||||
|
||||
@@ -23,13 +24,13 @@ public class ECLPSGraphicsEvent {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public ECLPSGraphicsEvent(ECLPS source, int id, Image image) {
|
||||
public ECLPSGraphicsEvent(ECLPS source, int id, Object image) {
|
||||
this.source = source;
|
||||
this.id = id;
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public ECLPSGraphicsEvent(ECLPS source, int id, Image image, Rectangle rectangle) {
|
||||
public ECLPSGraphicsEvent(ECLPS source, int id, Object image, Rectangle rectangle) {
|
||||
this.source = source;
|
||||
this.id = id;
|
||||
this.image = image;
|
||||
@@ -46,9 +47,13 @@ public class ECLPSGraphicsEvent {
|
||||
public int getID() { return this.id; }
|
||||
public int GetID() { return this.id; }
|
||||
|
||||
public void setImage(Image image) { this.image = image; }
|
||||
public Image getImage() { return this.image; }
|
||||
public Image GetImage() { return this.image; }
|
||||
public void setImage(Object image) { this.image = image; }
|
||||
public Object getImage() { return this.image; }
|
||||
public Object GetImage() { return this.image; }
|
||||
|
||||
public PixelBuffer getPixelBuffer() {
|
||||
return (this.image instanceof PixelBuffer) ? (PixelBuffer) this.image : null;
|
||||
}
|
||||
|
||||
public void setRectangle(Rectangle rect) { this.rect = rect; }
|
||||
public Rectangle getRectangle() { return this.rect; }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import haus.nightmare.lib3270j.graphics.Color;
|
||||
|
||||
/**
|
||||
* Presentation Space graphics services interface conforming to IBM Host On-Demand ECL.
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public interface ECLPSGraphicsServices {
|
||||
void setVisualComponent(Component comp);
|
||||
void setVisualComponent(Object comp);
|
||||
void setGraphicColor(Color[] colors, boolean b);
|
||||
void mousePressed(int x, int y, int button);
|
||||
void addGraphicsListener(ECLPSGraphicsListener listener);
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ECLSession {
|
||||
|
||||
private static final Logger log = Logger.getLogger(ECLSession.class.getName());
|
||||
|
||||
// Standard IBM HoD Session Property Keys
|
||||
// Standard Session Property Keys
|
||||
public static final String SESSION_HOST = "SESSION_HOST";
|
||||
public static final String SESSION_PORT = "SESSION_PORT";
|
||||
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
|
||||
@@ -29,6 +29,24 @@ public class ECLSession {
|
||||
public static final String SESSION_TN3270E = "SESSION_TN3270E";
|
||||
public static final String SESSION_WIN_TITLE = "SESSION_WIN_TITLE";
|
||||
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
|
||||
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
|
||||
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_AUTORECONNECT = "SESSION_AUTORECONNECT";
|
||||
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
|
||||
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 PASTE_FIELD_WRAP = "pasteFieldWrap";
|
||||
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
|
||||
public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode";
|
||||
public static final String ENTRYASSIST_DOCWORDWRAP = "EntryAssist_DOCwordWrap";
|
||||
public static final String ENTRYASSIST_STARTCOL = "EntryAssist_startCol";
|
||||
public static final String ENTRYASSIST_ENDCOL = "EntryAssist_endCol";
|
||||
public static final String ENTRYASSIST_BELL = "EntryAssist_bell";
|
||||
public static final String ENTRYASSIST_BELLCOL = "EntryAssist_bellCol";
|
||||
public static final String ENTRYASSIST_TABSTOPS = "EntryAssist_tabstops";
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final ECLConnection connection;
|
||||
@@ -98,6 +116,9 @@ public class ECLSession {
|
||||
String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true");
|
||||
config.setTn3270eEnabled("true".equalsIgnoreCase(tn3270eStr) || "yes".equalsIgnoreCase(tn3270eStr) || "1".equals(tn3270eStr));
|
||||
|
||||
String autoSysStr = getProp(props, SESSION_AUTO_SYS_UNLOCK, "autoSysUnlock", "AutoSysUnlock", "true");
|
||||
config.setAutoSysUnlock("true".equalsIgnoreCase(autoSysStr) || "yes".equalsIgnoreCase(autoSysStr) || "1".equals(autoSysStr));
|
||||
|
||||
String certUrl = getProp(props, "certificateURL", "CERTIFICATE_URL", "certificate_url", null);
|
||||
if (certUrl != null) config.setKeyStorePath(certUrl);
|
||||
String certPwd = getProp(props, "certificatePassword", "CERTIFICATE_PASSWORD", "certificate_password", null);
|
||||
@@ -118,6 +139,25 @@ public class ECLSession {
|
||||
config.setEnabledProtocols(tlsVer);
|
||||
}
|
||||
|
||||
String keepAliveStr = getProp(props, SESSION_KEEPALIVE, "keepAlive", "keepalive", "true");
|
||||
config.setKeepAliveEnabled("true".equalsIgnoreCase(keepAliveStr) || "yes".equalsIgnoreCase(keepAliveStr) || "1".equals(keepAliveStr));
|
||||
|
||||
String kaTimeoutStr = getProp(props, KEY_KEEPALIVE_TIMEOUT, "keepAliveTimeout", "keepalivetimeout", null);
|
||||
if (kaTimeoutStr != null) {
|
||||
try { config.setKeepAliveIntervalSeconds(Integer.parseInt(kaTimeoutStr.trim())); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
String kaTypeStr = getProp(props, KEY_KEEPALIVE_TYPE, "keepAliveType", "keepalivetype", null);
|
||||
if (kaTypeStr != null) config.setKeepAliveType(kaTypeStr);
|
||||
|
||||
String autoReconnectStr = getProp(props, SESSION_AUTORECONNECT, "autoReconnect", "autoreconnect", "false");
|
||||
config.setAutoReconnect("true".equalsIgnoreCase(autoReconnectStr) || "yes".equalsIgnoreCase(autoReconnectStr) || "1".equals(autoReconnectStr));
|
||||
|
||||
String retriesStr = getProp(props, SESSION_RECONNECT_RETRIES, "reconnectMaxRetries", "reconnectRetries", null);
|
||||
if (retriesStr != null) {
|
||||
try { config.setReconnectMaxRetries(Integer.parseInt(retriesStr.trim())); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -161,6 +201,7 @@ public class ECLSession {
|
||||
properties.setProperty(SESSION_SSL, String.valueOf(cfg.isUseTls()));
|
||||
if (cfg.getLuName() != null) properties.setProperty(SESSION_LU_NAME, cfg.getLuName());
|
||||
properties.setProperty(SESSION_TN3270E, String.valueOf(cfg.isTn3270eEnabled()));
|
||||
properties.setProperty(SESSION_AUTO_SYS_UNLOCK, String.valueOf(cfg.isAutoSysUnlock()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +412,42 @@ public class ECLSession {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isKeepAlive() {
|
||||
return (client != null) ? client.isKeepAliveEnabled() : true;
|
||||
}
|
||||
public boolean IsKeepAlive() { return isKeepAlive(); }
|
||||
public void setKeepAlive(boolean keepAlive) {
|
||||
this.properties.setProperty(SESSION_KEEPALIVE, String.valueOf(keepAlive));
|
||||
if (client != null) {
|
||||
client.setKeepAliveEnabled(keepAlive);
|
||||
}
|
||||
}
|
||||
public void SetKeepAlive(boolean keepAlive) { setKeepAlive(keepAlive); }
|
||||
|
||||
public int getKeepAliveTimeout() {
|
||||
return (client != null) ? client.getKeepAliveIntervalSeconds() : 120;
|
||||
}
|
||||
public int GetKeepAliveTimeout() { return getKeepAliveTimeout(); }
|
||||
public void setKeepAliveTimeout(int timeout) {
|
||||
this.properties.setProperty(KEY_KEEPALIVE_TIMEOUT, String.valueOf(timeout));
|
||||
if (client != null) {
|
||||
client.setKeepAliveIntervalSeconds(timeout);
|
||||
}
|
||||
}
|
||||
public void SetKeepAliveTimeout(int timeout) { setKeepAliveTimeout(timeout); }
|
||||
|
||||
public boolean isAutoReconnect() {
|
||||
return (client != null) ? client.isAutoReconnect() : false;
|
||||
}
|
||||
public boolean IsAutoReconnect() { return isAutoReconnect(); }
|
||||
public void setAutoReconnect(boolean autoReconnect) {
|
||||
this.properties.setProperty(SESSION_AUTORECONNECT, String.valueOf(autoReconnect));
|
||||
if (client != null) {
|
||||
client.setAutoReconnect(autoReconnect);
|
||||
}
|
||||
}
|
||||
public void SetAutoReconnect(boolean autoReconnect) { setAutoReconnect(autoReconnect); }
|
||||
|
||||
// ========== Automation Keystrokes & Waits ==========
|
||||
|
||||
/**
|
||||
@@ -455,6 +532,41 @@ public class ECLSession {
|
||||
dispose();
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() {
|
||||
if (client != null && client.getConfig() != null) {
|
||||
return client.getConfig().isAutoSysUnlock();
|
||||
}
|
||||
String s = properties.getProperty(SESSION_AUTO_SYS_UNLOCK);
|
||||
return s != null ? Boolean.parseBoolean(s) : true;
|
||||
}
|
||||
public boolean getAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
public boolean IsAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
public boolean GetAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
|
||||
public void setAutoSysUnlock(boolean unlock) {
|
||||
properties.setProperty(SESSION_AUTO_SYS_UNLOCK, String.valueOf(unlock));
|
||||
if (client != null) {
|
||||
client.setAutoSysUnlock(unlock);
|
||||
}
|
||||
}
|
||||
public void SetAutoSysUnlock(boolean unlock) { setAutoSysUnlock(unlock); }
|
||||
|
||||
public boolean getContentionResolution() {
|
||||
if (connection != null) return connection.getContentionResolution();
|
||||
if (client != null) return client.isContentionResolution();
|
||||
return false;
|
||||
}
|
||||
public boolean isContentionResolution() { return getContentionResolution(); }
|
||||
public boolean GetContentionResolution() { return getContentionResolution(); }
|
||||
public boolean IsContentionResolution() { return getContentionResolution(); }
|
||||
|
||||
public void setContentionResolution(boolean cr) {
|
||||
if (connection != null) {
|
||||
connection.setContentionResolution(cr);
|
||||
}
|
||||
}
|
||||
public void SetContentionResolution(boolean cr) { setContentionResolution(cr); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ECLSession[host=%s, port=%d, connected=%b, state=%s]",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Attention Identifier (AID) representation for 3270 presentation space.
|
||||
*/
|
||||
public final class AID implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final byte code;
|
||||
private final String name;
|
||||
|
||||
public static final AID clear = new AID((byte) 0x6D, "CLEAR");
|
||||
public static final AID enter = new AID((byte) 0x7D, "ENTER");
|
||||
public static final AID PA1 = new AID((byte) 0x6C, "PA1");
|
||||
public static final AID PA2 = new AID((byte) 0x6E, "PA2");
|
||||
public static final AID PA3 = new AID((byte) 0x6B, "PA3");
|
||||
|
||||
public static final AID PF1 = new AID((byte) 0xF1, "PF1");
|
||||
public static final AID PF2 = new AID((byte) 0xF2, "PF2");
|
||||
public static final AID PF3 = new AID((byte) 0xF3, "PF3");
|
||||
public static final AID PF4 = new AID((byte) 0xF4, "PF4");
|
||||
public static final AID PF5 = new AID((byte) 0xF5, "PF5");
|
||||
public static final AID PF6 = new AID((byte) 0xF6, "PF6");
|
||||
public static final AID PF7 = new AID((byte) 0xF7, "PF7");
|
||||
public static final AID PF8 = new AID((byte) 0xF8, "PF8");
|
||||
public static final AID PF9 = new AID((byte) 0xF9, "PF9");
|
||||
public static final AID PF10 = new AID((byte) 0x7A, "PF10");
|
||||
public static final AID PF11 = new AID((byte) 0x7B, "PF11");
|
||||
public static final AID PF12 = new AID((byte) 0x7C, "PF12");
|
||||
public static final AID PF13 = new AID((byte) 0xC1, "PF13");
|
||||
public static final AID PF14 = new AID((byte) 0xC2, "PF14");
|
||||
public static final AID PF15 = new AID((byte) 0xC3, "PF15");
|
||||
public static final AID PF16 = new AID((byte) 0xC4, "PF16");
|
||||
public static final AID PF17 = new AID((byte) 0xC5, "PF17");
|
||||
public static final AID PF18 = new AID((byte) 0xC6, "PF18");
|
||||
public static final AID PF19 = new AID((byte) 0xC7, "PF19");
|
||||
public static final AID PF20 = new AID((byte) 0xC8, "PF20");
|
||||
public static final AID PF21 = new AID((byte) 0xC9, "PF21");
|
||||
public static final AID PF22 = new AID((byte) 0x4A, "PF22");
|
||||
public static final AID PF23 = new AID((byte) 0x4B, "PF23");
|
||||
public static final AID PF24 = new AID((byte) 0x4C, "PF24");
|
||||
|
||||
public AID(byte code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public byte translate() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AID aid = (AID) o;
|
||||
return code == aid.code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name != null ? name : String.format("AID(0x%02X)", code & 0xFF);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
/**
|
||||
* Standard External Presentation Interface (EPI) DataStream processor interface.
|
||||
*/
|
||||
public interface DataStream {
|
||||
|
||||
/**
|
||||
* Analyzes an inbound 3270 data stream buffer and updates the screen model.
|
||||
*
|
||||
* @param buffer Byte array containing the inbound 3270 record
|
||||
* @param length Length of active bytes in the buffer
|
||||
* @throws EPIException If a data stream format or command error is encountered
|
||||
*/
|
||||
void analyze(byte[] buffer, int length) throws EPIException;
|
||||
|
||||
/**
|
||||
* Formats modified screen fields into an outbound 3270 data stream.
|
||||
*
|
||||
* @param buffer Target byte array to receive formatted outbound record
|
||||
* @return Number of bytes written into the buffer
|
||||
* @throws EPIException If encoding or formatting fails
|
||||
*/
|
||||
int format(byte[] buffer) throws EPIException;
|
||||
|
||||
/**
|
||||
* Serializes the entire screen buffer into an outbound 3270 data stream.
|
||||
*
|
||||
* @param buffer Target byte array to receive the full screen dump
|
||||
* @return Number of bytes written into the buffer
|
||||
* @throws EPIException If encoding fails
|
||||
*/
|
||||
int readBuffer(byte[] buffer) throws EPIException;
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* 3270 stream processor implementing the External Presentation Interface (EPI).
|
||||
* Provides high-level stream analysis, buffer formatting, 12/14-bit buffer address
|
||||
* encoding/decoding, and stream-level character translation conforming to GA23-0059.
|
||||
*/
|
||||
public class DataStream3270 implements DataStream, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger log = Logger.getLogger(DataStream3270.class.getName());
|
||||
|
||||
public static final byte[] ENCODE_TABLE = new byte[]{
|
||||
32, 65, 66, 67, 68, 69, 70, 71, 72, 73, 91, 46, 60, 40, 43, 33,
|
||||
38, 74, 75, 76, 77, 78, 79, 80, 81, 82, 93, 36, 42, 41, 59, 94,
|
||||
45, 47, 83, 84, 85, 86, 87, 88, 89, 90, 124, 44, 37, 95, 62, 63,
|
||||
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 35, 64, 39, 61, 34
|
||||
};
|
||||
|
||||
public static final int[] DECODE_TABLE = new int[]{
|
||||
0, 15, 63, 59, 27, 44, 16, 61, 13, 29, 28, 14, 43, 32, 11, 33,
|
||||
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 30, 12, 62, 46, 47,
|
||||
60, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 18, 19, 20, 21, 22,
|
||||
23, 24, 25, 34, 35, 36, 37, 38, 39, 40, 41, 10, -1, 26, 31, 45,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 42, -1, -1, -1
|
||||
};
|
||||
|
||||
public static final char[] EBCDIC_TABLE = new char[]{
|
||||
'@', 'O', '\u007f', '{', '[', 'l', 'P', '}', 'M', ']', '\\', 'N', 'k', '`', 'K', 'a',
|
||||
'\u00f0', '\u00f1', '\u00f2', '\u00f3', '\u00f4', '\u00f5', '\u00f6', '\u00f7', '\u00f8', '\u00f9', 'z', '^', 'L', '~', 'n', 'o',
|
||||
'|', '\u00c1', '\u00c2', '\u00c3', '\u00c4', '\u00c5', '\u00c6', '\u00c7', '\u00c8', '\u00c9', '\u00d1', '\u00d2', '\u00d3', '\u00d4', '\u00d5', '\u00d6',
|
||||
'\u00d7', '\u00d8', '\u00d9', '\u00e2', '\u00e3', '\u00e4', '\u00e5', '\u00e6', '\u00e7', '\u00e8', '\u00e9', 'J', '\u0000', 'Z', '_', 'm',
|
||||
'\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000',
|
||||
'\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', '\u0000', 'j', '\u0000', '\u0000', '\u0000'
|
||||
};
|
||||
|
||||
private static final char[] blankChars = new char[]{'\u0000', '\n', '\f', '\r', '\u000e', '\u000f', '\u0019'};
|
||||
private static final String blanks = new String(blankChars);
|
||||
|
||||
private Screen screen;
|
||||
private boolean formatted = false;
|
||||
|
||||
public DataStream3270(Screen screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
public Screen getScreen() {
|
||||
return screen;
|
||||
}
|
||||
|
||||
public void setScreen(Screen screen) {
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
public boolean isFormatted() {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
public void setFormatted(boolean formatted) {
|
||||
this.formatted = formatted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void analyze(byte[] buffer, int length) throws EPIException {
|
||||
if (buffer == null || length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
int fieldCount = 0;
|
||||
int bufCount = 0;
|
||||
Field currentField = null;
|
||||
int curPos = 0;
|
||||
int cursorTarget = 0;
|
||||
int spanTarget = 0;
|
||||
byte[] lineBuffer = new byte[screen.getWidth()];
|
||||
boolean isNewField = true;
|
||||
int wcc = 3;
|
||||
int screenWidth = screen.getWidth();
|
||||
|
||||
if (length > 2) {
|
||||
wcc = toEbcdic(buffer[1]);
|
||||
}
|
||||
|
||||
switch (buffer[index]) {
|
||||
case 49: // Write (0x31)
|
||||
case (byte) 0xF1:
|
||||
curPos = cursorTarget = (screen.getCursorRow() - 1) * screenWidth + (screen.getCursorColumn() - 1);
|
||||
if ((wcc & 1) != 0) {
|
||||
int totalFields = screen.fieldCount();
|
||||
for (int i = 1; i <= totalFields; i++) {
|
||||
Field f = screen.field(i);
|
||||
if (f != null && f.dataTag() == 1) {
|
||||
f.resetDataTag();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 50: // Read Buffer (0x32)
|
||||
case (byte) 0xF2:
|
||||
screen.readMode = true;
|
||||
return;
|
||||
case 53: // Erase / Write (0x35)
|
||||
case (byte) 0xF5:
|
||||
screen.reset();
|
||||
curPos = 0;
|
||||
cursorTarget = 0;
|
||||
this.formatted = false;
|
||||
break;
|
||||
default:
|
||||
throw new EPI3270Exception(96, buffer[index], 4608);
|
||||
}
|
||||
|
||||
try {
|
||||
index = 2;
|
||||
while (index < length) {
|
||||
int op = buffer[index] & 0xFF;
|
||||
switch (op) {
|
||||
case 9: { // PT (Program Tab)
|
||||
log.finer("PT");
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
case 16: { // SFE (Start Field Extended)
|
||||
log.finer("SFE");
|
||||
this.formatted = true;
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
}
|
||||
currentField = screen.getField(curPos);
|
||||
if (currentField == null) {
|
||||
currentField = new Field(screen, curPos);
|
||||
isNewField = true;
|
||||
} else {
|
||||
currentField.setAttribute(true);
|
||||
currentField.setBaseAttribute('\u0000');
|
||||
currentField.setExtAttribute('A', '\u0000');
|
||||
currentField.setExtAttribute('B', '\u0000');
|
||||
currentField.setExtAttribute('E', '\u0000');
|
||||
currentField.setExtAttribute('F', '\u0000');
|
||||
isNewField = false;
|
||||
}
|
||||
int numPairs = buffer[++index] & 0xFF;
|
||||
for (int p = 1; p <= numPairs; p++) {
|
||||
char attrType = (char) buffer[index + 1];
|
||||
char attrVal = toEbcdic(buffer[index + 2]);
|
||||
currentField.setExtAttribute(attrType, attrVal);
|
||||
index += 2;
|
||||
}
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
case 17: { // SBA (Set Buffer Address)
|
||||
log.finer("SBA");
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
currentField = null;
|
||||
}
|
||||
curPos = decodeAddress(buffer[index + 1], buffer[index + 2]);
|
||||
index += 2;
|
||||
break;
|
||||
}
|
||||
case 18: { // EUA (Erase Unprotected to Address)
|
||||
log.finer("EUA");
|
||||
spanTarget = decodeAddress(buffer[index + 1], buffer[index + 2]);
|
||||
screen.resetFields(curPos, spanTarget);
|
||||
index += 2;
|
||||
curPos = spanTarget;
|
||||
break;
|
||||
}
|
||||
case 19: { // IC (Insert Cursor)
|
||||
log.finer("IC");
|
||||
cursorTarget = curPos;
|
||||
break;
|
||||
}
|
||||
case 20: { // RA (Repeat to Address)
|
||||
log.finer("RA");
|
||||
spanTarget = decodeAddress(buffer[index + 1], buffer[index + 2]);
|
||||
index += 3;
|
||||
byte repeatByte = buffer[index];
|
||||
if (currentField == null) {
|
||||
currentField = new Field(screen, curPos);
|
||||
currentField.setAttribute(false);
|
||||
isNewField = true;
|
||||
}
|
||||
if (spanTarget <= curPos) {
|
||||
int totalSize = screenWidth * screen.getDepth();
|
||||
while (curPos < totalSize) {
|
||||
if (bufCount >= screenWidth) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
|
||||
bufCount = 0;
|
||||
}
|
||||
lineBuffer[bufCount++] = repeatByte;
|
||||
curPos++;
|
||||
}
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
currentField = null;
|
||||
curPos = 0;
|
||||
if (spanTarget > 0) {
|
||||
currentField = new Field(screen, curPos);
|
||||
currentField.setAttribute(false);
|
||||
isNewField = true;
|
||||
}
|
||||
}
|
||||
while (curPos < spanTarget) {
|
||||
if (bufCount >= screenWidth) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
|
||||
bufCount = 0;
|
||||
}
|
||||
lineBuffer[bufCount++] = repeatByte;
|
||||
curPos++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 29: { // SF (Start Field)
|
||||
log.finer("SF");
|
||||
this.formatted = true;
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
}
|
||||
currentField = screen.getField(curPos);
|
||||
if (currentField == null) {
|
||||
currentField = new Field(screen, curPos);
|
||||
isNewField = true;
|
||||
} else {
|
||||
currentField.setAttribute(true);
|
||||
currentField.setExtAttribute('A', '\u0000');
|
||||
currentField.setExtAttribute('B', '\u0000');
|
||||
currentField.setExtAttribute('E', '\u0000');
|
||||
currentField.setExtAttribute('F', '\u0000');
|
||||
isNewField = false;
|
||||
}
|
||||
currentField.setBaseAttribute(toEbcdic(buffer[++index]));
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
case 26:
|
||||
case 30: { // MF (Modify Field)
|
||||
log.finer("MF");
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
bufCount = 0;
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
currentField = null;
|
||||
}
|
||||
int numPairs = buffer[++index] & 0xFF;
|
||||
currentField = screen.getField(curPos);
|
||||
if (currentField != null) {
|
||||
currentField.setAttribute(true);
|
||||
for (int p = 1; p <= numPairs; p++) {
|
||||
currentField.setExtAttribute((char) buffer[index + 1], toEbcdic(buffer[index + 2]));
|
||||
index += 2;
|
||||
}
|
||||
isNewField = false;
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
for (int p = 1; p <= numPairs; p++) {
|
||||
index += 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 31:
|
||||
case 40: { // SA (Set Attribute)
|
||||
log.finer("SA");
|
||||
index += 2;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (currentField == null) {
|
||||
currentField = screen.getField(curPos - 1);
|
||||
if (currentField == null) {
|
||||
currentField = new Field(screen, curPos);
|
||||
isNewField = true;
|
||||
currentField.setAttribute(false);
|
||||
} else {
|
||||
isNewField = false;
|
||||
}
|
||||
}
|
||||
if (bufCount >= screenWidth) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, screenWidth);
|
||||
bufCount = 0;
|
||||
}
|
||||
byte b = buffer[index];
|
||||
lineBuffer[bufCount++] = (blanks.indexOf(b) != -1) ? (byte) 32 : b;
|
||||
curPos++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
if (currentField != null) {
|
||||
if (bufCount > 0) {
|
||||
currentField.setBytes(curPos - bufCount, lineBuffer, bufCount);
|
||||
}
|
||||
if (isNewField) {
|
||||
screen.insertField(currentField);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Error during analyze", e);
|
||||
throw new EPI3270Exception(90, e, 4609);
|
||||
}
|
||||
|
||||
int maxCell = screenWidth * screen.getDepth();
|
||||
if (cursorTarget >= 0 && cursorTarget < maxCell) {
|
||||
screen.setCursor(cursorTarget / screenWidth + 1, cursorTarget % screenWidth + 1);
|
||||
} else {
|
||||
log.fine("Cursor address out of range: " + cursorTarget);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int format(byte[] buffer) throws EPIException {
|
||||
if (buffer == null || buffer.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
AID aid = screen.getAID();
|
||||
buffer[pos++] = aid.translate();
|
||||
|
||||
if (aid.equals(AID.clear)) {
|
||||
screen.initList();
|
||||
screen.setCursor(1, 1);
|
||||
this.formatted = false;
|
||||
return pos;
|
||||
}
|
||||
if (aid.equals(AID.PA1) || aid.equals(AID.PA2) || aid.equals(AID.PA3)) {
|
||||
return pos;
|
||||
}
|
||||
|
||||
int cursorAddr = (screen.getCursorRow() - 1) * screen.getWidth() + (screen.getCursorColumn() - 1);
|
||||
encodeAddress(buffer, pos, cursorAddr);
|
||||
pos += 2;
|
||||
|
||||
int totalFields = screen.fieldCount();
|
||||
try {
|
||||
if (this.formatted) {
|
||||
for (int i = 1; i <= totalFields; i++) {
|
||||
Field field = screen.field(i);
|
||||
if (field != null && field.dataTag() == 1) {
|
||||
buffer[pos++] = 17; // SBA order
|
||||
encodeAddress(buffer, pos, field.getPosition() + 1);
|
||||
pos += 2;
|
||||
byte[] bytes = field.getBytes();
|
||||
if (bytes != null && bytes.length > 0) {
|
||||
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
|
||||
pos += bytes.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (totalFields > 0) {
|
||||
Field f1 = screen.field(1);
|
||||
byte[] bytes = (f1 != null) ? f1.getBytes() : null;
|
||||
if (bytes != null && bytes.length > 0) {
|
||||
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
|
||||
pos += bytes.length;
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
log.log(Level.WARNING, "Unsupported encoding during format", uee);
|
||||
throw new EPI3270Exception(90, uee, 4609);
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readBuffer(byte[] buffer) throws EPIException {
|
||||
if (buffer == null || buffer.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
buffer[pos++] = screen.getAID().translate();
|
||||
int cursorAddr = (screen.getCursorRow() - 1) * screen.getWidth() + (screen.getCursorColumn() - 1);
|
||||
encodeAddress(buffer, pos, cursorAddr);
|
||||
pos += 2;
|
||||
|
||||
int totalFields = screen.fieldCount();
|
||||
try {
|
||||
for (int i = 1; i <= totalFields; i++) {
|
||||
Field field = screen.field(i);
|
||||
if (field == null) continue;
|
||||
buffer[pos++] = 17; // SBA
|
||||
encodeAddress(buffer, pos, field.getPosition());
|
||||
pos += 2;
|
||||
if (field.hasAttribute()) {
|
||||
buffer[pos++] = 29; // SF
|
||||
buffer[pos++] = toAscii(field.baseAttribute());
|
||||
}
|
||||
byte[] bytes = field.getBytes();
|
||||
if (bytes != null && bytes.length > 0) {
|
||||
System.arraycopy(bytes, 0, buffer, pos, bytes.length);
|
||||
pos += bytes.length;
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
log.log(Level.WARNING, "Unsupported encoding during readBuffer", uee);
|
||||
throw new EPI3270Exception(90, uee, 4609);
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes 12-bit / 14-bit presentation space address into 2 bytes.
|
||||
*/
|
||||
public void encodeAddress(byte[] target, int offset, int address) {
|
||||
if (address < 0 || address > 4096) {
|
||||
target[offset] = 32;
|
||||
target[offset + 1] = 32;
|
||||
return;
|
||||
}
|
||||
int hi = address / 64;
|
||||
int lo = address % 64;
|
||||
target[offset] = ENCODE_TABLE[hi];
|
||||
target[offset + 1] = ENCODE_TABLE[lo];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes 2-byte presentation space address into linear buffer position.
|
||||
*/
|
||||
public int decodeAddress(int b1, int b2) {
|
||||
int v1 = b1 & 0xFF;
|
||||
int v2 = b2 & 0xFF;
|
||||
if (v1 < 32 || v1 > 127 || v2 < 32 || v2 > 127) {
|
||||
return -1;
|
||||
}
|
||||
int d1 = DECODE_TABLE[v1 - 32];
|
||||
int d2 = DECODE_TABLE[v2 - 32];
|
||||
if (d1 < 0 || d2 < 0) {
|
||||
return -1;
|
||||
}
|
||||
return d1 * 64 + d2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates single byte to EBCDIC presentation character.
|
||||
*/
|
||||
public char toEbcdic(int b) {
|
||||
int v = b & 0xFF;
|
||||
if (v < 32 || v > 127) {
|
||||
return (char) v;
|
||||
}
|
||||
return EBCDIC_TABLE[v - 32];
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates character back to ASCII byte representation.
|
||||
*/
|
||||
public byte toAscii(char c) {
|
||||
if (c < '@' || c > '\u00f9') {
|
||||
return (byte) c;
|
||||
}
|
||||
for (int i = 0; i < EBCDIC_TABLE.length; i++) {
|
||||
if (EBCDIC_TABLE[i] == c) {
|
||||
return (byte) (i + 32);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
/**
|
||||
* 3270 protocol specific External Presentation Interface (EPI) exception.
|
||||
*/
|
||||
public class EPI3270Exception extends EPIException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int commandOrOrder = 0;
|
||||
|
||||
public EPI3270Exception(int errorCode, int commandOrOrder, int reasonCode) {
|
||||
super(errorCode, reasonCode);
|
||||
this.commandOrOrder = commandOrOrder;
|
||||
}
|
||||
|
||||
public EPI3270Exception(int errorCode, Throwable cause, int reasonCode) {
|
||||
super(errorCode, cause, reasonCode);
|
||||
}
|
||||
|
||||
public EPI3270Exception(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public int getCommandOrOrder() {
|
||||
return commandOrOrder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
/**
|
||||
* Base exception for External Presentation Interface (EPI) stream operations.
|
||||
*/
|
||||
public class EPIException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int errorCode = 0;
|
||||
private int reasonCode = 0;
|
||||
|
||||
public EPIException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public EPIException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public EPIException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public EPIException(int errorCode, Throwable cause) {
|
||||
super(cause);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public EPIException(int errorCode, Throwable cause, int reasonCode) {
|
||||
super(cause);
|
||||
this.errorCode = errorCode;
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
public EPIException(int errorCode, int reasonCode) {
|
||||
super("EPI Exception error=" + errorCode + " reason=" + reasonCode);
|
||||
this.errorCode = errorCode;
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public int getReasonCode() {
|
||||
return reasonCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* Bidirectional bridge between EPI Screen model and lib3270j ScreenBuffer.
|
||||
*/
|
||||
public class EpiScreenBufferBridge {
|
||||
|
||||
/**
|
||||
* Copies contents from an EPI Screen into a ScreenBuffer presentation space.
|
||||
*/
|
||||
public static void copyToScreenBuffer(Screen epiScreen, ScreenBuffer target) {
|
||||
if (epiScreen == null || target == null) return;
|
||||
|
||||
synchronized (target.getRenderLock()) {
|
||||
int w = epiScreen.getWidth();
|
||||
int h = epiScreen.getDepth();
|
||||
if (target.getCols() != w || target.getRows() != h) {
|
||||
target.setDimensions(h, w);
|
||||
}
|
||||
target.clear();
|
||||
|
||||
int count = epiScreen.fieldCount();
|
||||
for (int i = 1; i <= count; i++) {
|
||||
Field f = epiScreen.field(i);
|
||||
if (f == null) continue;
|
||||
int pos = f.getPosition();
|
||||
if (pos >= 0 && pos < target.getSize()) {
|
||||
if (f.hasAttribute()) {
|
||||
byte fa = (byte) (f.baseAttribute() & 0xFF);
|
||||
target.setFieldAttribute(pos, fa);
|
||||
}
|
||||
try {
|
||||
byte[] bytes = f.getBytes();
|
||||
if (bytes != null) {
|
||||
int textPos = f.hasAttribute() ? pos + 1 : pos;
|
||||
for (int bIdx = 0; bIdx < bytes.length && (textPos + bIdx) < target.getSize(); bIdx++) {
|
||||
ExtendedAttribute ea = target.getCell(textPos + bIdx);
|
||||
ea.ec = bytes[bIdx];
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
int r = Math.max(1, Math.min(target.getRows(), epiScreen.getCursorRow()));
|
||||
int c = Math.max(1, Math.min(target.getCols(), epiScreen.getCursorColumn()));
|
||||
target.setCursorAddress((r - 1) * target.getCols() + (c - 1));
|
||||
target.translateToUnicode();
|
||||
target.markAllChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts fields from a ScreenBuffer presentation space into an EPI Screen.
|
||||
*/
|
||||
public static void copyFromScreenBuffer(ScreenBuffer source, Screen epiScreen) {
|
||||
if (source == null || epiScreen == null) return;
|
||||
|
||||
synchronized (source.getRenderLock()) {
|
||||
epiScreen.setWidth(source.getCols());
|
||||
epiScreen.setDepth(source.getRows());
|
||||
epiScreen.initList();
|
||||
|
||||
int size = source.getSize();
|
||||
Field currentField = null;
|
||||
byte[] buf = new byte[source.getCols()];
|
||||
int bufLen = 0;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
byte fa = source.getFieldAttributeAt(i);
|
||||
if (fa != 0) {
|
||||
if (currentField != null && bufLen > 0) {
|
||||
currentField.setBytes(0, buf, bufLen);
|
||||
epiScreen.insertField(currentField);
|
||||
bufLen = 0;
|
||||
}
|
||||
currentField = new Field(epiScreen, i);
|
||||
currentField.setAttribute(true);
|
||||
currentField.setBaseAttribute((char) (fa & 0xFF));
|
||||
} else if (currentField != null) {
|
||||
ExtendedAttribute ea = source.getCell(i);
|
||||
if (bufLen >= buf.length) {
|
||||
currentField.setBytes(0, buf, bufLen);
|
||||
bufLen = 0;
|
||||
}
|
||||
buf[bufLen++] = ea.ec;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentField != null && bufLen > 0) {
|
||||
currentField.setBytes(0, buf, bufLen);
|
||||
epiScreen.insertField(currentField);
|
||||
}
|
||||
|
||||
int cursorAddr = source.getCursorAddress();
|
||||
int cols = source.getCols();
|
||||
epiScreen.setCursor((cursorAddr / cols) + 1, (cursorAddr % cols) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Field representation within an EPI 3270 Screen.
|
||||
*/
|
||||
public class Field implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Screen screen;
|
||||
private int position;
|
||||
private int length;
|
||||
private boolean hasAttribute;
|
||||
private char baseAttribute;
|
||||
private final Map<Character, Character> extAttributes = new HashMap<>();
|
||||
private int dataTag; // 1 = modified, 0 = unmodified
|
||||
private final ByteArrayOutputStream content = new ByteArrayOutputStream();
|
||||
|
||||
public Field(Screen screen, int position) {
|
||||
this.screen = screen;
|
||||
this.position = position;
|
||||
this.hasAttribute = false;
|
||||
this.baseAttribute = '\0';
|
||||
this.dataTag = 0;
|
||||
}
|
||||
|
||||
public Screen getScreen() {
|
||||
return screen;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length > 0 ? length : content.size();
|
||||
}
|
||||
|
||||
public void setLength(int length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public boolean hasAttribute() {
|
||||
return hasAttribute;
|
||||
}
|
||||
|
||||
public void setAttribute(boolean hasAttribute) {
|
||||
this.hasAttribute = hasAttribute;
|
||||
}
|
||||
|
||||
public char baseAttribute() {
|
||||
return baseAttribute;
|
||||
}
|
||||
|
||||
public void setBaseAttribute(char baseAttribute) {
|
||||
this.hasAttribute = true;
|
||||
this.baseAttribute = baseAttribute;
|
||||
// Bit 0x01 in 3270 attribute indicates Modified Data Tag (MDT)
|
||||
if ((baseAttribute & 0x01) != 0) {
|
||||
this.dataTag = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void setExtAttribute(char type, char value) {
|
||||
this.hasAttribute = true;
|
||||
extAttributes.put(type, value);
|
||||
}
|
||||
|
||||
public char getExtAttribute(char type) {
|
||||
Character val = extAttributes.get(type);
|
||||
return val != null ? val : '\0';
|
||||
}
|
||||
|
||||
public int dataTag() {
|
||||
return dataTag;
|
||||
}
|
||||
|
||||
public void resetDataTag() {
|
||||
this.dataTag = 0;
|
||||
}
|
||||
|
||||
public void setDataTag(int dataTag) {
|
||||
this.dataTag = dataTag;
|
||||
}
|
||||
|
||||
public void setBytes(int offset, byte[] data, int length) {
|
||||
if (data != null && length > 0) {
|
||||
content.write(data, 0, length);
|
||||
this.length = content.size();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getBytes() throws UnsupportedEncodingException {
|
||||
return content.toByteArray();
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
byte[] bytes = content.toByteArray();
|
||||
return new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
content.reset();
|
||||
if (text != null) {
|
||||
byte[] b = text.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||
content.write(b, 0, b.length);
|
||||
this.length = b.length;
|
||||
this.dataTag = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
content.reset();
|
||||
this.length = 0;
|
||||
this.dataTag = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Screen representation within an EPI 3270 session.
|
||||
*/
|
||||
public class Screen implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int width = 80;
|
||||
private int depth = 24;
|
||||
private int cursorRow = 1;
|
||||
private int cursorColumn = 1;
|
||||
private AID aid = AID.enter;
|
||||
public boolean readMode = false;
|
||||
|
||||
private final List<Field> fields = new ArrayList<>();
|
||||
|
||||
public Screen() {
|
||||
this(80, 24);
|
||||
}
|
||||
|
||||
public Screen(int width, int depth) {
|
||||
this.width = width;
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public void setDepth(int depth) {
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public int getCursorRow() {
|
||||
return cursorRow;
|
||||
}
|
||||
|
||||
public int getCursorColumn() {
|
||||
return cursorColumn;
|
||||
}
|
||||
|
||||
public void setCursor(int row, int col) {
|
||||
this.cursorRow = row;
|
||||
this.cursorColumn = col;
|
||||
}
|
||||
|
||||
public AID getAID() {
|
||||
return aid;
|
||||
}
|
||||
|
||||
public void setAID(AID aid) {
|
||||
this.aid = aid != null ? aid : AID.enter;
|
||||
}
|
||||
|
||||
public int fieldCount() {
|
||||
return fields.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves field by 1-based index matching EPI convention.
|
||||
*/
|
||||
public Field field(int oneBasedIndex) {
|
||||
if (oneBasedIndex >= 1 && oneBasedIndex <= fields.size()) {
|
||||
return fields.get(oneBasedIndex - 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the field starting at or covering the given linear buffer position.
|
||||
*/
|
||||
public Field getField(int position) {
|
||||
for (Field f : fields) {
|
||||
if (f.getPosition() == position) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void insertField(Field field) {
|
||||
if (field == null) return;
|
||||
// Keep fields ordered by position
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
if (fields.get(i).getPosition() == field.getPosition()) {
|
||||
fields.set(i, field);
|
||||
return;
|
||||
} else if (fields.get(i).getPosition() > field.getPosition()) {
|
||||
fields.add(i, field);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fields.add(field);
|
||||
}
|
||||
|
||||
public void initList() {
|
||||
fields.clear();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
fields.clear();
|
||||
cursorRow = 1;
|
||||
cursorColumn = 1;
|
||||
readMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets fields within the range [start, end) by erasing unprotected content.
|
||||
*/
|
||||
public void resetFields(int start, int end) {
|
||||
int maxPos = width * depth;
|
||||
for (Field f : fields) {
|
||||
int pos = f.getPosition();
|
||||
boolean inRange;
|
||||
if (start <= end) {
|
||||
inRange = (pos >= start && pos < end);
|
||||
} else {
|
||||
inRange = (pos >= start || pos < end);
|
||||
}
|
||||
if (inRange) {
|
||||
// If unprotected, clear content
|
||||
if (f.hasAttribute() && (f.baseAttribute() & 0x20) == 0) {
|
||||
f.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,18 @@ import java.util.regex.Pattern;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* VM/CMS Spool and Print File Transfer facility matching IBM Host On-Demand
|
||||
* (com.ibm.eNetwork.ECL.xfer3270.CMSPrintXfer).
|
||||
* VM/CMS Spool and Print File Transfer facility matching Host On-Demand specifications.
|
||||
*
|
||||
* Provides:
|
||||
* 1. VM/CMS Virtual Reader and Printer spool file catalog parsing (CP QUERY RDR / PRT).
|
||||
* 2. ANSI / ASA carriage control conversion (Fortran print formatting: ' ', '0', '-', '1', '+').
|
||||
* 3. IBM 1403/3211 Machine carriage control channel command byte translation.
|
||||
* 3. Machine carriage control channel command byte translation.
|
||||
* 4. High-level print spool stream extraction and transfer helpers.
|
||||
*/
|
||||
public class CMSPrintXfer {
|
||||
|
||||
private static final Logger log = Logger.getLogger(CMSPrintXfer.class.getName());
|
||||
private static final Pattern HEADER_PATTERN = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
|
||||
|
||||
private final ECLXfer xfer;
|
||||
private final EbcdicTranslator translator;
|
||||
@@ -126,13 +126,12 @@ public class CMSPrintXfer {
|
||||
if (text == null || text.trim().isEmpty()) return entries;
|
||||
|
||||
String[] lines = text.split("\r?\n");
|
||||
Pattern headerPattern = Pattern.compile("(?i)ORIGINID|FILE\\s+CLASS|RECORDS|HOLD\\s+DATE");
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
if (trimmed.startsWith("--") || trimmed.startsWith("==")) continue;
|
||||
if (headerPattern.matcher(trimmed).find()) continue;
|
||||
if (HEADER_PATTERN.matcher(trimmed).find()) continue;
|
||||
|
||||
SpoolFileEntry entry = parseSpoolLine(trimmed, defaultDevice);
|
||||
if (entry != null) {
|
||||
@@ -303,11 +302,11 @@ public class CMSPrintXfer {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// IBM 1403/3211 Machine Carriage Control Translation
|
||||
// Machine Carriage Control Translation
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Translates IBM Machine Carriage Control Channel Command bytes into formatted text bytes.
|
||||
* Translates Machine Carriage Control Channel Command bytes into formatted text bytes.
|
||||
*
|
||||
* Command codes:
|
||||
* 0x01: Write without line advance
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
package haus.nightmare.lib3270j.ft;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Configuration for an IND$FILE file transfer session.
|
||||
* Ported from x3270's ft_conf_t (ft_private.h).
|
||||
*/
|
||||
public class FTConfig {
|
||||
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
|
||||
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern CP_PATTERN = Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
|
||||
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
|
||||
|
||||
/** Host operating system type */
|
||||
public enum HostType {
|
||||
TSO, CMS, CICS
|
||||
@@ -210,26 +221,22 @@ public class FTConfig {
|
||||
String trimmed = opts.trim();
|
||||
|
||||
// Extract and process parenthesized or space-separated tokens
|
||||
java.util.regex.Pattern recfmPattern = java.util.regex.Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
|
||||
java.util.regex.Matcher recfmMatcher = recfmPattern.matcher(trimmed);
|
||||
Matcher recfmMatcher = RECFM_PATTERN.matcher(trimmed);
|
||||
if (recfmMatcher.find()) {
|
||||
setRecfm(recfmMatcher.group(1));
|
||||
}
|
||||
|
||||
java.util.regex.Pattern lreclPattern = java.util.regex.Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
|
||||
java.util.regex.Matcher lreclMatcher = lreclPattern.matcher(trimmed);
|
||||
Matcher lreclMatcher = LRECL_PATTERN.matcher(trimmed);
|
||||
if (lreclMatcher.find()) {
|
||||
setLrecl(lreclMatcher.group(1));
|
||||
}
|
||||
|
||||
java.util.regex.Pattern blkPattern = java.util.regex.Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
|
||||
java.util.regex.Matcher blkMatcher = blkPattern.matcher(trimmed);
|
||||
Matcher blkMatcher = BLK_PATTERN.matcher(trimmed);
|
||||
if (blkMatcher.find()) {
|
||||
setBlksize(blkMatcher.group(1));
|
||||
}
|
||||
|
||||
java.util.regex.Pattern spacePattern = java.util.regex.Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
|
||||
java.util.regex.Matcher spaceMatcher = spacePattern.matcher(trimmed);
|
||||
Matcher spaceMatcher = SPACE_PATTERN.matcher(trimmed);
|
||||
if (spaceMatcher.find()) {
|
||||
try {
|
||||
this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
|
||||
@@ -239,8 +246,7 @@ public class FTConfig {
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
java.util.regex.Pattern avbPattern = java.util.regex.Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
|
||||
java.util.regex.Matcher avbMatcher = avbPattern.matcher(trimmed);
|
||||
Matcher avbMatcher = AVB_PATTERN.matcher(trimmed);
|
||||
if (avbMatcher.find()) {
|
||||
try {
|
||||
this.avblock = Integer.parseInt(avbMatcher.group(1));
|
||||
@@ -248,14 +254,12 @@ public class FTConfig {
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
java.util.regex.Pattern cpPattern = java.util.regex.Pattern.compile("(?i)CODEPAGE[\\s\\(]+([A-Za-z0-9_-]+)\\)?");
|
||||
java.util.regex.Matcher cpMatcher = cpPattern.matcher(trimmed);
|
||||
Matcher cpMatcher = CP_PATTERN.matcher(trimmed);
|
||||
if (cpMatcher.find()) {
|
||||
this.codePage = cpMatcher.group(1);
|
||||
}
|
||||
|
||||
java.util.regex.Pattern mtuPattern = java.util.regex.Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
|
||||
java.util.regex.Matcher mtuMatcher = mtuPattern.matcher(trimmed);
|
||||
Matcher mtuMatcher = MTU_PATTERN.matcher(trimmed);
|
||||
if (mtuMatcher.find()) {
|
||||
try {
|
||||
setDftBufferSize(Integer.parseInt(mtuMatcher.group(1)));
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Platform-neutral Color representation encapsulating 32-bit ARGB.
|
||||
* Completely decouples lib3270j from java.awt.Color.
|
||||
*/
|
||||
public class Color implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final Color WHITE = new Color(0xFFFFFFFF);
|
||||
public static final Color LIGHT_GRAY = new Color(0xFFC0C0C0);
|
||||
public static final Color GRAY = new Color(0xFF808080);
|
||||
public static final Color DARK_GRAY = new Color(0xFF404040);
|
||||
public static final Color BLACK = new Color(0xFF000000);
|
||||
public static final Color RED = new Color(0xFFFF0000);
|
||||
public static final Color PINK = new Color(0xFFFFAFAF);
|
||||
public static final Color ORANGE = new Color(0xFFFFC800);
|
||||
public static final Color YELLOW = new Color(0xFFFFFF00);
|
||||
public static final Color GREEN = new Color(0xFF00FF00);
|
||||
public static final Color MAGENTA = new Color(0xFFFF00FF);
|
||||
public static final Color CYAN = new Color(0xFF00FFFF);
|
||||
public static final Color BLUE = new Color(0xFF0000FF);
|
||||
|
||||
private final int value;
|
||||
|
||||
public Color(int rgb) {
|
||||
this.value = 0xFF000000 | rgb;
|
||||
}
|
||||
|
||||
public Color(int rgba, boolean hasAlpha) {
|
||||
if (hasAlpha) {
|
||||
this.value = rgba;
|
||||
} else {
|
||||
this.value = 0xFF000000 | rgba;
|
||||
}
|
||||
}
|
||||
|
||||
public Color(int r, int g, int b) {
|
||||
this(r, g, b, 255);
|
||||
}
|
||||
|
||||
public Color(int r, int g, int b, int a) {
|
||||
this.value = ((a & 0xFF) << 24) |
|
||||
((r & 0xFF) << 16) |
|
||||
((g & 0xFF) << 8) |
|
||||
(b & 0xFF);
|
||||
}
|
||||
|
||||
public Color(float r, float g, float b) {
|
||||
this((int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5));
|
||||
}
|
||||
|
||||
public Color(float r, float g, float b, float a) {
|
||||
this((int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5), (int) (a * 255 + 0.5));
|
||||
}
|
||||
|
||||
public int getRGB() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public int getRed() {
|
||||
return (value >> 16) & 0xFF;
|
||||
}
|
||||
|
||||
public int getGreen() {
|
||||
return (value >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
public int getBlue() {
|
||||
return value & 0xFF;
|
||||
}
|
||||
|
||||
public int getAlpha() {
|
||||
return (value >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Color)) return false;
|
||||
return this.value == ((Color) obj).value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[r=" + getRed() + ",g=" + getGreen() + ",b=" + getBlue() + ",a=" + getAlpha() + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Default standalone implementation of PixelBuffer.
|
||||
* Provides pure Java software rasterization into a 32-bit ARGB contiguous integer array.
|
||||
*/
|
||||
public class DefaultPixelBuffer implements PixelBuffer {
|
||||
|
||||
private int width;
|
||||
private int height;
|
||||
private int[] pixels;
|
||||
|
||||
private boolean hasClip = false;
|
||||
private int clipX;
|
||||
private int clipY;
|
||||
private int clipWidth;
|
||||
private int clipHeight;
|
||||
|
||||
public DefaultPixelBuffer(int width, int height) {
|
||||
this.width = Math.max(1, width);
|
||||
this.height = Math.max(1, height);
|
||||
this.pixels = new int[this.width * this.height];
|
||||
}
|
||||
|
||||
public DefaultPixelBuffer(int width, int height, int[] existingPixels) {
|
||||
this.width = Math.max(1, width);
|
||||
this.height = Math.max(1, height);
|
||||
if (existingPixels != null && existingPixels.length >= this.width * this.height) {
|
||||
this.pixels = existingPixels;
|
||||
} else {
|
||||
this.pixels = new int[this.width * this.height];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getPixels() {
|
||||
return pixels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getPixel(int x, int y) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return 0;
|
||||
return pixels[y * width + x];
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setPixelDirect(int x, int y, int argb) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
||||
if (isClipped(x, y)) return;
|
||||
pixels[y * width + x] = argb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setPixel(int x, int y, int argb) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
||||
if (isClipped(x, y)) return;
|
||||
|
||||
int srcA = (argb >>> 24) & 0xFF;
|
||||
if (srcA == 0) return;
|
||||
|
||||
int idx = y * width + x;
|
||||
if (srcA == 255) {
|
||||
pixels[idx] = argb;
|
||||
return;
|
||||
}
|
||||
|
||||
int dst = pixels[idx];
|
||||
int dstA = (dst >>> 24) & 0xFF;
|
||||
if (dstA == 0) {
|
||||
pixels[idx] = argb;
|
||||
return;
|
||||
}
|
||||
|
||||
int srcR = (argb >>> 16) & 0xFF;
|
||||
int srcG = (argb >>> 8) & 0xFF;
|
||||
int srcB = argb & 0xFF;
|
||||
|
||||
int dstR = (dst >>> 16) & 0xFF;
|
||||
int dstG = (dst >>> 8) & 0xFF;
|
||||
int dstB = dst & 0xFF;
|
||||
|
||||
int outA = srcA + dstA * (255 - srcA) / 255;
|
||||
if (outA == 0) {
|
||||
pixels[idx] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
int outR = (srcR * srcA + dstR * dstA * (255 - srcA) / 255) / outA;
|
||||
int outG = (srcG * srcA + dstG * dstA * (255 - srcA) / 255) / outA;
|
||||
int outB = (srcB * srcA + dstB * dstA * (255 - srcA) / 255) / outA;
|
||||
|
||||
pixels[idx] = (outA << 24) | (outR << 16) | (outG << 8) | outB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
clear(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear(int argb) {
|
||||
Arrays.fill(pixels, argb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setClip(int x, int y, int width, int height) {
|
||||
this.hasClip = true;
|
||||
this.clipX = x;
|
||||
this.clipY = y;
|
||||
this.clipWidth = Math.max(0, width);
|
||||
this.clipHeight = Math.max(0, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clearClip() {
|
||||
this.hasClip = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClipped(int x, int y) {
|
||||
if (!hasClip) return false;
|
||||
return x < clipX || x >= (clipX + clipWidth) || y < clipY || y >= (clipY + clipHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void fillRect(int x, int y, int w, int h, int argb) {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
int x1 = Math.max(0, x);
|
||||
int y1 = Math.max(0, y);
|
||||
int x2 = Math.min(width, x + w);
|
||||
int y2 = Math.min(height, y + h);
|
||||
|
||||
if (hasClip) {
|
||||
x1 = Math.max(x1, clipX);
|
||||
y1 = Math.max(y1, clipY);
|
||||
x2 = Math.min(x2, clipX + clipWidth);
|
||||
y2 = Math.min(y2, clipY + clipHeight);
|
||||
}
|
||||
|
||||
for (int row = y1; row < y2; row++) {
|
||||
int rowOffset = row * width;
|
||||
Arrays.fill(pixels, rowOffset + x1, rowOffset + x2, argb);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY) {
|
||||
if (srcPixels == null || srcW <= 0 || srcH <= 0) return;
|
||||
|
||||
for (int r = 0; r < srcH; r++) {
|
||||
int sy = srcY + r;
|
||||
int dy = dstY + r;
|
||||
if (dy < 0 || dy >= height) continue;
|
||||
|
||||
for (int c = 0; c < srcW; c++) {
|
||||
int sx = srcX + c;
|
||||
int dx = dstX + c;
|
||||
if (dx < 0 || dx >= width) continue;
|
||||
if (isClipped(dx, dy)) continue;
|
||||
|
||||
int sp = srcPixels[sy * srcW + sx];
|
||||
setPixel(dx, dy, sp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, int argb) {
|
||||
drawLineBresenham(x1, y1, x2, y2, argb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) {
|
||||
int dx = Math.abs(x1 - x0);
|
||||
int dy = Math.abs(y1 - y0);
|
||||
int sx = (x0 < x1) ? 1 : -1;
|
||||
int sy = (y0 < y1) ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
int curX = x0;
|
||||
int curY = y0;
|
||||
|
||||
while (true) {
|
||||
setPixel(curX, curY, color);
|
||||
if (curX == x1 && curY == y1) break;
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
curX += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
curY += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
|
||||
int lw = Math.max(1, lineWidth);
|
||||
int ix0 = (int) Math.round(x0);
|
||||
int iy0 = (int) Math.round(y0);
|
||||
int ix1 = (int) Math.round(x1);
|
||||
int iy1 = (int) Math.round(y1);
|
||||
|
||||
if (lw == 1) {
|
||||
drawLineBresenham(ix0, iy0, ix1, iy1, colorArgb);
|
||||
} else {
|
||||
int half = lw / 2;
|
||||
for (int ox = -half; ox <= half; ox++) {
|
||||
for (int oy = -half; oy <= half; oy++) {
|
||||
drawLineBresenham(ix0 + ox, iy0 + oy, ix1 + ox, iy1 + oy, colorArgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int colorArgb, double strokeWidth) {
|
||||
// Pure Java anti-aliased line rendering
|
||||
double dx = x1 - x0;
|
||||
double dy = y1 - y0;
|
||||
double len = Math.hypot(dx, dy);
|
||||
if (len < 1e-4) {
|
||||
setPixel((int) Math.round(x0), (int) Math.round(y0), colorArgb);
|
||||
return;
|
||||
}
|
||||
|
||||
double radius = Math.max(0.5, strokeWidth * 0.5);
|
||||
int minX = (int) Math.floor(Math.min(x0, x1) - radius - 1);
|
||||
int maxX = (int) Math.ceil(Math.max(x0, x1) + radius + 1);
|
||||
int minY = (int) Math.floor(Math.min(y0, y1) - radius - 1);
|
||||
int maxY = (int) Math.ceil(Math.max(y0, y1) + radius + 1);
|
||||
|
||||
minX = Math.max(0, minX);
|
||||
maxX = Math.min(width - 1, maxX);
|
||||
minY = Math.max(0, minY);
|
||||
maxY = Math.min(height - 1, maxY);
|
||||
|
||||
int baseAlpha = (colorArgb >>> 24) & 0xFF;
|
||||
if (baseAlpha == 0) baseAlpha = 255;
|
||||
int rgbOnly = colorArgb & 0x00FFFFFF;
|
||||
|
||||
double invLenSq = 1.0 / (len * len);
|
||||
|
||||
for (int py = minY; py <= maxY; py++) {
|
||||
for (int px = minX; px <= maxX; px++) {
|
||||
double u = ((px - x0) * dx + (py - y0) * dy) * invLenSq;
|
||||
u = Math.max(0.0, Math.min(1.0, u));
|
||||
double projX = x0 + u * dx;
|
||||
double projY = y0 + u * dy;
|
||||
double dist = Math.hypot(px - projX, py - projY);
|
||||
|
||||
if (dist <= radius) {
|
||||
double coverage = 1.0 - (dist / radius);
|
||||
coverage = Math.sin(coverage * Math.PI * 0.5); // Smooth cosine roll-off
|
||||
int effectiveAlpha = (int) (baseAlpha * coverage);
|
||||
if (effectiveAlpha > 0) {
|
||||
setPixel(px, py, (effectiveAlpha << 24) | rgbOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lightweight pure-Java POJO dimension for 2D width and height.
|
||||
* Completely decouples lib3270j from java.awt.Dimension.
|
||||
*/
|
||||
public class Dimension implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int width;
|
||||
public int height;
|
||||
|
||||
public Dimension() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
public Dimension(Dimension d) {
|
||||
this(d != null ? d.width : 0, d != null ? d.height : 0);
|
||||
}
|
||||
|
||||
public Dimension(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setSize(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public void setSize(Dimension d) {
|
||||
if (d != null) {
|
||||
this.width = d.width;
|
||||
this.height = d.height;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Dimension)) return false;
|
||||
Dimension d = (Dimension) obj;
|
||||
return (width == d.width) && (height == d.height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[width=" + width + ",height=" + height + "]";
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class FillArea {
|
||||
/**
|
||||
* IBM Host On-Demand multi-polygon constructor.
|
||||
*/
|
||||
public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, java.awt.Color color) {
|
||||
public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, Color color) {
|
||||
this();
|
||||
if (color != null) {
|
||||
this.fillColor = color.getRGB();
|
||||
@@ -72,9 +72,28 @@ public class FillArea {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized java.awt.Rectangle getBounds() {
|
||||
public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, int argb) {
|
||||
this();
|
||||
this.fillColor = argb;
|
||||
if (px != null && py != null && polyCounts != null) {
|
||||
int offset = 0;
|
||||
for (int i = 0; i < numPolys && i < polyCounts.length; i++) {
|
||||
int count = polyCounts[i];
|
||||
if (count >= 2 && offset + count <= px.length && offset + count <= py.length) {
|
||||
int[] sx = new int[count];
|
||||
int[] sy = new int[count];
|
||||
System.arraycopy(px, offset, sx, 0, count);
|
||||
System.arraycopy(py, offset, sy, 0, count);
|
||||
addPolygon(sx, sy, count);
|
||||
}
|
||||
offset += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized Rectangle getBounds() {
|
||||
if (edges.isEmpty()) {
|
||||
return new java.awt.Rectangle(0, 0, 0, 0);
|
||||
return new Rectangle(0, 0, 0, 0);
|
||||
}
|
||||
double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
|
||||
double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
|
||||
@@ -88,7 +107,7 @@ public class FillArea {
|
||||
int y = (int) Math.floor(minY);
|
||||
int w = (int) Math.ceil(maxX) - x + 1;
|
||||
int h = (int) Math.ceil(maxY) - y + 1;
|
||||
return new java.awt.Rectangle(x, y, Math.max(0, w), Math.max(0, h));
|
||||
return new Rectangle(x, y, Math.max(0, w), Math.max(0, h));
|
||||
}
|
||||
|
||||
public synchronized void setFillModeOR() {
|
||||
@@ -123,18 +142,20 @@ public class FillArea {
|
||||
this.pixelPattern = pat;
|
||||
}
|
||||
|
||||
public synchronized java.awt.Image getImage() {
|
||||
java.awt.Rectangle b = getBounds();
|
||||
public synchronized PixelBuffer getPixelBuffer() {
|
||||
Rectangle b = getBounds();
|
||||
if (b.width <= 0 || b.height <= 0) {
|
||||
return new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
return new DefaultPixelBuffer(1, 1);
|
||||
}
|
||||
GraphicsPlane tempPlane = new GraphicsPlane(b.x + b.width, b.y + b.height);
|
||||
fill(tempPlane, fillColor, 0, solidFill ? GocaConstants.PT_SOLID : 0, false, 0, 0, 1, 0, 0, null);
|
||||
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(b.width, b.height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
java.awt.Graphics g = img.getGraphics();
|
||||
g.drawImage(tempPlane.getImage(), -b.x, -b.y, null);
|
||||
g.dispose();
|
||||
return img;
|
||||
DefaultPixelBuffer cropped = new DefaultPixelBuffer(b.width, b.height);
|
||||
cropped.blit(tempPlane.getRgbBuffer(), b.x, b.y, b.width, b.height, 0, 0);
|
||||
return cropped;
|
||||
}
|
||||
|
||||
public synchronized Object getImage() {
|
||||
return getPixelBuffer();
|
||||
}
|
||||
|
||||
public synchronized void dispose() {
|
||||
@@ -255,6 +276,29 @@ public class FillArea {
|
||||
}
|
||||
}
|
||||
|
||||
private void plotFillPixel(GraphicsPlane plane, int x, int y, int colorArgb, boolean isMixOr) {
|
||||
if (isMixOr) {
|
||||
int dst = plane.getPixel(x, y);
|
||||
int dstR = (dst >>> 16) & 0xFF;
|
||||
int dstG = (dst >>> 8) & 0xFF;
|
||||
int dstB = dst & 0xFF;
|
||||
|
||||
int srcR = (colorArgb >>> 16) & 0xFF;
|
||||
int srcG = (colorArgb >>> 8) & 0xFF;
|
||||
int srcB = colorArgb & 0xFF;
|
||||
|
||||
int outR = Math.min(255, dstR | srcR);
|
||||
int outG = Math.min(255, dstG | srcG);
|
||||
int outB = Math.min(255, dstB | srcB);
|
||||
int outA = Math.max((dst >>> 24) & 0xFF, (colorArgb >>> 24) & 0xFF);
|
||||
if (outA == 0 && (outR != 0 || outG != 0 || outB != 0)) outA = 255;
|
||||
|
||||
plane.setPixelDirect(x, y, (outA << 24) | (outR << 16) | (outG << 8) | outB);
|
||||
} else {
|
||||
plane.setPixel(x, y, colorArgb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule.
|
||||
*/
|
||||
@@ -270,16 +314,11 @@ public class FillArea {
|
||||
|
||||
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int bg = bgColorArgb;
|
||||
|
||||
// Background mix / transparency rule for Black fills:
|
||||
// BMX_TRANSPARENT / 0 or 2 / MIX_DEFAULT: Transparent black
|
||||
// BMX_OPAQUE / 1: Opaque background overpaint
|
||||
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
||||
(bgMix == GocaConstants.BMX_DEFAULT || bgMix == GocaConstants.BMX_TRANSPARENT ||
|
||||
bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0 || bgMix == 2);
|
||||
boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1);
|
||||
|
||||
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
||||
boolean isMixOr = fillModeOR || (plane.getMixMode() == GocaConstants.MIX_OR && (getBounds().width >= 5 && getBounds().height >= 5 && getBounds().width * getBounds().height >= 100));
|
||||
|
||||
if (pattern != GocaConstants.PT_EMPTY) {
|
||||
double minY = Double.MAX_VALUE;
|
||||
double maxY = Double.MIN_VALUE;
|
||||
|
||||
@@ -340,18 +379,18 @@ public class FillArea {
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
plane.setPixel(x, y, fill);
|
||||
plotFillPixel(plane, x, y, fill, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
plane.setPixel(x, y, bg);
|
||||
plotFillPixel(plane, x, y, bg, isMixOr);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
|
||||
plotFillPixel(plane, x, y, fill, isMixOr);
|
||||
} else if (patRows != null) {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
plane.setPixel(x, y, fill);
|
||||
plotFillPixel(plane, x, y, fill, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
plane.setPixel(x, y, bg);
|
||||
plotFillPixel(plane, x, y, bg, isMixOr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,18 +413,18 @@ public class FillArea {
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
plane.setPixel(x, y, fill);
|
||||
plotFillPixel(plane, x, y, fill, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
plane.setPixel(x, y, bg);
|
||||
plotFillPixel(plane, x, y, bg, isMixOr);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
plane.setPixel(x, y, fill);
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
|
||||
plotFillPixel(plane, x, y, fill, isMixOr);
|
||||
} else if (patRows != null) {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
plane.setPixel(x, y, fill);
|
||||
plotFillPixel(plane, x, y, fill, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
plane.setPixel(x, y, bg);
|
||||
plotFillPixel(plane, x, y, bg, isMixOr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Point;
|
||||
|
||||
/**
|
||||
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
|
||||
|
||||
@@ -20,6 +20,7 @@ public class GocaDecoder {
|
||||
private int curX = 0;
|
||||
private int curY = 0;
|
||||
private int curColor = GocaConstants.GOCA_COLORS[0];
|
||||
private int fgMix = GocaConstants.MIX_DEFAULT;
|
||||
private int bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec)
|
||||
private int bgColor = GocaConstants.GOCA_COLORS[8]; // Black
|
||||
private int lineType = GocaConstants.LT_SOLID;
|
||||
@@ -35,8 +36,8 @@ public class GocaDecoder {
|
||||
private double charAngle = 0.0;
|
||||
private double charShear = 0.0;
|
||||
private double fractionalLineWidth = 1.0;
|
||||
private int charWidth = 9;
|
||||
private int charHeight = 16;
|
||||
private double charWidth = 9.0;
|
||||
private double charHeight = 16.0;
|
||||
private int charSet = 0;
|
||||
private int charPrecision = GocaConstants.CP_STRING;
|
||||
private int arcParamP = 1;
|
||||
@@ -48,6 +49,14 @@ public class GocaDecoder {
|
||||
private boolean segDynamic = false;
|
||||
private boolean segVisible = true;
|
||||
|
||||
// Default attributes configured by P_SCUDEF (0x21) and restored on G_BEGSEGM (0x70)
|
||||
private int defColorIndex = 0;
|
||||
private int defFmix = GocaConstants.MIX_DEFAULT;
|
||||
private int defLineType = GocaConstants.LT_SOLID;
|
||||
private int defLineWidth = GocaConstants.LW_NORMAL;
|
||||
private int defPattern = GocaConstants.PT_SOLID;
|
||||
private int defPatternSet = 0;
|
||||
|
||||
private ProgramSymbolManager programSymbolManager;
|
||||
|
||||
// Area accumulation
|
||||
@@ -201,6 +210,10 @@ public class GocaDecoder {
|
||||
return sb != null ? sb.tag : 0;
|
||||
}
|
||||
|
||||
public synchronized int getCurrentSegId() {
|
||||
return currentSegId;
|
||||
}
|
||||
|
||||
private void trackPoint(int x, int y) {
|
||||
if (currentSegId != 0) {
|
||||
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
|
||||
@@ -277,6 +290,10 @@ public class GocaDecoder {
|
||||
flags, segChained, segDynamic, segVisible));
|
||||
}
|
||||
|
||||
public synchronized int getFgMix() {
|
||||
return fgMix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes GOCA order 0x11 Fractional Line Width calculation.
|
||||
*/
|
||||
@@ -303,15 +320,25 @@ public class GocaDecoder {
|
||||
segmentBoundsMap.clear();
|
||||
activeSegmentsInOrder.clear();
|
||||
currentSegId = 0;
|
||||
defColorIndex = 0;
|
||||
defFmix = GocaConstants.MIX_DEFAULT;
|
||||
defLineType = GocaConstants.LT_SOLID;
|
||||
defLineWidth = GocaConstants.LW_NORMAL;
|
||||
defPattern = GocaConstants.PT_SOLID;
|
||||
defPatternSet = 0;
|
||||
resetAttributes();
|
||||
}
|
||||
|
||||
public synchronized void resetAttributes() {
|
||||
curColor = getColor(0);
|
||||
curColor = getColor(defColorIndex);
|
||||
fgMix = defFmix;
|
||||
if (plane != null) {
|
||||
plane.setMixMode(defFmix);
|
||||
}
|
||||
bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec)
|
||||
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
|
||||
lineType = GocaConstants.LT_SOLID;
|
||||
lineWidth = GocaConstants.LW_NORMAL;
|
||||
lineType = defLineType;
|
||||
lineWidth = defLineWidth;
|
||||
fractionalLineWidth = 1.0;
|
||||
if (plane != null) {
|
||||
plane.setFractionalLineWidth(1.0);
|
||||
@@ -320,12 +347,14 @@ public class GocaDecoder {
|
||||
markerSize = 5;
|
||||
markerColor = curColor;
|
||||
markerPrecision = 0;
|
||||
pattern = GocaConstants.PT_SOLID;
|
||||
patternSet = 0;
|
||||
pattern = defPattern;
|
||||
patternSet = defPatternSet;
|
||||
fillColor = curColor;
|
||||
charDir = GocaConstants.CD_LR;
|
||||
charAngle = 0.0;
|
||||
charShear = 0.0;
|
||||
charWidth = 9.0;
|
||||
charHeight = 16.0;
|
||||
charSet = 0;
|
||||
charPrecision = GocaConstants.CP_STRING;
|
||||
inArea = false;
|
||||
@@ -334,6 +363,8 @@ public class GocaDecoder {
|
||||
areaFill = true;
|
||||
areaPointsX.clear();
|
||||
areaPointsY.clear();
|
||||
areaPolygons.clear();
|
||||
currentPolyPts = 0;
|
||||
inImage = false;
|
||||
imgBitDepth = GocaConstants.BPP_1;
|
||||
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
@@ -359,35 +390,45 @@ public class GocaDecoder {
|
||||
* 4. Self-defining orders with multi-byte payloads (GLINE 0xC1, GARC 0xC6, GCHST 0xC3, GRLINE 0xE1, etc.)
|
||||
* have a 1-byte length byte at data[idx + 1], making total length = payloadLen + 2.
|
||||
*/
|
||||
private int getOrderLength(byte[] data, int idx, int end) {
|
||||
int getOrderLength(byte[] data, int idx, int end) {
|
||||
int order = data[idx] & 0xFF;
|
||||
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
|
||||
return 1;
|
||||
}
|
||||
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 93 00, 91 00)
|
||||
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00)
|
||||
if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM ||
|
||||
order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE ||
|
||||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG ||
|
||||
(inImage && order == GocaConstants.G_GEIMG_ALT)) {
|
||||
order == GocaConstants.G_GPOP) {
|
||||
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
||||
}
|
||||
// G_GEIMG (0x93 End Image): self-defining draw order (e.g. 93 02 00 00 or 93 00 or 93)
|
||||
if (order == GocaConstants.G_GEIMG) {
|
||||
return (idx + 1 < end) ? ((data[idx + 1] & 0xFF) + 2) : 1;
|
||||
}
|
||||
if (idx + 1 >= end) {
|
||||
return -1;
|
||||
}
|
||||
// Fractional Line Width (0x11): 2-byte operand [int][frac] or 1-byte operand [int]
|
||||
// Fractional Line Width (0x11): 2-byte operand [int][frac] in standalone test or 1-byte operand [int] in stream
|
||||
if (order == GocaConstants.G_GSFLW) {
|
||||
return (idx + 2 < end) ? 3 : 2;
|
||||
if (idx + 3 == end) {
|
||||
return 3;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
|
||||
if (order < 0x20) {
|
||||
return 2;
|
||||
}
|
||||
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1)
|
||||
// GSCS (0x38) can optionally have a 1-byte length prefix (0x01) in synthetic tests
|
||||
if (order == GocaConstants.G_GSCS && data[idx + 1] == 0x01 && idx + 2 < end) {
|
||||
return 3;
|
||||
}
|
||||
// Short 2-byte attribute orders per IBM Host On-Demand HODDrawOrder2Byte
|
||||
if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT ||
|
||||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD ||
|
||||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMP ||
|
||||
order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
|
||||
return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2;
|
||||
return 2;
|
||||
}
|
||||
if (order == GocaConstants.G_GCALL) {
|
||||
return (data[idx + 1] == 0x04 && idx + 5 < end) ? 6 : 5;
|
||||
@@ -514,31 +555,45 @@ public class GocaDecoder {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GEIMG: // 0x93
|
||||
case GocaConstants.G_GEIMG_ALT: { // 0x91
|
||||
if (inImage || order == GocaConstants.G_GEIMG) {
|
||||
case GocaConstants.G_GEIMG: { // 0x93 End Image
|
||||
endImage();
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GBIMGC: { // 0x91: Begin Image at Current Position (G_GBIMGC)
|
||||
if (inImage) {
|
||||
endImage();
|
||||
idx += orderLen;
|
||||
} else {
|
||||
// 0x91: Begin Image at Current Position (G_GBIMGC)
|
||||
if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
|
||||
int w = readCoord(inputData, idx + 2);
|
||||
int h = readCoord(inputData, idx + 4);
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
if (payloadLen >= 5) {
|
||||
int fmt = inputData[idx + 6] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 6) {
|
||||
compression = inputData[idx + 7] & 0xFF;
|
||||
}
|
||||
beginImage(curX, curY, w, h, bitDepth, compression);
|
||||
}
|
||||
idx += orderLen;
|
||||
}
|
||||
int w = 0, h = 0;
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
|
||||
w = readCoord(inputData, idx + 4);
|
||||
h = readCoord(inputData, idx + 6);
|
||||
if (payloadLen >= 7) {
|
||||
int fmt = inputData[idx + 8] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 8) {
|
||||
compression = inputData[idx + 9] & 0xFF;
|
||||
}
|
||||
} else if (payloadLen >= 4 && idx + 2 + payloadLen <= end) {
|
||||
w = readCoord(inputData, idx + 2);
|
||||
h = readCoord(inputData, idx + 4);
|
||||
if (payloadLen >= 5) {
|
||||
int fmt = inputData[idx + 6] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 6) {
|
||||
compression = inputData[idx + 7] & 0xFF;
|
||||
}
|
||||
}
|
||||
beginImage(curX, curY, w, h, bitDepth, compression);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
|
||||
@@ -688,7 +743,14 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCH: { // Set Character Cell (0x33)
|
||||
if (payloadLen >= 4 && idx + 5 < end) {
|
||||
if (payloadLen >= 8 && idx + 9 < end) {
|
||||
int wInt = readCoord(inputData, idx + 2);
|
||||
int hInt = readCoord(inputData, idx + 4);
|
||||
int wFrac = ((inputData[idx + 6] & 0xFF) << 8) | (inputData[idx + 7] & 0xFF);
|
||||
int hFrac = ((inputData[idx + 8] & 0xFF) << 8) | (inputData[idx + 9] & 0xFF);
|
||||
charWidth = wInt + (wFrac / 65536.0);
|
||||
charHeight = hInt + (hFrac / 65536.0);
|
||||
} else if (payloadLen >= 4 && idx + 5 < end) {
|
||||
charWidth = readCoord(inputData, idx + 2);
|
||||
charHeight = readCoord(inputData, idx + 4);
|
||||
}
|
||||
@@ -739,17 +801,26 @@ public class GocaDecoder {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case 0x04: // HODSegmentCharacteristics (2-byte NOP per HoD)
|
||||
case 0x05:
|
||||
case 0x06:
|
||||
case 0x12: {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
|
||||
lineType = inputData[idx + 1] & 0xFF;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case 0x04:
|
||||
case 0x05:
|
||||
case 0x12:
|
||||
case GocaConstants.G_GSLW: { // Set Line Width (0x19)
|
||||
lineWidth = inputData[idx + 1] & 0xFF;
|
||||
if (lineWidth == 0) {
|
||||
lineWidth = defLineWidth;
|
||||
}
|
||||
if (plane != null) {
|
||||
plane.setLineWidth(lineWidth);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
@@ -803,7 +874,15 @@ public class GocaDecoder {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSMX:
|
||||
case GocaConstants.G_GSMX: { // Set Mix (0x0C)
|
||||
fgMix = inputData[idx + 1] & 0xFF;
|
||||
if (plane != null) {
|
||||
plane.setMixMode(fgMix);
|
||||
}
|
||||
logger.info("GOCA GSMX: fgMix=" + fgMix);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSMS_SET:
|
||||
case GocaConstants.G_GPOP: {
|
||||
idx += orderLen;
|
||||
@@ -816,9 +895,10 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GBAR: { // Begin Area (0x68)
|
||||
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||
boolean drawBoundary = (flags & 0x80) != 0;
|
||||
int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
|
||||
int flags = inputData[idx + 1] & 0xFF;
|
||||
// IBM bit numbering: Bit 1 (0x40) is the boundary flag per IBM Host On-Demand HODDecoder:2229
|
||||
boolean drawBoundary = (flags & 0x40) != 0;
|
||||
int fillRule = (flags & 0x20) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
|
||||
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
|
||||
beginArea(drawBoundary, fillRule);
|
||||
idx += orderLen;
|
||||
@@ -923,21 +1003,39 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GBIMG: { // Begin Image (0xD1)
|
||||
if (inImage) {
|
||||
endImage();
|
||||
}
|
||||
if (payloadLen >= 8 && idx + 2 + payloadLen <= end) {
|
||||
int x = readCoord(inputData, idx + 2);
|
||||
int y = readCoord(inputData, idx + 4);
|
||||
int w = readCoord(inputData, idx + 6);
|
||||
int h = readCoord(inputData, idx + 8);
|
||||
int w, h;
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
if (payloadLen >= 9) {
|
||||
int fmt = inputData[idx + 10] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 10) {
|
||||
compression = inputData[idx + 11] & 0xFF;
|
||||
w = readCoord(inputData, idx + 8);
|
||||
h = readCoord(inputData, idx + 10);
|
||||
if (payloadLen >= 11) {
|
||||
int fmt = inputData[idx + 12] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 12) {
|
||||
compression = inputData[idx + 13] & 0xFF;
|
||||
}
|
||||
} else {
|
||||
w = readCoord(inputData, idx + 6);
|
||||
h = readCoord(inputData, idx + 8);
|
||||
if (payloadLen >= 9) {
|
||||
int fmt = inputData[idx + 10] & 0xFF;
|
||||
if (fmt == 2) bitDepth = GocaConstants.BPP_2;
|
||||
else if (fmt == 4) bitDepth = GocaConstants.BPP_4;
|
||||
else if (fmt == 8) bitDepth = GocaConstants.BPP_8;
|
||||
}
|
||||
if (payloadLen >= 10) {
|
||||
compression = inputData[idx + 11] & 0xFF;
|
||||
}
|
||||
}
|
||||
beginImage(x, y, w, h, bitDepth, compression);
|
||||
}
|
||||
@@ -1017,13 +1115,9 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults (HODCurrentDefaults)
|
||||
// Note: Per IBM 3179G / HOD architecture, 0x21 sets default drawing attributes (color, line, pattern).
|
||||
// It is NOT an executive segment redraw order. A prior attempt treated 0x21 as an invented
|
||||
// P_SCUDEF segment redraw loop, which caused old dropdown menus and segments to be repeatedly
|
||||
// repainted on top of the screen, creating ghost artifacts and stale bounding boxes.
|
||||
if (idx + 1 < end) {
|
||||
int pLen = data[idx + 1] & 0xFF;
|
||||
logger.fine(String.format("GOCA Set Current Defaults (0x21): len=%d", pLen));
|
||||
processCurrentDefaults(data, idx, pLen);
|
||||
idx += pLen + 2;
|
||||
} else {
|
||||
idx++;
|
||||
@@ -1068,6 +1162,63 @@ public class GocaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
private void processCurrentDefaults(byte[] data, int offset, int pLen) {
|
||||
if (pLen < 4 || offset + pLen + 2 > data.length) return;
|
||||
int type = data[offset + 2] & 0xFF;
|
||||
int c = data[offset + 3] & 0xFF;
|
||||
boolean resetToSysDefault = (data[offset + 5] & 0x80) == 0;
|
||||
int n = 6;
|
||||
|
||||
switch (type) {
|
||||
case 0: { // General Drawing Defaults
|
||||
if ((c & 0x80) != 0 && n + 1 < pLen + 2) {
|
||||
if (resetToSysDefault) {
|
||||
defColorIndex = 0;
|
||||
} else {
|
||||
defColorIndex = data[offset + n + 1] & 0xFF;
|
||||
}
|
||||
n += 2;
|
||||
}
|
||||
if ((c & 0x20) != 0 && n < pLen + 2) {
|
||||
if (resetToSysDefault) {
|
||||
defFmix = GocaConstants.MIX_DEFAULT;
|
||||
} else {
|
||||
defFmix = data[offset + n++] & 0xFF;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: { // Line Defaults
|
||||
if ((c & 0x80) != 0 && n < pLen + 2) {
|
||||
defLineType = resetToSysDefault ? GocaConstants.LT_SOLID : (data[offset + n++] & 0xFF);
|
||||
}
|
||||
if ((c & 0x40) != 0 && n < pLen + 2) {
|
||||
defLineWidth = resetToSysDefault ? GocaConstants.LW_NORMAL : (data[offset + n++] & 0xFF);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 4: { // Pattern Defaults
|
||||
if ((c & 0x80) != 0 && n < pLen + 2) {
|
||||
defPattern = resetToSysDefault ? GocaConstants.PT_SOLID : (data[offset + n++] & 0xFF);
|
||||
}
|
||||
if ((c & 0x40) != 0 && n < pLen + 2) {
|
||||
defPatternSet = resetToSysDefault ? 0 : (data[offset + n++] & 0xFF);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
logger.info(String.format("GOCA P_SCUDEF: type=%d defColor=%d defFmix=%d defLineType=%d defLineWidth=%d defPattern=%d",
|
||||
type, defColorIndex, defFmix, defLineType, defLineWidth, defPattern));
|
||||
}
|
||||
|
||||
public synchronized int getDefColorIndex() { return defColorIndex; }
|
||||
public synchronized int getDefFmix() { return defFmix; }
|
||||
public synchronized int getDefLineType() { return defLineType; }
|
||||
public synchronized int getDefLineWidth() { return defLineWidth; }
|
||||
public synchronized int getDefPattern() { return defPattern; }
|
||||
|
||||
private void beginArea(boolean drawBoundary) {
|
||||
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
|
||||
}
|
||||
@@ -1488,11 +1639,11 @@ public class GocaDecoder {
|
||||
if (textLen <= 0) return;
|
||||
|
||||
// IBM 3179G vector graphics base cell is 9x16
|
||||
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
|
||||
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
|
||||
double cw = charWidth > 0 ? (charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
|
||||
double ch = charHeight > 0 ? (charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
|
||||
|
||||
int cellW = (charWidth > 0 ? charWidth : 9);
|
||||
int cellH = (charHeight > 0 ? charHeight : 16);
|
||||
int cellW = (int) Math.round(charWidth > 0 ? charWidth : 9.0);
|
||||
int cellH = (int) Math.round(charHeight > 0 ? charHeight : 16.0);
|
||||
switch (charDir) {
|
||||
case GocaConstants.CD_TB:
|
||||
trackPoint(startX, startY);
|
||||
@@ -1547,7 +1698,7 @@ public class GocaDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
startX += (charWidth > 0 ? charWidth : 9);
|
||||
startX += (int) Math.round(charWidth > 0 ? charWidth : 9.0);
|
||||
}
|
||||
curX = startX;
|
||||
curY = startY;
|
||||
@@ -1565,20 +1716,20 @@ public class GocaDecoder {
|
||||
switch (charDir) {
|
||||
case GocaConstants.CD_TB:
|
||||
curX = startX;
|
||||
curY = startY - (textLen * (charHeight > 0 ? charHeight : 16));
|
||||
curY = startY - (int) Math.round(textLen * (charHeight > 0 ? charHeight : 16.0));
|
||||
break;
|
||||
case GocaConstants.CD_RL:
|
||||
curX = startX - (textLen * (charWidth > 0 ? charWidth : 9));
|
||||
curX = startX - (int) Math.round(textLen * (charWidth > 0 ? charWidth : 9.0));
|
||||
curY = startY;
|
||||
break;
|
||||
case GocaConstants.CD_BT:
|
||||
curX = startX;
|
||||
curY = startY + (textLen * (charHeight > 0 ? charHeight : 16));
|
||||
curY = startY + (int) Math.round(textLen * (charHeight > 0 ? charHeight : 16.0));
|
||||
break;
|
||||
case GocaConstants.CD_LR:
|
||||
case GocaConstants.CD_DEFAULT:
|
||||
default:
|
||||
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
|
||||
curX = startX + (int) Math.round(textLen * (charWidth > 0 ? charWidth : 9.0));
|
||||
curY = startY;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -12,9 +9,9 @@ import java.util.logging.Logger;
|
||||
/**
|
||||
* Offscreen rendering surface for GOCA vector graphics.
|
||||
* Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix.
|
||||
* Pure Java software rasterizer compatible with standard Java SE (Swing) and Android (Bitmap).
|
||||
* Pure Java software rasterizer compatible with standard Java SE, Android, and headless environments.
|
||||
*/
|
||||
public class GraphicsPlane {
|
||||
public class GraphicsPlane implements PixelBuffer {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName());
|
||||
|
||||
@@ -100,20 +97,118 @@ public class GraphicsPlane {
|
||||
drawLine((double) x1, (double) y1, (double) x2, (double) y2, currentColorArgb, currentLineType, currentLineWidth);
|
||||
}
|
||||
|
||||
public synchronized BufferedImage toBufferedImage() {
|
||||
BufferedImage img = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
if (rgbBuffer != null) {
|
||||
img.setRGB(0, 0, canvasWidth, canvasHeight, rgbBuffer, 0, canvasWidth);
|
||||
@Override
|
||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, int argb) {
|
||||
drawLine((double) x1, (double) y1, (double) x2, (double) y2, argb, currentLineType, currentLineWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getHeight() {
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int[] getPixels() {
|
||||
return rgbBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void fillRect(int x, int y, int width, int height, int argb) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
int x1 = Math.max(0, x);
|
||||
int y1 = Math.max(0, y);
|
||||
int x2 = Math.min(canvasWidth, x + width);
|
||||
int y2 = Math.min(canvasHeight, y + height);
|
||||
for (int cy = y1; cy < y2; cy++) {
|
||||
for (int cx = x1; cx < x2; cx++) {
|
||||
setPixel(cx, cy, argb);
|
||||
}
|
||||
}
|
||||
return img;
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized Image getImage() {
|
||||
return toBufferedImage();
|
||||
@Override
|
||||
public synchronized void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY) {
|
||||
if (srcPixels == null || srcW <= 0 || srcH <= 0) return;
|
||||
for (int r = 0; r < srcH; r++) {
|
||||
int sy = srcY + r;
|
||||
int dy = dstY + r;
|
||||
if (dy < 0 || dy >= canvasHeight) continue;
|
||||
|
||||
for (int c = 0; c < srcW; c++) {
|
||||
int sx = srcX + c;
|
||||
int dx = dstX + c;
|
||||
if (dx < 0 || dx >= canvasWidth) continue;
|
||||
if (isClipped(dx, dy)) continue;
|
||||
|
||||
int idx = sy * srcW + sx;
|
||||
if (idx < srcPixels.length) {
|
||||
setPixel(dx, dy, srcPixels[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized Graphics getGraphics() {
|
||||
return toBufferedImage().getGraphics();
|
||||
public synchronized void copyPixels(int[] srcPixels, int srcOffset, int srcScan, int dstX, int dstY, int width, int height) {
|
||||
if (srcPixels == null || width <= 0 || height <= 0) return;
|
||||
for (int r = 0; r < height; r++) {
|
||||
int cy = dstY + r;
|
||||
if (cy < 0 || cy >= canvasHeight) continue;
|
||||
int srcRowStart = srcOffset + r * srcScan;
|
||||
for (int c = 0; c < width; c++) {
|
||||
int cx = dstX + c;
|
||||
if (cx < 0 || cx >= canvasWidth) continue;
|
||||
int p = srcPixels[srcRowStart + c];
|
||||
setPixel(cx, cy, p);
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setClip(int x, int y, int width, int height) {
|
||||
if (width < 0 || height < 0) {
|
||||
clearClip();
|
||||
} else {
|
||||
this.clipPixelXMin = x;
|
||||
this.clipPixelYMin = y;
|
||||
this.clipPixelXMax = x + width;
|
||||
this.clipPixelYMax = y + height;
|
||||
this.viewingWindowActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clearClip() {
|
||||
this.clipPixelXMin = 0;
|
||||
this.clipPixelYMin = 0;
|
||||
this.clipPixelXMax = canvasWidth;
|
||||
this.clipPixelYMax = canvasHeight;
|
||||
this.viewingWindowActive = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClipped(int x, int y) {
|
||||
if (!viewingWindowActive) {
|
||||
return x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight;
|
||||
}
|
||||
return x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax;
|
||||
}
|
||||
|
||||
public synchronized Rectangle getClip() {
|
||||
if (!viewingWindowActive) {
|
||||
return new Rectangle(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
return new Rectangle(clipPixelXMin, clipPixelYMin, clipPixelXMax - clipPixelXMin, clipPixelYMax - clipPixelYMin);
|
||||
}
|
||||
|
||||
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||
@@ -214,11 +309,18 @@ public class GraphicsPlane {
|
||||
updateViewingWindowPixels();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
clear(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear(int argb) {
|
||||
if (rgbBuffer != null) {
|
||||
Arrays.fill(rgbBuffer, 0);
|
||||
Arrays.fill(rgbBuffer, argb);
|
||||
}
|
||||
hasContent = false;
|
||||
this.currentMixMode = 0;
|
||||
hasContent = (argb != 0);
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
@@ -316,9 +418,27 @@ public class GraphicsPlane {
|
||||
return transform;
|
||||
}
|
||||
|
||||
// Character cell dimensions matching Query Reply presentation space
|
||||
private int charWidth = 9;
|
||||
private int charHeight = 16;
|
||||
|
||||
public synchronized void setCharDimensions(int width, int height) {
|
||||
if (width > 0) this.charWidth = width;
|
||||
if (height > 0) this.charHeight = height;
|
||||
this.transform.setDefaultCharMetrics(this.charWidth, this.charHeight);
|
||||
}
|
||||
|
||||
public int getCharWidth() {
|
||||
return charWidth;
|
||||
}
|
||||
|
||||
public int getCharHeight() {
|
||||
return charHeight;
|
||||
}
|
||||
|
||||
public int getTotalWidth() {
|
||||
int cols = screenCols > 0 ? screenCols : 80;
|
||||
return cols * 9;
|
||||
return cols * charWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,7 +475,7 @@ public class GraphicsPlane {
|
||||
*/
|
||||
public int getTotalHeight() {
|
||||
int rows = screenRows > 0 ? screenRows : 24;
|
||||
return rows * 16;
|
||||
return rows * charHeight;
|
||||
}
|
||||
|
||||
public int getXMax() {
|
||||
@@ -426,8 +546,28 @@ public class GraphicsPlane {
|
||||
return yMax - ny;
|
||||
}
|
||||
|
||||
public synchronized int getPixel(int x, int y) {
|
||||
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
||||
return rgbBuffer[y * canvasWidth + x];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public synchronized void setPixelDirect(int x, int y, int colorArgb) {
|
||||
if (viewingWindowActive) {
|
||||
if (x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
||||
rgbBuffer[y * canvasWidth + x] = colorArgb;
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
|
||||
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending in Paint Mode.
|
||||
*/
|
||||
public synchronized void setPixel(int x, int y, int colorArgb) {
|
||||
if (viewingWindowActive) {
|
||||
@@ -436,14 +576,26 @@ public class GraphicsPlane {
|
||||
}
|
||||
}
|
||||
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
|
||||
if (currentMixMode == GocaConstants.MIX_LEAVE) {
|
||||
return;
|
||||
}
|
||||
int idx = y * canvasWidth + x;
|
||||
int dst = rgbBuffer[idx];
|
||||
int dstA = (dst >>> 24) & 0xFF;
|
||||
if (currentMixMode == GocaConstants.MIX_UNDER && dstA != 0) {
|
||||
return;
|
||||
}
|
||||
if (currentMixMode == GocaConstants.MIX_XOR) {
|
||||
rgbBuffer[idx] = 0xFF000000 | (dst ^ colorArgb);
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
return;
|
||||
}
|
||||
int srcA = (colorArgb >>> 24) & 0xFF;
|
||||
if (srcA == 0) return;
|
||||
int idx = y * canvasWidth + x;
|
||||
if (srcA == 255) {
|
||||
rgbBuffer[idx] = colorArgb;
|
||||
} else {
|
||||
int dst = rgbBuffer[idx];
|
||||
int dstA = (dst >>> 24) & 0xFF;
|
||||
if (dstA == 0) {
|
||||
rgbBuffer[idx] = colorArgb;
|
||||
} else {
|
||||
@@ -481,110 +633,95 @@ public class GraphicsPlane {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an anti-aliased line using Xiaolin Wu's algorithm with sub-pixel double coordinates.
|
||||
* Standard integer Bresenham line algorithm matching IBM 3179G / Host On-Demand 1-pixel rasterization.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) {
|
||||
int dx = Math.abs(x1 - x0);
|
||||
int dy = Math.abs(y1 - y0);
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
int curX = x0;
|
||||
int curY = y0;
|
||||
|
||||
while (true) {
|
||||
setPixel(curX, curY, color);
|
||||
if (curX == x1 && curY == y1) break;
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
curX += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
curY += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parallel line drawing algorithm matching IBM Host On-Demand HODGraphUtil.drawHODLine.
|
||||
*/
|
||||
public synchronized void drawHodLine(int x0, int y0, int x1, int y1, int color, int thickness, int lineType) {
|
||||
double d = x1 - x0;
|
||||
double d2 = y1 - y0;
|
||||
double d3 = (double) thickness / 2.0;
|
||||
double d4 = (x0 == x1) ? Math.PI : Math.atan(d2 / d) + Math.PI / 2.0;
|
||||
double d5 = Math.cos(d4);
|
||||
double d6 = Math.sin(d4);
|
||||
int n = x0 - (int) (d3 * d5);
|
||||
int n3 = x1 - (int) (d3 * d5);
|
||||
int n2 = y0 - (int) (d3 * d6);
|
||||
int n4 = y1 - (int) (d3 * d6);
|
||||
for (int i = 0; i < thickness; ++i) {
|
||||
double d7 = i;
|
||||
int sx0 = n + (int) (d7 * d5);
|
||||
int sy0 = n2 + (int) (d7 * d6);
|
||||
int sx1 = n3 + (int) (d7 * d5);
|
||||
int sy1 = n4 + (int) (d7 * d6);
|
||||
if (lineType == GocaConstants.LT_SOLID || lineType == GocaConstants.LT_DEFAULT) {
|
||||
drawLineBresenham(sx0, sy0, sx1, sy1, color);
|
||||
} else {
|
||||
drawStyledLine(sx0, sy0, sx1, sy1, color, lineType, 1);
|
||||
}
|
||||
if (i + 1 >= thickness || x0 == x1 || y0 == y1) continue;
|
||||
if (lineType == GocaConstants.LT_SOLID || lineType == GocaConstants.LT_DEFAULT) {
|
||||
drawLineBresenham(sx0 + 1, sy0, sx1 + 1, sy1, color);
|
||||
} else {
|
||||
drawStyledLine(sx0 + 1, sy0, sx1 + 1, sy1, color, lineType, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a line matching IBM 3179G / Host On-Demand rasterization.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
|
||||
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : 0x00FFFFFF;
|
||||
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : (GocaConstants.GOCA_COLORS[0] & 0x00FFFFFF);
|
||||
int ix0 = (int) Math.round(x0);
|
||||
int iy0 = (int) Math.round(y0);
|
||||
int ix1 = (int) Math.round(x1);
|
||||
int iy1 = (int) Math.round(y1);
|
||||
|
||||
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
|
||||
drawStyledLine((int) Math.round(x0), (int) Math.round(y0),
|
||||
(int) Math.round(x1), (int) Math.round(y1),
|
||||
(0xFF << 24) | color, lineType, lineWidth);
|
||||
return;
|
||||
}
|
||||
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
|
||||
|
||||
// Special case: single point or zero-length line
|
||||
if (Math.abs(x1 - x0) < 1e-5 && Math.abs(y1 - y0) < 1e-5) {
|
||||
drawPixelWithThickness((int) Math.round(x0), (int) Math.round(y0), (0xFF << 24) | color, (lineWidth == GocaConstants.LW_THICK) ? 2 : 1);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
|
||||
if (steep) {
|
||||
double tmp = x0; x0 = y0; y0 = tmp;
|
||||
tmp = x1; x1 = y1; y1 = tmp;
|
||||
}
|
||||
if (x0 > x1) {
|
||||
double tmp = x0; x0 = x1; x1 = tmp;
|
||||
tmp = y0; y0 = y1; y1 = tmp;
|
||||
}
|
||||
|
||||
double dx = x1 - x0;
|
||||
double dy = y1 - y0;
|
||||
double gradient = (dx == 0.0) ? 1.0 : (dy / dx);
|
||||
|
||||
// First endpoint
|
||||
double xend = Math.round(x0);
|
||||
double yend = y0 + gradient * (xend - x0);
|
||||
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
|
||||
int xpxl1 = (int) xend;
|
||||
int ypxl1 = (int) Math.floor(yend);
|
||||
|
||||
if (steep) {
|
||||
plotPixelWu(ypxl1, xpxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(ypxl1 + 1, xpxl1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
} else {
|
||||
plotPixelWu(xpxl1, ypxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(xpxl1, ypxl1 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
}
|
||||
double intery = yend + gradient;
|
||||
|
||||
// Second endpoint
|
||||
xend = Math.round(x1);
|
||||
yend = y1 + gradient * (xend - x1);
|
||||
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
|
||||
int xpxl2 = (int) xend;
|
||||
int ypxl2 = (int) Math.floor(yend);
|
||||
|
||||
if (steep) {
|
||||
plotPixelWu(ypxl2, xpxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(ypxl2 + 1, xpxl2, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
} else {
|
||||
plotPixelWu(xpxl2, ypxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
|
||||
plotPixelWu(xpxl2, ypxl2 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
|
||||
}
|
||||
|
||||
// Main anti-aliased stepping loop
|
||||
if (steep) {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int y = (int) Math.floor(intery);
|
||||
double frac = intery - y;
|
||||
plotPixelWu(y, x, color, 1.0 - frac, lineWidth);
|
||||
plotPixelWu(y + 1, x, color, frac, lineWidth);
|
||||
intery += gradient;
|
||||
if (thickness <= 1) {
|
||||
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
|
||||
drawStyledLine(ix0, iy0, ix1, iy1, (0xFF << 24) | color, lineType, 1);
|
||||
} else {
|
||||
drawLineBresenham(ix0, iy0, ix1, iy1, (0xFF << 24) | color);
|
||||
}
|
||||
} else {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int y = (int) Math.floor(intery);
|
||||
double frac = intery - y;
|
||||
plotPixelWu(x, y, color, 1.0 - frac, lineWidth);
|
||||
plotPixelWu(x, y + 1, color, frac, lineWidth);
|
||||
intery += gradient;
|
||||
}
|
||||
drawHodLine(ix0, iy0, ix1, iy1, (0xFF << 24) | color, thickness, lineType);
|
||||
}
|
||||
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
|
||||
if (brightness <= 0.0) return;
|
||||
if (lineWidth == GocaConstants.LW_THICK || fractionalLineWidth >= 1.5) {
|
||||
int extra = (int) Math.round(Math.max(1, fractionalLineWidth - 0.5));
|
||||
setPixelCoverage(x, y, colorRgb, 1.0);
|
||||
for (int dx = -extra; dx <= extra; dx++) {
|
||||
for (int dy = -extra; dy <= extra; dy++) {
|
||||
if (dx == 0 && dy == 0) continue;
|
||||
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Perceptual gamma correction for crisp contrast on dark backgrounds
|
||||
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
|
||||
setPixelCoverage(x, y, colorRgb, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an absolute or relative line using anti-aliasing for smooth vectors.
|
||||
*/
|
||||
@@ -592,6 +729,111 @@ public class GraphicsPlane {
|
||||
drawLine((double) x1, (double) y1, (double) x2, (double) y2, colorArgb, lineType, lineWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int argb, double strokeWidth) {
|
||||
drawLineAA(x0, y0, x1, y1, argb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an anti-aliased line using Xiaolin Wu's algorithm directly into the pixel buffer.
|
||||
* Pure Java implementation replacing AWT Graphics2D rendering for sub-pixel vector strokes.
|
||||
*/
|
||||
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int color) {
|
||||
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
|
||||
if (steep) {
|
||||
double t = x0; x0 = y0; y0 = t;
|
||||
t = x1; x1 = y1; y1 = t;
|
||||
}
|
||||
if (x0 > x1) {
|
||||
double t = x0; x0 = x1; x1 = t;
|
||||
t = y0; y0 = y1; y1 = t;
|
||||
}
|
||||
|
||||
double dx = x1 - x0;
|
||||
double dy = y1 - y0;
|
||||
double gradient = (dx == 0.0) ? 1.0 : dy / dx;
|
||||
|
||||
// Handle first endpoint
|
||||
double xend = Math.round(x0);
|
||||
double yend = y0 + gradient * (xend - x0);
|
||||
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
|
||||
int xpxl1 = (int) xend;
|
||||
int ypxl1 = (int) Math.floor(yend);
|
||||
if (steep) {
|
||||
plotAA(ypxl1, xpxl1, (1.0 - (yend - Math.floor(yend))) * xgap, color);
|
||||
plotAA(ypxl1 + 1, xpxl1, (yend - Math.floor(yend)) * xgap, color);
|
||||
} else {
|
||||
plotAA(xpxl1, ypxl1, (1.0 - (yend - Math.floor(yend))) * xgap, color);
|
||||
plotAA(xpxl1, ypxl1 + 1, (yend - Math.floor(yend)) * xgap, color);
|
||||
}
|
||||
double intery = yend + gradient;
|
||||
|
||||
// Handle second endpoint
|
||||
xend = Math.round(x1);
|
||||
yend = y1 + gradient * (xend - x1);
|
||||
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
|
||||
int xpxl2 = (int) xend;
|
||||
int ypxl2 = (int) Math.floor(yend);
|
||||
if (steep) {
|
||||
plotAA(ypxl2, xpxl2, (1.0 - (yend - Math.floor(yend))) * xgap, color);
|
||||
plotAA(ypxl2 + 1, xpxl2, (yend - Math.floor(yend)) * xgap, color);
|
||||
} else {
|
||||
plotAA(xpxl2, ypxl2, (1.0 - (yend - Math.floor(yend))) * xgap, color);
|
||||
plotAA(xpxl2, ypxl2 + 1, (yend - Math.floor(yend)) * xgap, color);
|
||||
}
|
||||
|
||||
// Main loop
|
||||
if (steep) {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int iy = (int) Math.floor(intery);
|
||||
double fpart = intery - iy;
|
||||
plotAA(iy, x, 1.0 - fpart, color);
|
||||
plotAA(iy + 1, x, fpart, color);
|
||||
intery += gradient;
|
||||
}
|
||||
} else {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int iy = (int) Math.floor(intery);
|
||||
double fpart = intery - iy;
|
||||
plotAA(x, iy, 1.0 - fpart, color);
|
||||
plotAA(x, iy + 1, fpart, color);
|
||||
intery += gradient;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
private void plotAA(int x, int y, double brightness, int color) {
|
||||
if (x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight || brightness <= 0.0) return;
|
||||
if (brightness > 1.0) brightness = 1.0;
|
||||
int sa = (color >>> 24) & 0xFF;
|
||||
if (sa == 0) sa = 0xFF;
|
||||
int alpha = (int) Math.round(sa * brightness);
|
||||
if (alpha <= 0) return;
|
||||
|
||||
int sr = (color >>> 16) & 0xFF;
|
||||
int sg = (color >>> 8) & 0xFF;
|
||||
int sb = color & 0xFF;
|
||||
|
||||
int idx = y * canvasWidth + x;
|
||||
int dst = rgbBuffer[idx];
|
||||
int da = (dst >>> 24) & 0xFF;
|
||||
if (da == 0) {
|
||||
rgbBuffer[idx] = (alpha << 24) | (sr << 16) | (sg << 8) | sb;
|
||||
} else {
|
||||
int dr = (dst >>> 16) & 0xFF;
|
||||
int dg = (dst >>> 8) & 0xFF;
|
||||
int db = dst & 0xFF;
|
||||
int invA = 255 - alpha;
|
||||
int outR = (sr * alpha + dr * invA) / 255;
|
||||
int outG = (sg * alpha + dg * invA) / 255;
|
||||
int outB = (sb * alpha + db * invA) / 255;
|
||||
int outA = Math.min(255, da + alpha);
|
||||
rgbBuffer[idx] = (outA << 24) | (outR << 16) | (outG << 8) | outB;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
|
||||
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
|
||||
@@ -657,9 +899,15 @@ public class GraphicsPlane {
|
||||
private void drawPixelWithThickness(int x, int y, int color, int thickness) {
|
||||
if (thickness <= 1) {
|
||||
setPixel(x, y, color);
|
||||
} else if (thickness == 2) {
|
||||
setPixel(x, y, color);
|
||||
setPixel(x + 1, y, color);
|
||||
setPixel(x, y + 1, color);
|
||||
setPixel(x + 1, y + 1, color);
|
||||
} else {
|
||||
for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) {
|
||||
for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) {
|
||||
int r = thickness / 2;
|
||||
for (int dy = -r; dy <= r; dy++) {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
setPixel(x + dx, y + dy, color);
|
||||
}
|
||||
}
|
||||
@@ -796,6 +1044,32 @@ public class GraphicsPlane {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
|
||||
*/
|
||||
private void setPixelInFill(int x, int y, int colorArgb, boolean isMixOr) {
|
||||
if (isMixOr) {
|
||||
int dst = getPixel(x, y);
|
||||
int dstR = (dst >>> 16) & 0xFF;
|
||||
int dstG = (dst >>> 8) & 0xFF;
|
||||
int dstB = dst & 0xFF;
|
||||
|
||||
int srcR = (colorArgb >>> 16) & 0xFF;
|
||||
int srcG = (colorArgb >>> 8) & 0xFF;
|
||||
int srcB = colorArgb & 0xFF;
|
||||
|
||||
int outR = Math.min(255, dstR | srcR);
|
||||
int outG = Math.min(255, dstG | srcG);
|
||||
int outB = Math.min(255, dstB | srcB);
|
||||
int outA = Math.max((dst >>> 24) & 0xFF, (colorArgb >>> 24) & 0xFF);
|
||||
if (outA == 0 && (outR != 0 || outG != 0 || outB != 0)) outA = 255;
|
||||
|
||||
setPixelDirect(x, y, (outA << 24) | (outR << 16) | (outG << 8) | outB);
|
||||
} else {
|
||||
setPixel(x, y, colorArgb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
|
||||
*/
|
||||
@@ -807,19 +1081,23 @@ public class GraphicsPlane {
|
||||
|
||||
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int bg = bgColorArgb;
|
||||
|
||||
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
|
||||
(bgMix == GocaConstants.BMX_DEFAULT || bgMix == GocaConstants.BMX_TRANSPARENT ||
|
||||
bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0 || bgMix == 2);
|
||||
boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1);
|
||||
|
||||
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
|
||||
int minY = py[0];
|
||||
int maxY = py[0];
|
||||
for (int i = 1; i < numPoints; i++) {
|
||||
if (py[i] < minY) minY = py[i];
|
||||
if (py[i] > maxY) maxY = py[i];
|
||||
}
|
||||
int minY = py[0];
|
||||
int maxY = py[0];
|
||||
int minX = px[0];
|
||||
int maxX = px[0];
|
||||
for (int i = 1; i < numPoints; i++) {
|
||||
if (py[i] < minY) minY = py[i];
|
||||
if (py[i] > maxY) maxY = py[i];
|
||||
if (px[i] < minX) minX = px[i];
|
||||
if (px[i] > maxX) maxX = px[i];
|
||||
}
|
||||
int bWidth = maxX - minX + 1;
|
||||
int bHeight = maxY - minY + 1;
|
||||
boolean isMixOr = (currentMixMode == GocaConstants.MIX_OR && bWidth >= 5 && bHeight >= 5 && (bWidth * bHeight >= 100));
|
||||
|
||||
if (pattern != GocaConstants.PT_EMPTY) {
|
||||
minY = Math.max(0, minY);
|
||||
maxY = Math.min(canvasHeight - 1, maxY);
|
||||
|
||||
@@ -881,18 +1159,18 @@ public class GraphicsPlane {
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
setPixel(x, y, fill);
|
||||
setPixelInFill(x, y, fill, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
setPixel(x, y, bg);
|
||||
setPixelInFill(x, y, bg, isMixOr);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
|
||||
setPixelInFill(x, y, fill, isMixOr);
|
||||
} else {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
setPixel(x, y, fill);
|
||||
setPixelInFill(x, y, fill, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
setPixel(x, y, bg);
|
||||
setPixelInFill(x, y, bg, isMixOr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -914,18 +1192,18 @@ public class GraphicsPlane {
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
setPixel(x, y, fill);
|
||||
setPixelInFill(x, y, fill, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
setPixel(x, y, bg);
|
||||
setPixelInFill(x, y, bg, isMixOr);
|
||||
}
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
|
||||
setPixel(x, y, fill);
|
||||
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
|
||||
setPixelInFill(x, y, fill, isMixOr);
|
||||
} else {
|
||||
int b = patRows[y & 7] & 0xFF;
|
||||
if (((b >> (7 - (x & 7))) & 1) != 0) {
|
||||
setPixel(x, y, fill);
|
||||
setPixelInFill(x, y, fill, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
setPixel(x, y, bg);
|
||||
setPixelInFill(x, y, bg, isMixOr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -943,7 +1221,12 @@ public class GraphicsPlane {
|
||||
for (int i = 0; i < pLen - 1; i++) {
|
||||
drawLine((double) px[offset + i], (double) py[offset + i],
|
||||
(double) px[offset + i + 1], (double) py[offset + i + 1],
|
||||
boundaryColorArgb, lineType, lineWidth);
|
||||
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
|
||||
}
|
||||
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
|
||||
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
|
||||
(double) px[offset], (double) py[offset],
|
||||
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
|
||||
}
|
||||
}
|
||||
offset += pLen;
|
||||
@@ -1085,7 +1368,10 @@ public class GraphicsPlane {
|
||||
double curX = x;
|
||||
double curY = y;
|
||||
|
||||
double radAngle = Math.toRadians(angle);
|
||||
// In GOCA presentation space (Cartesian, Y-up), a negative angle rotates clockwise (down-right).
|
||||
// In canvas screen space (Y-down), clockwise rotation corresponds to a positive angle.
|
||||
double screenAngle = -angle;
|
||||
double radAngle = Math.toRadians(screenAngle);
|
||||
double cosA = Math.cos(radAngle);
|
||||
double sinA = Math.sin(radAngle);
|
||||
|
||||
@@ -1099,7 +1385,7 @@ public class GraphicsPlane {
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle);
|
||||
drawVssChar(curX, curY, c, color, cw, ch, screenAngle, shearAngle);
|
||||
|
||||
if (angle != 0.0) {
|
||||
curX += cw * cosA;
|
||||
@@ -1150,6 +1436,49 @@ public class GraphicsPlane {
|
||||
double sinA = Math.sin(radAngle);
|
||||
double tanShear = Math.tan(Math.toRadians(shearAngle));
|
||||
|
||||
if (ch < 6.0) {
|
||||
int ptr = offset;
|
||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||
if (order == 0xC1) {
|
||||
int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF;
|
||||
int numPoints = byteLen / 4;
|
||||
int dataPtr = ptr + 2;
|
||||
|
||||
if (numPoints >= 2) {
|
||||
double[] px = new double[numPoints];
|
||||
double[] py = new double[numPoints];
|
||||
for (int p = 0; p < numPoints; p++) {
|
||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||
|
||||
double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||
|
||||
double sx = nx - ny * tanShear;
|
||||
double sy = ny;
|
||||
|
||||
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
|
||||
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
|
||||
|
||||
px[p] = x + rx;
|
||||
py[p] = y + ry;
|
||||
}
|
||||
|
||||
for (int p = 0; p < numPoints - 1; p++) {
|
||||
drawLineAA(px[p], py[p], px[p + 1], py[p + 1], color);
|
||||
}
|
||||
}
|
||||
ptr += 2 + byteLen;
|
||||
} else {
|
||||
ptr++;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
int ptr = offset;
|
||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||
@@ -1161,9 +1490,6 @@ public class GraphicsPlane {
|
||||
if (numPoints >= 2) {
|
||||
double[] px = new double[numPoints];
|
||||
double[] py = new double[numPoints];
|
||||
int[] ipx = new int[numPoints];
|
||||
int[] ipy = new int[numPoints];
|
||||
|
||||
for (int p = 0; p < numPoints; p++) {
|
||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||
@@ -1179,19 +1505,6 @@ public class GraphicsPlane {
|
||||
|
||||
px[p] = x + rx;
|
||||
py[p] = y + ry;
|
||||
ipx[p] = (int) Math.round(px[p]);
|
||||
ipy[p] = (int) Math.round(py[p]);
|
||||
}
|
||||
|
||||
// If contour is closed (e.g. bold character loop), fill with solid color
|
||||
int firstVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
|
||||
int firstVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
|
||||
int lastVx = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 1] & 0xFF);
|
||||
int lastVy = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 3] & 0xFF);
|
||||
|
||||
boolean isClosed = (numPoints >= 4) && (firstVx == lastVx) && (firstVy == lastVy);
|
||||
if (isClosed) {
|
||||
fillArea(ipx, ipy, numPoints, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
|
||||
}
|
||||
|
||||
for (int p = 0; p < numPoints - 1; p++) {
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Image;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.image.FilteredImageSource;
|
||||
import java.awt.image.MemoryImageSource;
|
||||
|
||||
/**
|
||||
* Bitmap image container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBitImage).
|
||||
* Pure Java software implementation completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODBitImage {
|
||||
protected Component vComponent;
|
||||
protected Object vComponent;
|
||||
protected Dimension iSize = new Dimension();
|
||||
protected Dimension iScaledSize = new Dimension();
|
||||
protected int iDepth;
|
||||
protected int iScanLength;
|
||||
protected boolean _iUseGraphicColors;
|
||||
protected byte[] iScaledImageData;
|
||||
protected Image[] hImage;
|
||||
protected Image[] iScaledImage;
|
||||
protected PixelBuffer[] hImage;
|
||||
protected PixelBuffer[] iScaledImage;
|
||||
protected int transparentBG = 0;
|
||||
protected byte[] hImageData;
|
||||
protected int iBaseColor;
|
||||
|
||||
public HODBitImage(Component comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
|
||||
public HODBitImage(Object comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
|
||||
this.vComponent = comp;
|
||||
this.iSize.width = width;
|
||||
this.iSize.height = height;
|
||||
@@ -32,8 +26,8 @@ public class HODBitImage {
|
||||
this.iBaseColor = baseColor;
|
||||
this.iDepth = depth;
|
||||
this._iUseGraphicColors = useGraphicColors;
|
||||
this.hImage = new Image[depth == 1 ? 17 : 1];
|
||||
this.iScaledImage = new Image[depth == 1 ? 17 : 1];
|
||||
this.hImage = new PixelBuffer[depth == 1 ? 17 : 1];
|
||||
this.iScaledImage = new PixelBuffer[depth == 1 ? 17 : 1];
|
||||
this.buildHODImage();
|
||||
}
|
||||
|
||||
@@ -72,11 +66,19 @@ public class HODBitImage {
|
||||
return nArray;
|
||||
}
|
||||
|
||||
public Image getHODImage(int colorIdx) {
|
||||
return this.getHODImage(this.iSize.width, this.iSize.height, colorIdx);
|
||||
public Object getHODImage(int colorIdx) {
|
||||
return this.getHODPixelBuffer(this.iSize.width, this.iSize.height, colorIdx);
|
||||
}
|
||||
|
||||
public Image getHODImage(int w, int h, int colorIdx) {
|
||||
public Object getHODImage(int w, int h, int colorIdx) {
|
||||
return this.getHODPixelBuffer(w, h, colorIdx);
|
||||
}
|
||||
|
||||
public PixelBuffer getHODPixelBuffer(int colorIdx) {
|
||||
return this.getHODPixelBuffer(this.iSize.width, this.iSize.height, colorIdx);
|
||||
}
|
||||
|
||||
public PixelBuffer getHODPixelBuffer(int w, int h, int colorIdx) {
|
||||
if (w <= 0 || h <= 0) return null;
|
||||
boolean diffColor = this.iBaseColor != colorIdx && this.iDepth == 1;
|
||||
boolean matchesBase = this.iSize.width == w && this.iSize.height == h;
|
||||
@@ -88,13 +90,18 @@ public class HODBitImage {
|
||||
this.iScaledSize.height = h;
|
||||
this.scaleHODImage();
|
||||
}
|
||||
Image image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx];
|
||||
PixelBuffer image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx];
|
||||
if (image == null) {
|
||||
HODColorChangeFilter filter = new HODColorChangeFilter(this.getHODColor(colorIdx));
|
||||
Image base = matchesBase ? this.hImage[this.iBaseColor] : this.iScaledImage[this.iBaseColor];
|
||||
PixelBuffer base = matchesBase ? this.hImage[this.iBaseColor] : this.iScaledImage[this.iBaseColor];
|
||||
if (base != null) {
|
||||
FilteredImageSource source = new FilteredImageSource(base.getSource(), filter);
|
||||
image = Toolkit.getDefaultToolkit().createImage(source);
|
||||
int bw = base.getWidth();
|
||||
int bh = base.getHeight();
|
||||
int[] basePixels = base.getPixels();
|
||||
int[] filtered = new int[bw * bh];
|
||||
System.arraycopy(basePixels, 0, filtered, 0, filtered.length);
|
||||
filter.apply(filtered, 0, filtered.length);
|
||||
image = new DefaultPixelBuffer(bw, bh, filtered);
|
||||
if (matchesBase) {
|
||||
this.hImage[colorIdx] = image;
|
||||
} else {
|
||||
@@ -121,27 +128,47 @@ public class HODBitImage {
|
||||
private void buildHODImage() {
|
||||
if (this.iSize.width <= 0 || this.iSize.height <= 0) return;
|
||||
int[] pixels = getHODImageData(this.iBaseColor);
|
||||
MemoryImageSource mis = new MemoryImageSource(this.iSize.width, this.iSize.height, pixels, 0, this.iSize.width);
|
||||
Image img = Toolkit.getDefaultToolkit().createImage(mis);
|
||||
PixelBuffer buf = new DefaultPixelBuffer(this.iSize.width, this.iSize.height, pixels);
|
||||
if (this.iDepth == 1) {
|
||||
this.hImage[this.iBaseColor] = img;
|
||||
this.hImage[this.iBaseColor] = buf;
|
||||
} else {
|
||||
this.hImage[0] = img;
|
||||
this.hImage[0] = buf;
|
||||
}
|
||||
}
|
||||
|
||||
private void scaleHODImage() {
|
||||
if (this.iScaledSize.width <= 0 || this.iScaledSize.height <= 0) return;
|
||||
Image base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0];
|
||||
int sw = this.iScaledSize.width;
|
||||
int sh = this.iScaledSize.height;
|
||||
if (sw <= 0 || sh <= 0) return;
|
||||
PixelBuffer base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0];
|
||||
if (base != null) {
|
||||
this.iScaledImage[this.iDepth == 1 ? this.iBaseColor : 0] =
|
||||
base.getScaledInstance(this.iScaledSize.width, this.iScaledSize.height, Image.SCALE_FAST);
|
||||
int bw = base.getWidth();
|
||||
int bh = base.getHeight();
|
||||
int[] src = base.getPixels();
|
||||
int[] dst = new int[sw * sh];
|
||||
for (int dy = 0; dy < sh; dy++) {
|
||||
int sy = dy * bh / sh;
|
||||
int srcOffset = sy * bw;
|
||||
int dstOffset = dy * sw;
|
||||
for (int dx = 0; dx < sw; dx++) {
|
||||
int sx = dx * bw / sw;
|
||||
dst[dstOffset + dx] = src[srcOffset + sx];
|
||||
}
|
||||
}
|
||||
this.iScaledImage[this.iDepth == 1 ? this.iBaseColor : 0] = new DefaultPixelBuffer(sw, sh, dst);
|
||||
}
|
||||
}
|
||||
|
||||
private int getHODColor(int idx) {
|
||||
if (idx == 0 && this.vComponent != null) {
|
||||
return this.vComponent.getBackground().getRGB();
|
||||
try {
|
||||
java.lang.reflect.Method m = this.vComponent.getClass().getMethod("getBackground");
|
||||
Object bg = m.invoke(this.vComponent);
|
||||
if (bg != null) {
|
||||
java.lang.reflect.Method mRgb = bg.getClass().getMethod("getRGB");
|
||||
return ((Number) mRgb.invoke(bg)).intValue();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return GocaConstants.getGocaColorArgb(idx);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
|
||||
/**
|
||||
* Bounding box encapsulation matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBounds).
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.RGBImageFilter;
|
||||
|
||||
/**
|
||||
* Image filter that replaces occurrences of one color with another color.
|
||||
* Conforms to IBM Host On-Demand HODColorChangeFilter.
|
||||
* Platform-neutral implementation independent of java.awt.
|
||||
*/
|
||||
public class HODColorChangeFilter extends RGBImageFilter {
|
||||
public class HODColorChangeFilter {
|
||||
|
||||
protected boolean canFilterIndexColorModel = true;
|
||||
private int oldRgb;
|
||||
private int newRgb;
|
||||
|
||||
public HODColorChangeFilter(int newRgb) {
|
||||
this.canFilterIndexColorModel = true;
|
||||
this.oldRgb = -1;
|
||||
this.newRgb = newRgb | 0xFF000000;
|
||||
}
|
||||
@@ -23,7 +21,6 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
||||
}
|
||||
|
||||
public HODColorChangeFilter(int oldRgb, int newRgb) {
|
||||
this.canFilterIndexColorModel = true;
|
||||
this.oldRgb = oldRgb & 0x00FFFFFF;
|
||||
this.newRgb = newRgb;
|
||||
}
|
||||
@@ -48,7 +45,6 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
||||
this.newRgb = newRgb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int filterRGB(int x, int y, int rgb) {
|
||||
if (oldRgb == -1) {
|
||||
if ((rgb & 0xFF000000) != 0) {
|
||||
@@ -61,4 +57,20 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
||||
}
|
||||
return rgb;
|
||||
}
|
||||
|
||||
public void apply(int[] pixels, int offset, int length) {
|
||||
if (pixels == null) return;
|
||||
int end = Math.min(pixels.length, offset + length);
|
||||
for (int i = offset; i < end; i++) {
|
||||
pixels[i] = filterRGB(0, 0, pixels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void apply(PixelBuffer buffer) {
|
||||
if (buffer == null) return;
|
||||
int[] pixels = buffer.getPixels();
|
||||
if (pixels != null) {
|
||||
apply(pixels, 0, pixels.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.Point;
|
||||
import java.awt.Polygon;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* Headless graphics plane facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODGraphicsPlane).
|
||||
* Pure Java software implementation completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODGraphicsPlane {
|
||||
private final GraphicsPlane delegate;
|
||||
@@ -40,6 +32,10 @@ public class HODGraphicsPlane {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public PixelBuffer getPixelBuffer() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public void resize(Dimension dim, boolean keepContent) {
|
||||
if (dim != null) {
|
||||
delegate.setDimensions(dim.width, dim.height);
|
||||
@@ -52,15 +48,19 @@ public class HODGraphicsPlane {
|
||||
this.bounds.set(0, 0, delegate.getCanvasWidth(), delegate.getCanvasHeight());
|
||||
}
|
||||
|
||||
public Graphics getHODGraphics() {
|
||||
return delegate.getGraphics();
|
||||
public Object getHODGraphics() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public Image getHODImage() {
|
||||
return delegate.getImage();
|
||||
public Object getHODImage() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public void setHODTemporaryGraphics(Graphics g) {
|
||||
public PixelBuffer getHODPixelBuffer() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public void setHODTemporaryGraphics(Object g) {
|
||||
// No-op or temporary override
|
||||
}
|
||||
|
||||
@@ -116,35 +116,32 @@ public class HODGraphicsPlane {
|
||||
}
|
||||
|
||||
public void drawHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||
Graphics g = delegate.getGraphics();
|
||||
if (g != null) {
|
||||
g.setColor(currentColor);
|
||||
g.drawArc(x, y, width, height, startAngle, arcAngle);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + width, y + height);
|
||||
}
|
||||
double rx = width / 2.0;
|
||||
double ry = height / 2.0;
|
||||
double cx = x + rx;
|
||||
double cy = y + ry;
|
||||
delegate.drawArc(cx, cy, rx, ry, startAngle, arcAngle, currentColor.getRGB(), currentLineType, currentLineWidth, false);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + width, y + height);
|
||||
}
|
||||
|
||||
public void fillHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||
Graphics g = delegate.getGraphics();
|
||||
if (g != null) {
|
||||
g.setColor(currentColor);
|
||||
g.fillArc(x, y, width, height, startAngle, arcAngle);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + width, y + height);
|
||||
}
|
||||
double rx = width / 2.0;
|
||||
double ry = height / 2.0;
|
||||
double cx = x + rx;
|
||||
double cy = y + ry;
|
||||
delegate.drawArc(cx, cy, rx, ry, startAngle, arcAngle, currentColor.getRGB(), currentLineType, currentLineWidth, true);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + width, y + height);
|
||||
}
|
||||
|
||||
public void drawHODImage(HODBitImage bitImage, int x, int y, int w, int h) {
|
||||
if (bitImage != null) {
|
||||
Image img = bitImage.getHODImage(w, h, currentColorIndex);
|
||||
if (img != null) {
|
||||
Graphics g = delegate.getGraphics();
|
||||
if (g != null) {
|
||||
g.drawImage(img, x, y, null);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + w, y + h);
|
||||
}
|
||||
PixelBuffer buf = bitImage.getHODPixelBuffer(w, h, currentColorIndex);
|
||||
if (buf != null) {
|
||||
delegate.blit(buf.getPixels(), 0, 0, buf.getWidth(), buf.getHeight(), x, y);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + w, y + h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Rectangle;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Visual part container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODPart).
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODPart extends Rectangle implements Serializable {
|
||||
protected Component hodParent;
|
||||
protected Font _hodFont;
|
||||
protected Object hodParent;
|
||||
protected Object _hodFont;
|
||||
protected Color foregroundColor;
|
||||
protected Color backgroundColor;
|
||||
protected Boolean isTransparent;
|
||||
@@ -22,33 +17,36 @@ public class HODPart extends Rectangle implements Serializable {
|
||||
|
||||
protected HODPart() {}
|
||||
|
||||
public HODPart(Component component) {
|
||||
public HODPart(Object component) {
|
||||
this();
|
||||
this.setHODParent(component);
|
||||
}
|
||||
|
||||
public HODPart(Component component, Dimension dimension) {
|
||||
super(dimension);
|
||||
public HODPart(Object component, Dimension dimension) {
|
||||
super(0, 0, dimension != null ? dimension.width : 0, dimension != null ? dimension.height : 0);
|
||||
this.setHODParent(component);
|
||||
}
|
||||
|
||||
public HODPart(Component component, Rectangle rectangle) {
|
||||
super(rectangle);
|
||||
public HODPart(Object component, Rectangle rectangle) {
|
||||
super(rectangle != null ? rectangle.x : 0, rectangle != null ? rectangle.y : 0,
|
||||
rectangle != null ? rectangle.width : 0, rectangle != null ? rectangle.height : 0);
|
||||
this.setHODParent(component);
|
||||
}
|
||||
|
||||
public HODPart(HODPart hODPart) {
|
||||
this(hODPart.getHODParent(), hODPart.getSize());
|
||||
this.setHODBackground(hODPart.getHODBackground());
|
||||
this.setHODForeground(hODPart.getHODForeground());
|
||||
this.setHODFont(hODPart.getHODFont());
|
||||
this(hODPart != null ? hODPart.getHODParent() : null, hODPart != null ? hODPart.getSize() : null);
|
||||
if (hODPart != null) {
|
||||
this.setHODBackground(hODPart.getHODBackground());
|
||||
this.setHODForeground(hODPart.getHODForeground());
|
||||
this.setHODFont(hODPart.getHODFont());
|
||||
}
|
||||
}
|
||||
|
||||
public Component getHODParent() {
|
||||
public Object getHODParent() {
|
||||
return this.hodParent;
|
||||
}
|
||||
|
||||
public void setHODParent(Component component) {
|
||||
public void setHODParent(Object component) {
|
||||
if (component != null && !component.equals(this.hodParent)) {
|
||||
this.hodParent = component;
|
||||
}
|
||||
@@ -56,18 +54,28 @@ public class HODPart extends Rectangle implements Serializable {
|
||||
|
||||
public void repaint() {
|
||||
if (this.hodParent != null) {
|
||||
this.hodParent.repaint(this.x, this.y, this.width, this.height);
|
||||
try {
|
||||
Method m = this.hodParent.getClass().getMethod("repaint", int.class, int.class, int.class, int.class);
|
||||
m.invoke(this.hodParent, this.x, this.y, this.width, this.height);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void paint(Component component, Graphics graphics, int x, int y, int w, int h) {
|
||||
public void paint(Object component, Object graphics, int x, int y, int w, int h) {
|
||||
this.setBounds(x, y, w, h);
|
||||
if (Boolean.TRUE.equals(this._visible)) {
|
||||
this.paintHODView(graphics);
|
||||
}
|
||||
}
|
||||
|
||||
protected void paintHODView(Graphics graphics) {}
|
||||
public void paint(PixelBuffer buffer, int x, int y, int w, int h) {
|
||||
this.setBounds(x, y, w, h);
|
||||
if (Boolean.TRUE.equals(this._visible)) {
|
||||
this.paintHODView(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
protected void paintHODView(Object graphics) {}
|
||||
|
||||
public Color getHODBackground() {
|
||||
return this.backgroundColor;
|
||||
@@ -85,11 +93,11 @@ public class HODPart extends Rectangle implements Serializable {
|
||||
this.foregroundColor = color;
|
||||
}
|
||||
|
||||
public Font getHODFont() {
|
||||
public Object getHODFont() {
|
||||
return this._hodFont;
|
||||
}
|
||||
|
||||
public void setHODFont(Font font) {
|
||||
public void setHODFont(Object font) {
|
||||
this._hodFont = font;
|
||||
}
|
||||
|
||||
|
||||
+50
-9
@@ -1,12 +1,10 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Point;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Programmed Symbol Set manager facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODProgramSymbolManager).
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODProgramSymbolManager {
|
||||
public static final int MAX_HOD_SLOT = 254;
|
||||
@@ -51,14 +49,57 @@ public class HODProgramSymbolManager {
|
||||
delegate.loadProgrammedSymbolSet(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public void drawHODImageCharacter(Graphics g, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
|
||||
if (g == null || pt == null) return;
|
||||
public void drawHODImageCharacter(PixelBuffer pb, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
|
||||
if (pb == null || pt == null) return;
|
||||
ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint);
|
||||
if (slot != null) {
|
||||
int fg = GocaConstants.getGocaColorArgb(colorIdx);
|
||||
BufferedImage img = slot.getScaledImage(cellW, cellH, fg, 0);
|
||||
if (img != null) {
|
||||
g.drawImage(img, pt.x, pt.y, null);
|
||||
PixelBuffer glyph = slot.getScaledPixelBuffer(cellW, cellH, fg, 0);
|
||||
if (glyph != null) {
|
||||
int sw = glyph.getWidth();
|
||||
int sh = glyph.getHeight();
|
||||
int[] srcPx = glyph.getPixels();
|
||||
int[] dstPx = pb.getPixels();
|
||||
int pw = pb.getWidth();
|
||||
int ph = pb.getHeight();
|
||||
for (int r = 0; r < sh; r++) {
|
||||
int dy = pt.y + r;
|
||||
if (dy < 0 || dy >= ph) continue;
|
||||
for (int c = 0; c < sw; c++) {
|
||||
int dx = pt.x + c;
|
||||
if (dx < 0 || dx >= pw) continue;
|
||||
int p = srcPx[r * sw + c];
|
||||
if ((p & 0xFF000000) != 0) {
|
||||
dstPx[dy * pw + dx] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void drawHODImageCharacter(Object g, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
|
||||
if (g == null || pt == null) return;
|
||||
if (g instanceof PixelBuffer) {
|
||||
drawHODImageCharacter((PixelBuffer) g, lcid, codepoint, pt, colorIdx, cellW, cellH);
|
||||
return;
|
||||
}
|
||||
// Fallback for AWT Graphics if passed reflectively
|
||||
ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint);
|
||||
if (slot != null) {
|
||||
int fg = GocaConstants.getGocaColorArgb(colorIdx);
|
||||
PixelBuffer glyph = slot.getScaledPixelBuffer(cellW, cellH, fg, 0);
|
||||
if (glyph != null) {
|
||||
try {
|
||||
Class<?> biClass = Class.forName("java.awt.image.BufferedImage");
|
||||
Object bi = biClass.getConstructor(int.class, int.class, int.class)
|
||||
.newInstance(glyph.getWidth(), glyph.getHeight(), 2); // TYPE_INT_ARGB
|
||||
Method setRGB = biClass.getMethod("setRGB", int.class, int.class, int.class, int.class, int[].class, int.class, int.class);
|
||||
setRGB.invoke(bi, 0, 0, glyph.getWidth(), glyph.getHeight(), glyph.getPixels(), 0, glyph.getWidth());
|
||||
|
||||
Method drawImg = g.getClass().getMethod("drawImage", Class.forName("java.awt.Image"), int.class, int.class, Class.forName("java.awt.image.ImageObserver"));
|
||||
drawImg.invoke(g, bi, pt.x, pt.y, null);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
|
||||
/**
|
||||
* Coordinate transform adapter matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODTransform).
|
||||
|
||||
+19
-6
@@ -1,18 +1,16 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.RGBImageFilter;
|
||||
|
||||
/**
|
||||
* Image filter that keys out a specific background color by setting its alpha to 0x00.
|
||||
* Conforms to IBM Host On-Demand HODTransparentColorFilter.
|
||||
* Platform-neutral implementation independent of java.awt.
|
||||
*/
|
||||
public class HODTransparentColorFilter extends RGBImageFilter {
|
||||
public class HODTransparentColorFilter {
|
||||
|
||||
protected boolean canFilterIndexColorModel = true;
|
||||
private int transparentRgb;
|
||||
|
||||
public HODTransparentColorFilter(int rgb) {
|
||||
this.canFilterIndexColorModel = true;
|
||||
this.transparentRgb = rgb & 0x00FFFFFF;
|
||||
}
|
||||
|
||||
@@ -28,11 +26,26 @@ public class HODTransparentColorFilter extends RGBImageFilter {
|
||||
this.transparentRgb = rgb & 0x00FFFFFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int filterRGB(int x, int y, int rgb) {
|
||||
if ((rgb & 0x00FFFFFF) == transparentRgb) {
|
||||
return 0x00000000;
|
||||
}
|
||||
return rgb;
|
||||
}
|
||||
|
||||
public void apply(int[] pixels, int offset, int length) {
|
||||
if (pixels == null) return;
|
||||
int end = Math.min(pixels.length, offset + length);
|
||||
for (int i = offset; i < end; i++) {
|
||||
pixels[i] = filterRGB(0, 0, pixels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void apply(PixelBuffer buffer) {
|
||||
if (buffer == null) return;
|
||||
int[] pixels = buffer.getPixels();
|
||||
if (pixels != null) {
|
||||
apply(pixels, 0, pixels.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.Insets;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Wallpaper background manager matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODWallpaper).
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODWallpaper extends HODPart {
|
||||
public static final int HOD_TILE = 0;
|
||||
@@ -17,8 +12,8 @@ public class HODWallpaper extends HODPart {
|
||||
public static final int HOD_STRETCH = 2;
|
||||
|
||||
private int _display = HOD_CENTER;
|
||||
private Image rawImage;
|
||||
private Image backgroundImage;
|
||||
private Object rawImage;
|
||||
private Object backgroundImage;
|
||||
|
||||
public HODWallpaper() {
|
||||
this(HOD_CENTER);
|
||||
@@ -28,7 +23,7 @@ public class HODWallpaper extends HODPart {
|
||||
this.setDisplay(displayMode);
|
||||
}
|
||||
|
||||
public HODWallpaper(Image image, int displayMode) {
|
||||
public HODWallpaper(Object image, int displayMode) {
|
||||
this(displayMode);
|
||||
this.setImage(image);
|
||||
}
|
||||
@@ -46,30 +41,40 @@ public class HODWallpaper extends HODPart {
|
||||
return this._display;
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
public void setImage(Object image) {
|
||||
this.rawImage = image;
|
||||
this.backgroundImage = null;
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
public Image getHODImage() {
|
||||
public Object getHODImage() {
|
||||
return this.rawImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Component component, Graphics graphics, int x, int y, int w, int h) {
|
||||
public void paint(Object component, Object graphics, int x, int y, int w, int h) {
|
||||
this.setBounds(x, y, w, h);
|
||||
this.setHODParent(component);
|
||||
super.paint(component, graphics, x, y, w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintHODView(Graphics graphics) {
|
||||
Image image = this.getHODImage();
|
||||
int display = this.getDisplay();
|
||||
Component component = this.getHODParent();
|
||||
public void paint(PixelBuffer buffer, int x, int y, int w, int h) {
|
||||
this.setBounds(x, y, w, h);
|
||||
super.paint(buffer, x, y, w, h);
|
||||
}
|
||||
|
||||
if (image == null || component == null) {
|
||||
@Override
|
||||
protected void paintHODView(Object graphics) {
|
||||
Object image = this.getHODImage();
|
||||
int display = this.getDisplay();
|
||||
|
||||
if (image == null || graphics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (graphics instanceof PixelBuffer && image instanceof PixelBuffer) {
|
||||
paintHODViewBuffer((PixelBuffer) graphics, (PixelBuffer) image);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,7 +87,83 @@ public class HODWallpaper extends HODPart {
|
||||
}
|
||||
}
|
||||
|
||||
protected void hodtileImage(Graphics graphics, Image image) {
|
||||
public void paintHODViewBuffer(PixelBuffer dst, PixelBuffer src) {
|
||||
int display = this.getDisplay();
|
||||
Insets insets = this.getInsets();
|
||||
int availX = this.x + insets.left;
|
||||
int availY = this.y + insets.top;
|
||||
int availW = this.width - (insets.left + insets.right);
|
||||
int availH = this.height - (insets.top + insets.bottom);
|
||||
if (availW <= 0 || availH <= 0 || src.getWidth() <= 0 || src.getHeight() <= 0) return;
|
||||
|
||||
if (display == HOD_CENTER) {
|
||||
int cx = availX + (availW - src.getWidth()) / 2;
|
||||
int cy = availY + (availH - src.getHeight()) / 2;
|
||||
blitBuffer(dst, src, cx, cy);
|
||||
} else if (display == HOD_TILE) {
|
||||
int cols = (availW / src.getWidth()) + 1;
|
||||
int rows = (availH / src.getHeight()) + 1;
|
||||
int curX = availX;
|
||||
for (int i = 0; i < cols; i++) {
|
||||
int curY = availY;
|
||||
for (int j = 0; j < rows; j++) {
|
||||
blitBuffer(dst, src, curX, curY);
|
||||
curY += src.getHeight();
|
||||
}
|
||||
curX += src.getWidth();
|
||||
}
|
||||
} else if (display == HOD_STRETCH) {
|
||||
scaleBuffer(dst, src, availX, availY, availW, availH);
|
||||
}
|
||||
}
|
||||
|
||||
private void blitBuffer(PixelBuffer dst, PixelBuffer src, int dstX, int dstY) {
|
||||
int sw = src.getWidth();
|
||||
int sh = src.getHeight();
|
||||
int dw = dst.getWidth();
|
||||
int dh = dst.getHeight();
|
||||
int[] srcPx = src.getPixels();
|
||||
int[] dstPx = dst.getPixels();
|
||||
|
||||
for (int r = 0; r < sh; r++) {
|
||||
int dy = dstY + r;
|
||||
if (dy < 0 || dy >= dh) continue;
|
||||
for (int c = 0; c < sw; c++) {
|
||||
int dx = dstX + c;
|
||||
if (dx < 0 || dx >= dw) continue;
|
||||
int p = srcPx[r * sw + c];
|
||||
if ((p & 0xFF000000) != 0) {
|
||||
dstPx[dy * dw + dx] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void scaleBuffer(PixelBuffer dst, PixelBuffer src, int dx, int dy, int dw, int dh) {
|
||||
int sw = src.getWidth();
|
||||
int sh = src.getHeight();
|
||||
int targetW = dst.getWidth();
|
||||
int targetH = dst.getHeight();
|
||||
int[] srcPx = src.getPixels();
|
||||
int[] dstPx = dst.getPixels();
|
||||
|
||||
for (int r = 0; r < dh; r++) {
|
||||
int outY = dy + r;
|
||||
if (outY < 0 || outY >= targetH) continue;
|
||||
int sy = r * sh / dh;
|
||||
for (int c = 0; c < dw; c++) {
|
||||
int outX = dx + c;
|
||||
if (outX < 0 || outX >= targetW) continue;
|
||||
int sx = c * sw / dw;
|
||||
int p = srcPx[sy * sw + sx];
|
||||
if ((p & 0xFF000000) != 0) {
|
||||
dstPx[outY * targetW + outX] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void hodtileImage(Object graphics, Object image) {
|
||||
Dimension imgSize = getImageSize(image);
|
||||
if (imgSize.width <= 0 || imgSize.height <= 0) return;
|
||||
|
||||
@@ -99,40 +180,86 @@ public class HODWallpaper extends HODPart {
|
||||
for (int i = 0; i < cols; i++) {
|
||||
int curY = startY;
|
||||
for (int j = 0; j < rows; j++) {
|
||||
graphics.drawImage(image, curX, curY, this.getHODParent());
|
||||
invokeDrawImage(graphics, image, curX, curY);
|
||||
curY += imgSize.height;
|
||||
}
|
||||
curX += imgSize.width;
|
||||
}
|
||||
}
|
||||
|
||||
protected void centerHODImage(Graphics graphics, Image image) {
|
||||
protected void centerHODImage(Object graphics, Object image) {
|
||||
Dimension imgSize = getImageSize(image);
|
||||
if (imgSize.width <= 0 || imgSize.height <= 0) return;
|
||||
|
||||
Insets insets = this.getInsets();
|
||||
int cx = this.x + insets.left + (this.width - imgSize.width) / 2;
|
||||
int cy = this.y + insets.top + (this.height - imgSize.height) / 2;
|
||||
graphics.drawImage(image, cx, cy, this.getHODParent());
|
||||
invokeDrawImage(graphics, image, cx, cy);
|
||||
}
|
||||
|
||||
protected void stretchHODImage(Graphics graphics, Image image) {
|
||||
protected void stretchHODImage(Object graphics, Object image) {
|
||||
Insets insets = this.getInsets();
|
||||
int sx = this.x + insets.left;
|
||||
int sy = this.y + insets.top;
|
||||
int sw = this.width - (insets.left + insets.right);
|
||||
int sh = this.height - (insets.top + insets.bottom);
|
||||
graphics.drawImage(image, sx, sy, sw, sh, this.getHODParent());
|
||||
invokeDrawImage(graphics, image, sx, sy, sw, sh);
|
||||
}
|
||||
|
||||
private Dimension getImageSize(Image image) {
|
||||
private void invokeDrawImage(Object graphics, Object img, int x, int y) {
|
||||
if (graphics == null || img == null) return;
|
||||
try {
|
||||
for (Method m : graphics.getClass().getMethods()) {
|
||||
if (m.getName().equals("drawImage")) {
|
||||
Class<?>[] pts = m.getParameterTypes();
|
||||
if (pts.length == 4 && pts[1] == int.class && pts[2] == int.class) {
|
||||
m.invoke(graphics, img, x, y, this.getHODParent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
private void invokeDrawImage(Object graphics, Object img, int x, int y, int w, int h) {
|
||||
if (graphics == null || img == null) return;
|
||||
try {
|
||||
for (Method m : graphics.getClass().getMethods()) {
|
||||
if (m.getName().equals("drawImage")) {
|
||||
Class<?>[] pts = m.getParameterTypes();
|
||||
if (pts.length == 6 && pts[1] == int.class && pts[2] == int.class && pts[3] == int.class && pts[4] == int.class) {
|
||||
m.invoke(graphics, img, x, y, w, h, this.getHODParent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
private Dimension getImageSize(Object image) {
|
||||
if (image == null) return new Dimension(0, 0);
|
||||
Component c = this.getHODParent();
|
||||
int w = image.getWidth(c);
|
||||
int h = image.getHeight(c);
|
||||
if (w <= 0 && image instanceof BufferedImage) {
|
||||
w = ((BufferedImage) image).getWidth();
|
||||
h = ((BufferedImage) image).getHeight();
|
||||
if (image instanceof PixelBuffer) {
|
||||
PixelBuffer pb = (PixelBuffer) image;
|
||||
return new Dimension(pb.getWidth(), pb.getHeight());
|
||||
}
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
try {
|
||||
Method mw = image.getClass().getMethod("getWidth");
|
||||
w = ((Number) mw.invoke(image)).intValue();
|
||||
Method mh = image.getClass().getMethod("getHeight");
|
||||
h = ((Number) mh.invoke(image)).intValue();
|
||||
} catch (Throwable ignored) {
|
||||
try {
|
||||
for (Method m : image.getClass().getMethods()) {
|
||||
if (m.getName().equals("getWidth") && m.getParameterCount() == 1) {
|
||||
w = ((Number) m.invoke(image, this.getHODParent())).intValue();
|
||||
}
|
||||
if (m.getName().equals("getHeight") && m.getParameterCount() == 1) {
|
||||
h = ((Number) m.invoke(image, this.getHODParent())).intValue();
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored2) {}
|
||||
}
|
||||
return new Dimension(Math.max(0, w), Math.max(0, h));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lightweight pure-Java POJO representing border insets.
|
||||
* Completely decouples lib3270j from java.awt.Insets.
|
||||
*/
|
||||
public class Insets implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int top;
|
||||
public int left;
|
||||
public int bottom;
|
||||
public int right;
|
||||
|
||||
public Insets(int top, int left, int bottom, int right) {
|
||||
this.top = top;
|
||||
this.left = left;
|
||||
this.bottom = bottom;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Insets)) return false;
|
||||
Insets i = (Insets) obj;
|
||||
return top == i.top && left == i.left && bottom == i.bottom && right == i.right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(top, left, bottom, right);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[top=" + top + ",left=" + left + ",bottom=" + bottom + ",right=" + right + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Platform-neutral pixel buffer interface for 32-bit ARGB offscreen rasterization.
|
||||
* Compatible with pure Java SE (Swing), Android (Bitmap), and headless environments.
|
||||
*/
|
||||
public interface PixelBuffer {
|
||||
|
||||
/** Returns width of the pixel buffer in pixels. */
|
||||
int getWidth();
|
||||
|
||||
/** Returns height of the pixel buffer in pixels. */
|
||||
int getHeight();
|
||||
|
||||
/** Returns contiguous 32-bit ARGB pixel array. */
|
||||
int[] getPixels();
|
||||
|
||||
/** Returns ARGB pixel at specified coordinate, or 0 if out of bounds. */
|
||||
int getPixel(int x, int y);
|
||||
|
||||
/** Sets ARGB pixel at specified coordinate with alpha blending and mix mode. */
|
||||
void setPixel(int x, int y, int argb);
|
||||
|
||||
/** Sets ARGB pixel directly without blending. */
|
||||
void setPixelDirect(int x, int y, int argb);
|
||||
|
||||
/** Clears buffer to fully transparent (0x00000000). */
|
||||
void clear();
|
||||
|
||||
/** Clears buffer to specified ARGB color. */
|
||||
void clear(int argb);
|
||||
|
||||
/** Draws a 1-pixel line using integer Bresenham algorithm. */
|
||||
void drawLine(int x1, int y1, int x2, int y2, int argb);
|
||||
|
||||
/** Draws a 1-pixel line using Bresenham algorithm. */
|
||||
void drawLineBresenham(int x0, int y0, int x1, int y1, int argb);
|
||||
|
||||
/** Draws a stroked line with line type and line width. */
|
||||
void drawLine(double x0, double y0, double x1, double y1, int argb, int lineType, int lineWidth);
|
||||
|
||||
/** Draws an anti-aliased sub-pixel line segment with stroke width. */
|
||||
void drawLineAA(double x0, double y0, double x1, double y1, int argb, double strokeWidth);
|
||||
|
||||
/** Fills a rectangular region with specified ARGB color. */
|
||||
void fillRect(int x, int y, int width, int height, int argb);
|
||||
|
||||
/** Sets clipping rectangle. */
|
||||
void setClip(int x, int y, int width, int height);
|
||||
|
||||
/** Clears clipping rectangle. */
|
||||
void clearClip();
|
||||
|
||||
/** Checks if coordinate falls outside current clipping bounds. */
|
||||
boolean isClipped(int x, int y);
|
||||
|
||||
/** Copies a rectangular block of pixels from source array into this buffer. */
|
||||
void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lightweight pure-Java POJO point for 2D presentation coordinates.
|
||||
* Completely decouples lib3270j from java.awt.Point.
|
||||
*/
|
||||
public class Point implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int x;
|
||||
public int y;
|
||||
|
||||
public Point() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
public Point(Point p) {
|
||||
this(p != null ? p.x : 0, p != null ? p.y : 0);
|
||||
}
|
||||
|
||||
public Point(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setLocation(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public void setLocation(Point p) {
|
||||
if (p != null) {
|
||||
this.x = p.x;
|
||||
this.y = p.y;
|
||||
}
|
||||
}
|
||||
|
||||
public void translate(int dx, int dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Point)) return false;
|
||||
Point pt = (Point) obj;
|
||||
return (x == pt.x) && (y == pt.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[x=" + x + ",y=" + y + "]";
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package haus.nightmare.lib3270j.graphics;
|
||||
/**
|
||||
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
||||
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
|
||||
* Decoupled from java.awt using PixelBuffer.
|
||||
*/
|
||||
public class ProgramSymbolSet {
|
||||
|
||||
@@ -64,8 +65,8 @@ public class ProgramSymbolSet {
|
||||
private int[] cachedRgbArray;
|
||||
private int cachedFgRgb = -1;
|
||||
private int cachedBgRgb = -1;
|
||||
private java.awt.image.BufferedImage cachedImage;
|
||||
private java.awt.image.BufferedImage cachedScaledImage;
|
||||
private PixelBuffer cachedPixelBuffer;
|
||||
private PixelBuffer cachedScaledPixelBuffer;
|
||||
private int cachedTargetW = 0;
|
||||
private int cachedTargetH = 0;
|
||||
private int cachedScaledFgRgb = -1;
|
||||
@@ -103,23 +104,21 @@ public class ProgramSymbolSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
||||
* Enables unscaled 1:1 hardware blitting in Java2D.
|
||||
* Returns a PixelBuffer scaled directly to target dimensions (cellWidth x cellHeight).
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
public synchronized PixelBuffer getScaledPixelBuffer(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
if (targetW <= 0 || targetH <= 0) {
|
||||
return getImage(fgArgb, bgArgb);
|
||||
return getPixelBuffer(fgArgb, bgArgb);
|
||||
}
|
||||
if (targetW == width && targetH == height) {
|
||||
return getImage(fgArgb, bgArgb);
|
||||
return getPixelBuffer(fgArgb, bgArgb);
|
||||
}
|
||||
if (cachedScaledImage != null && cachedTargetW == targetW && cachedTargetH == targetH
|
||||
if (cachedScaledPixelBuffer != null && cachedTargetW == targetW && cachedTargetH == targetH
|
||||
&& cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) {
|
||||
return cachedScaledImage;
|
||||
return cachedScaledPixelBuffer;
|
||||
}
|
||||
int[] srcRgb = getRgbPixels(fgArgb, bgArgb);
|
||||
java.awt.image.BufferedImage scaled = new java.awt.image.BufferedImage(targetW, targetH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData();
|
||||
int[] dstRgb = new int[targetW * targetH];
|
||||
|
||||
for (int dy = 0; dy < targetH; dy++) {
|
||||
int sy = dy * height / targetH;
|
||||
@@ -131,30 +130,36 @@ public class ProgramSymbolSet {
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedScaledImage = scaled;
|
||||
this.cachedScaledPixelBuffer = new DefaultPixelBuffer(targetW, targetH, dstRgb);
|
||||
this.cachedTargetW = targetW;
|
||||
this.cachedTargetH = targetH;
|
||||
this.cachedScaledFgRgb = fgArgb;
|
||||
this.cachedScaledBgRgb = bgArgb;
|
||||
return scaled;
|
||||
return this.cachedScaledPixelBuffer;
|
||||
}
|
||||
|
||||
public synchronized PixelBuffer getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
return getScaledPixelBuffer(targetW, targetH, fgArgb, bgArgb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the cached BufferedImage for this symbol glyph.
|
||||
* Eliminates per-cell heap allocations during high frame rate rendering.
|
||||
* Computes and returns the cached PixelBuffer for this symbol glyph.
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) {
|
||||
if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||
return cachedImage;
|
||||
public synchronized PixelBuffer getPixelBuffer(int fgArgb, int bgArgb) {
|
||||
if (cachedPixelBuffer != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||
return cachedPixelBuffer;
|
||||
}
|
||||
int[] rgb = getRgbPixels(fgArgb, bgArgb);
|
||||
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||
int[] imgData = new int[rgb.length];
|
||||
System.arraycopy(rgb, 0, imgData, 0, rgb.length);
|
||||
this.cachedImage = img;
|
||||
this.cachedPixelBuffer = new DefaultPixelBuffer(width, height, imgData);
|
||||
this.cachedFgRgb = fgArgb;
|
||||
this.cachedBgRgb = bgArgb;
|
||||
return img;
|
||||
return this.cachedPixelBuffer;
|
||||
}
|
||||
|
||||
public synchronized PixelBuffer getImage(int fgArgb, int bgArgb) {
|
||||
return getPixelBuffer(fgArgb, bgArgb);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lightweight pure-Java POJO rectangle for 2D presentation coordinates.
|
||||
* Completely decouples lib3270j from java.awt.Rectangle.
|
||||
*/
|
||||
public class Rectangle implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int x;
|
||||
public int y;
|
||||
public int width;
|
||||
public int height;
|
||||
|
||||
public Rectangle() {
|
||||
this(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public Rectangle(Rectangle r) {
|
||||
this(r != null ? r.x : 0, r != null ? r.y : 0, r != null ? r.width : 0, r != null ? r.height : 0);
|
||||
}
|
||||
|
||||
public Rectangle(int x, int y, int width, int height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Rectangle(int width, int height) {
|
||||
this(0, 0, width, height);
|
||||
}
|
||||
|
||||
public Rectangle(Point p, Dimension d) {
|
||||
this(p != null ? p.x : 0, p != null ? p.y : 0, d != null ? d.width : 0, d != null ? d.height : 0);
|
||||
}
|
||||
|
||||
public Rectangle(Point p) {
|
||||
this(p != null ? p.x : 0, p != null ? p.y : 0, 0, 0);
|
||||
}
|
||||
|
||||
public Rectangle(Dimension d) {
|
||||
this(0, 0, d != null ? d.width : 0, d != null ? d.height : 0);
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setBounds(int x, int y, int width, int height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public void setBounds(Rectangle r) {
|
||||
if (r != null) {
|
||||
setBounds(r.x, r.y, r.width, r.height);
|
||||
}
|
||||
}
|
||||
|
||||
public Point getLocation() {
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
public void setLocation(Point p) {
|
||||
if (p != null) {
|
||||
this.x = p.x;
|
||||
this.y = p.y;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLocation(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public Dimension getSize() {
|
||||
return new Dimension(width, height);
|
||||
}
|
||||
|
||||
public void setSize(Dimension d) {
|
||||
if (d != null) {
|
||||
this.width = d.width;
|
||||
this.height = d.height;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSize(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public boolean contains(int X, int Y) {
|
||||
int w = this.width;
|
||||
int h = this.height;
|
||||
if ((w | h) < 0) return false;
|
||||
int x = this.x;
|
||||
int y = this.y;
|
||||
if (X < x || Y < y) return false;
|
||||
w += x;
|
||||
h += y;
|
||||
return ((w < x || w > X) && (h < y || h > Y));
|
||||
}
|
||||
|
||||
public boolean contains(Point p) {
|
||||
return p != null && contains(p.x, p.y);
|
||||
}
|
||||
|
||||
public boolean intersects(Rectangle r) {
|
||||
if (r == null) return false;
|
||||
int tw = this.width;
|
||||
int th = this.height;
|
||||
int rw = r.width;
|
||||
int rh = r.height;
|
||||
if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) return false;
|
||||
int tx = this.x;
|
||||
int ty = this.y;
|
||||
int rx = r.x;
|
||||
int ry = r.y;
|
||||
rw += rx;
|
||||
rh += ry;
|
||||
tw += tx;
|
||||
th += ty;
|
||||
return ((rw < rx || rw > tx) &&
|
||||
(rh < ry || rh > ty) &&
|
||||
(tw < tx || tw > rx) &&
|
||||
(th < ty || th > ry));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return (width <= 0) || (height <= 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Rectangle)) return false;
|
||||
Rectangle r = (Rectangle) obj;
|
||||
return ((x == r.x) &&
|
||||
(y == r.y) &&
|
||||
(width == r.width) &&
|
||||
(height == r.height));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[x=" + x + ",y=" + y + ",width=" + width + ",height=" + height + "]";
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,91 @@ public class InputProcessor {
|
||||
}
|
||||
}
|
||||
public boolean isInsertMode() { return insertMode; }
|
||||
public void setInsertMode(boolean insert) { this.insertMode = insert; }
|
||||
public void setInsertMode(boolean insert) {
|
||||
this.insertMode = insert;
|
||||
if (oia != null) {
|
||||
oia.notifyOIAChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BellListener {
|
||||
void onBell();
|
||||
}
|
||||
|
||||
private BellListener bellListener;
|
||||
private boolean bellEnabled = false;
|
||||
private int bellColumn = 74; // 0-based index for column 75
|
||||
private boolean insertOffOnAid = true;
|
||||
private boolean aplKeyboardMode = false;
|
||||
private boolean numericFieldLock = true;
|
||||
private boolean autoSkipEnabled = true;
|
||||
|
||||
public void setBellListener(BellListener listener) { this.bellListener = listener; }
|
||||
public BellListener getBellListener() { return bellListener; }
|
||||
public boolean isBellEnabled() { return bellEnabled; }
|
||||
public void setBellEnabled(boolean enabled) { this.bellEnabled = enabled; }
|
||||
public int getBellColumn() { return bellColumn; }
|
||||
public void setBellColumn(int col) { this.bellColumn = col; }
|
||||
|
||||
public boolean isInsertOffOnAid() { return insertOffOnAid; }
|
||||
public void setInsertOffOnAid(boolean val) { this.insertOffOnAid = val; }
|
||||
|
||||
public boolean isAplKeyboardMode() { return aplKeyboardMode; }
|
||||
public void setAplKeyboardMode(boolean enabled) {
|
||||
this.aplKeyboardMode = enabled;
|
||||
if (oia != null) {
|
||||
oia.notifyOIAChanged();
|
||||
}
|
||||
}
|
||||
public void toggleAplKeyboardMode() {
|
||||
setAplKeyboardMode(!aplKeyboardMode);
|
||||
}
|
||||
|
||||
public boolean isNumericFieldLock() { return numericFieldLock; }
|
||||
public void setNumericFieldLock(boolean lock) { this.numericFieldLock = lock; }
|
||||
|
||||
public boolean isAutoSkipEnabled() { return autoSkipEnabled; }
|
||||
public void setAutoSkipEnabled(boolean enabled) { this.autoSkipEnabled = enabled; }
|
||||
|
||||
/**
|
||||
* Map ASCII key character to IBM 3270 APL / Graphic Escape code point.
|
||||
*/
|
||||
private int getAplCodeForChar(char ch) {
|
||||
char upper = Character.toUpperCase(ch);
|
||||
switch (upper) {
|
||||
case 'A': return 0x81; // ⍺ Alpha
|
||||
case 'B': return 0x82; // ⊥ Up tack / decode
|
||||
case 'C': return 0x83; // ∩ Intersection
|
||||
case 'D': return 0x84; // ⌊ Floor
|
||||
case 'E': return 0x85; // │ Vertical line
|
||||
case 'F': return 0x87; // ∇ Del / Grad
|
||||
case 'G': return 0x88; // ∆ Delta
|
||||
case 'H': return 0x89; // ⍳ Iota
|
||||
case 'I': return 0x8A; // → Right arrow
|
||||
case 'J': return 0x8B; // ⍞ Quote Quad
|
||||
case 'K': return 0x8C; // ≤ Less than or equal
|
||||
case 'L': return 0xAD; // [ Bracket left
|
||||
case 'M': return 0x8E; // × Multiply
|
||||
case 'N': return 0x8F; // ÷ Divide
|
||||
case 'O': return 0x90; // ⍟ Circle Star
|
||||
case 'P': return 0x91; // ⌹ Domino / Quad divide
|
||||
case 'Q': return 0x92; // ⊤ Down tack / encode
|
||||
case 'R': return 0x95; // ⍴ Rho
|
||||
case 'S': return 0x94; // ⌈ Ceiling
|
||||
case 'T': return 0x98; // ⍷ Epsilon underbar
|
||||
case 'U': return 0x93; // ∪ Union
|
||||
case 'V': return 0x97; // ≠ Not equal
|
||||
case 'W': return 0x96; // ⍵ Omega
|
||||
case 'X': return 0xAC; // ⍉ Transpose
|
||||
case 'Y': return 0xA8; // ↑ Up arrow / Take
|
||||
case 'Z': return 0xA9; // ↓ Down arrow / Drop
|
||||
case '-': return 0xA2; // ─ Horizontal line
|
||||
case '|': return 0x85; // │ Vertical line
|
||||
case '+': return 0xCB; // ┼ Cross
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter a character at the current cursor position.
|
||||
@@ -142,8 +226,8 @@ public class InputProcessor {
|
||||
int baddr = screen.getCursorAddress();
|
||||
baddr = ((baddr % size) + size) % size;
|
||||
|
||||
// Entry Assist DOC mode / Word Wrap handling
|
||||
if ((screen.isEntryAssistDOCmode() || screen.isEntryAssistWordWrap()) && !isNvtMode()) {
|
||||
// Entry Assist Word Wrap handling
|
||||
if (screen.isEntryAssistWordWrap() && !isNvtMode()) {
|
||||
int curCol = baddr % screen.getCols();
|
||||
int endCol = screen.getEntryAssistEndColumn();
|
||||
int startCol = screen.getEntryAssistStartColumn();
|
||||
@@ -178,7 +262,7 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
// Numeric-only field check: digits 0-9, minus (-), period (.), space ( ), DUP, FM
|
||||
if (faIsNumeric(faVal & 0xFF)) {
|
||||
if (numericFieldLock && faIsNumeric(faVal & 0xFF)) {
|
||||
boolean isValidNumeric = (ch >= '0' && ch <= '9') || ch == '-' || ch == '.' || ch == ' '
|
||||
|| ch == '*' || ch == ';' || ch == (char) FCORDER_DUP || ch == (char) FCORDER_FM;
|
||||
if (!isValidNumeric) {
|
||||
@@ -191,8 +275,20 @@ public class InputProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// Translate character to EBCDIC
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
// Translate character to EBCDIC (or APL Graphic Escape if in APL Keyboard Mode)
|
||||
boolean isAplChar = false;
|
||||
int ebc = -1;
|
||||
if (aplKeyboardMode) {
|
||||
int aplCode = getAplCodeForChar(ch);
|
||||
if (aplCode >= 0) {
|
||||
ebc = aplCode;
|
||||
ch = translator.mapAPL(aplCode);
|
||||
isAplChar = true;
|
||||
}
|
||||
}
|
||||
if (ebc < 0) {
|
||||
ebc = translator.unicodeToEbcdic(ch);
|
||||
}
|
||||
if (ebc < 0) return;
|
||||
|
||||
if (insertMode) {
|
||||
@@ -229,6 +325,9 @@ public class InputProcessor {
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
ea.ec = (byte) ebc;
|
||||
ea.ucs4 = ch;
|
||||
if (isAplChar) {
|
||||
ea.cs = ExtendedAttribute.CS_GE;
|
||||
}
|
||||
|
||||
// Set MDT on field attribute
|
||||
if (screen.isFormatted()) {
|
||||
@@ -244,7 +343,7 @@ public class InputProcessor {
|
||||
int nextAddr = screen.incrementAddress(baddr);
|
||||
if (screen.getCell(nextAddr).isFieldAttribute()) {
|
||||
byte nextFa = screen.getCell(nextAddr).fa;
|
||||
if (faIsSkip(nextFa & 0xFF)) {
|
||||
if (autoSkipEnabled && faIsSkip(nextFa & 0xFF)) {
|
||||
// Auto-skip field (Protected + Numeric): jump to next unprotected field
|
||||
int skipTarget = screen.findNextUnprotected(nextAddr);
|
||||
screen.setCursorAddress(skipTarget);
|
||||
@@ -264,6 +363,28 @@ public class InputProcessor {
|
||||
screen.setCursorAddress((baddr + 1) % size);
|
||||
}
|
||||
|
||||
// Entry Assist DOC mode: if we typed into or past the right margin, advance to startCol on next row
|
||||
if (screen != null && screen.isEntryAssistDOCmode() && !screen.isEntryAssistWordWrap() && !isNvtMode()) {
|
||||
int typedCol = baddr % screen.getCols();
|
||||
if (typedCol >= screen.getEntryAssistEndColumn()) {
|
||||
int curRow = baddr / screen.getCols();
|
||||
int nextRow = (curRow + 1) % screen.getRows();
|
||||
int targetAddr = nextRow * screen.getCols() + screen.getEntryAssistStartColumn();
|
||||
if (screen.isFormatted()) {
|
||||
targetAddr = screen.findNextUnprotected(targetAddr - 1);
|
||||
}
|
||||
screen.setCursorAddress(targetAddr);
|
||||
}
|
||||
}
|
||||
|
||||
// Audible End-of-Line Warning Signal
|
||||
if (bellEnabled && screen != null) {
|
||||
int curCol = screen.getCursorCol();
|
||||
if (curCol == bellColumn && bellListener != null) {
|
||||
bellListener.onBell();
|
||||
}
|
||||
}
|
||||
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
@@ -291,6 +412,155 @@ public class InputProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all character locations on the given row are protected.
|
||||
*/
|
||||
public boolean isLineProtected(int row) {
|
||||
if (screen == null || !screen.isFormatted()) return false;
|
||||
int cols = screen.getCols();
|
||||
int rows = screen.getRows();
|
||||
if (row < 0 || row >= rows) return false;
|
||||
int start = row * cols;
|
||||
int end = start + cols;
|
||||
for (int i = start; i < end; i++) {
|
||||
if (screen.getCell(i).isFieldAttribute()) continue;
|
||||
byte fa = screen.getFieldAttributeAt(i);
|
||||
if (!faIsProtected(fa & 0xFF)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste text into the presentation space.
|
||||
* When enablePasteFromExcel is true:
|
||||
* - '\t' triggers a Tab action advancing to the next unprotected input field (SBA).
|
||||
* - '\n' or '\r\n' triggers a NewLine action advancing to the first unprotected field of the next line.
|
||||
* When pasteStopAtProtectedLine is true:
|
||||
* - Halts or truncates paste if cursor reaches a protected line/boundary or if the current field is full.
|
||||
*
|
||||
* @param text text to paste
|
||||
* @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances
|
||||
* @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries
|
||||
* @return number of characters pasted
|
||||
*/
|
||||
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) {
|
||||
if (text == null || text.isEmpty() || screen == null || keyboardLocked) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isNvtMode()) {
|
||||
try {
|
||||
fsm.sendNVTString(text);
|
||||
return text.length();
|
||||
} catch (IOException e) {
|
||||
log.warning("Failed to send NVT paste: " + e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
int len = text.length();
|
||||
int i = 0;
|
||||
|
||||
while (i < len && !keyboardLocked) {
|
||||
char ch = text.charAt(i);
|
||||
|
||||
// Handle newline sequences: \r\n, \r, or \n
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
if (ch == '\r' && (i + 1) < len && text.charAt(i + 1) == '\n') {
|
||||
i++; // skip \n of \r\n
|
||||
}
|
||||
if (enablePasteFromExcel) {
|
||||
int curRow = screen.getCursorRow();
|
||||
int nextRow = (curRow + 1) % screen.getRows();
|
||||
if (pasteStopAtProtectedLine && isLineProtected(nextRow)) {
|
||||
break; // Stop paste when next line is protected
|
||||
}
|
||||
newline();
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle tab character
|
||||
if (ch == '\t') {
|
||||
if (enablePasteFromExcel) {
|
||||
tab();
|
||||
int newAddr = screen.getCursorAddress();
|
||||
if (pasteStopAtProtectedLine && screen.isFormatted()) {
|
||||
byte fa = screen.getFieldAttributeAt(newAddr);
|
||||
if (faIsProtected(fa & 0xFF)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular printable character
|
||||
if (ch >= 0x20 && ch != 0x7F) {
|
||||
if (screen.isFormatted()) {
|
||||
int baddr = screen.getCursorAddress();
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
if (ea.isFieldAttribute()) {
|
||||
baddr = (baddr + 1) % (screen.getRows() * screen.getCols());
|
||||
}
|
||||
byte faVal = screen.getFieldAttributeAt(baddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
if (pasteStopAtProtectedLine) {
|
||||
break; // Stop paste immediately at protected boundary
|
||||
} else {
|
||||
tab();
|
||||
baddr = screen.getCursorAddress();
|
||||
if (faIsProtected(screen.getFieldAttributeAt(baddr) & 0xFF)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeCharacter(ch);
|
||||
count++;
|
||||
|
||||
// If pasteStopAtProtectedLine is enabled, check if the cursor after typing has hit a protected field
|
||||
if (pasteStopAtProtectedLine && screen.isFormatted() && !keyboardLocked) {
|
||||
int curAddr = screen.getCursorAddress();
|
||||
ExtendedAttribute curCell = screen.getCell(curAddr);
|
||||
if (curCell.isFieldAttribute()) {
|
||||
byte nextFa = curCell.fa;
|
||||
if (faIsProtected(nextFa & 0xFF)) {
|
||||
if (i + 1 < len) {
|
||||
char nextCh = text.charAt(i + 1);
|
||||
if (nextCh != '\t' && nextCh != '\r' && nextCh != '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
byte curFa = screen.getFieldAttributeAt(curAddr);
|
||||
if (faIsProtected(curFa & 0xFF)) {
|
||||
if (i + 1 < len) {
|
||||
char nextCh = text.charAt(i + 1);
|
||||
if (nextCh != '\t' && nextCh != '\r' && nextCh != '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields).
|
||||
*/
|
||||
@@ -523,6 +793,9 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
if (aidCode == AID_CLEAR) {
|
||||
if (insertOffOnAid && insertMode) {
|
||||
setInsertMode(false);
|
||||
}
|
||||
screen.clear();
|
||||
screen.markAllChanged();
|
||||
if (graphicsPlane != null) {
|
||||
@@ -538,6 +811,10 @@ public class InputProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (insertOffOnAid && insertMode) {
|
||||
setInsertMode(false);
|
||||
}
|
||||
|
||||
lastAid = aidCode;
|
||||
setKeyboardLocked(true);
|
||||
|
||||
@@ -792,6 +1069,10 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
public void tab() {
|
||||
if (screen != null && screen.isEntryAssistDOCmode()) {
|
||||
screen.processWordTab(true);
|
||||
return;
|
||||
}
|
||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
@@ -806,6 +1087,10 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
public void backTab() {
|
||||
if (screen != null && screen.isEntryAssistDOCmode()) {
|
||||
screen.processWordTab(false);
|
||||
return;
|
||||
}
|
||||
if (!screen.isFormatted()) return;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
@@ -1546,6 +1831,22 @@ public class InputProcessor {
|
||||
case "lightpen":
|
||||
processLightPen();
|
||||
break;
|
||||
case "docmode":
|
||||
if (screen != null) {
|
||||
screen.setEntryAssistDOCmode(!screen.isEntryAssistDOCmode());
|
||||
if (oia != null) oia.notifyOIAChanged();
|
||||
}
|
||||
break;
|
||||
case "wordwrap":
|
||||
if (screen != null) {
|
||||
screen.setEntryAssistWordWrap(!screen.isEntryAssistWordWrap());
|
||||
if (oia != null) oia.notifyOIAChanged();
|
||||
}
|
||||
break;
|
||||
case "apl":
|
||||
case "aplmode":
|
||||
toggleAplKeyboardMode();
|
||||
break;
|
||||
default:
|
||||
if (token.startsWith("pf")) {
|
||||
try {
|
||||
|
||||
@@ -14,4 +14,7 @@ public interface ConnectionListener {
|
||||
|
||||
/** Called when TN3270E negotiation completes. */
|
||||
default void onTN3270ENegotiated(String deviceType, String deviceName) {}
|
||||
|
||||
/** Called when TN3270E functions negotiation completes or changes. */
|
||||
default void onTN3270EFunctionsNegotiated(boolean[] functions) {}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,7 @@ public interface ScreenUpdateListener {
|
||||
|
||||
/** Called when the screen size changes (erase/write vs erase/write alternate). */
|
||||
default void onScreenSizeChanged(int rows, int cols) {}
|
||||
|
||||
/** Called when the keyboard is unlocked (e.g. via WCC restore, AUTO_SYS_UNLOCK, or Contention Resolution SDI). */
|
||||
default void onKeyboardUnlocked() {}
|
||||
}
|
||||
|
||||
@@ -1305,6 +1305,9 @@ public class NvtProcessor {
|
||||
}
|
||||
|
||||
private void notifyScreenUpdated() {
|
||||
if (screenBuffer != null) {
|
||||
screenBuffer.notifyScreenUpdate();
|
||||
}
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
@@ -60,6 +65,61 @@ public class ScreenBuffer {
|
||||
private final EbcdicTranslator translator;
|
||||
private final Object renderLock = new Object();
|
||||
|
||||
// Synchronization primitives and screen update listeners (Phase 12)
|
||||
private final ReentrantLock syncLock = new ReentrantLock();
|
||||
private final Condition syncCondition = syncLock.newCondition();
|
||||
private final List<ScreenUpdateListener> updateListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
public ReentrantLock getSyncLock() {
|
||||
return syncLock;
|
||||
}
|
||||
|
||||
public Condition getSyncCondition() {
|
||||
return syncCondition;
|
||||
}
|
||||
|
||||
public void addUpdateListener(ScreenUpdateListener l) {
|
||||
if (l != null && !updateListeners.contains(l)) {
|
||||
updateListeners.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeUpdateListener(ScreenUpdateListener l) {
|
||||
updateListeners.remove(l);
|
||||
}
|
||||
|
||||
public void signalWaiters() {
|
||||
syncLock.lock();
|
||||
try {
|
||||
syncCondition.signalAll();
|
||||
} finally {
|
||||
syncLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyScreenUpdate() {
|
||||
updateDisplaySnapshot();
|
||||
signalWaiters();
|
||||
for (ScreenUpdateListener l : updateListeners) {
|
||||
try {
|
||||
l.onScreenUpdated();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyCursorMoved(int oldAddress, int newAddress) {
|
||||
signalWaiters();
|
||||
for (ScreenUpdateListener l : updateListeners) {
|
||||
try {
|
||||
l.onCursorMoved(oldAddress, newAddress);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyCursorMoved() {
|
||||
notifyCursorMoved(cursorAddress, cursorAddress);
|
||||
}
|
||||
|
||||
public Object getRenderLock() {
|
||||
return renderLock;
|
||||
}
|
||||
@@ -272,9 +332,16 @@ public class ScreenBuffer {
|
||||
|
||||
// ========== Cursor ==========
|
||||
public int getCursorAddress() { return cursorAddress; }
|
||||
public synchronized void setCursorAddress(int addr) {
|
||||
this.cursorAddress = addr;
|
||||
this.displayCursorAddress = addr;
|
||||
public void setCursorAddress(int addr) {
|
||||
int oldAddr;
|
||||
synchronized (this) {
|
||||
oldAddr = this.cursorAddress;
|
||||
this.cursorAddress = addr;
|
||||
this.displayCursorAddress = addr;
|
||||
}
|
||||
if (oldAddr != addr) {
|
||||
notifyCursorMoved(oldAddr, addr);
|
||||
}
|
||||
}
|
||||
public synchronized void setCursorPosition(int row, int col) {
|
||||
int r = Math.max(0, Math.min(row, rows - 1));
|
||||
@@ -831,19 +898,28 @@ public class ScreenBuffer {
|
||||
}
|
||||
|
||||
public synchronized void setText(String text) {
|
||||
setText(text, 0);
|
||||
}
|
||||
|
||||
public synchronized void setText(String text, int pos) {
|
||||
if (text == null) return;
|
||||
int size = rows * cols;
|
||||
int len = Math.min(text.length(), size);
|
||||
if (pos < 0 || pos >= size) return;
|
||||
int len = Math.min(text.length(), size - pos);
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = text.charAt(i);
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||
buffer[i].ucs4 = ch;
|
||||
buffer[pos + i].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||
buffer[pos + i].ucs4 = ch;
|
||||
}
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void setText(String text, int row, int col) {
|
||||
setText(text, row * cols + col);
|
||||
}
|
||||
|
||||
public int searchString(String target) {
|
||||
if (target == null || target.isEmpty()) return -1;
|
||||
String full = getText();
|
||||
@@ -1071,12 +1147,16 @@ public class ScreenBuffer {
|
||||
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
|
||||
public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); }
|
||||
|
||||
public void setLeftMargin(int n) { setEntryAssistStartColumn(n); }
|
||||
public void setRightMargin(int n) { setEntryAssistEndColumn(n); }
|
||||
public void setWordTabPositions(int[] stops) { setEntryAssistTabStops(stops); }
|
||||
|
||||
/**
|
||||
* Perform Entry Assist word wrap if typing near/past end margin.
|
||||
* Moves any partial word typed on the current line to the beginning of the next line (docStartCol).
|
||||
*/
|
||||
public synchronized boolean handleWordWrap(int curAddr, char typedChar) {
|
||||
if (!docMode && !wordWrap) return false;
|
||||
if (!wordWrap) return false;
|
||||
int size = rows * cols;
|
||||
if (size <= 0) return false;
|
||||
curAddr = ((curAddr % size) + size) % size;
|
||||
@@ -1115,8 +1195,10 @@ public class ScreenBuffer {
|
||||
ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen];
|
||||
for (int i = 0; i < wordLen; i++) {
|
||||
wordCells[i] = new ExtendedAttribute();
|
||||
wordCells[i].copyFrom(getCell(wordStartAddr + i));
|
||||
getCell(wordStartAddr + i).clear();
|
||||
ExtendedAttribute srcCell = getCell(wordStartAddr + i);
|
||||
wordCells[i].copyFrom(srcCell);
|
||||
srcCell.ec = 0;
|
||||
srcCell.ucs4 = 0;
|
||||
}
|
||||
|
||||
int nextRow = (curRow + 1) % rows;
|
||||
|
||||
@@ -6,6 +6,8 @@ import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -28,6 +30,10 @@ public class TelnetConnection {
|
||||
private final TelnetFSM fsm;
|
||||
private final ConnectionConfig config;
|
||||
|
||||
private final AtomicLong lastActivityTime = new AtomicLong(System.currentTimeMillis());
|
||||
private ScheduledExecutorService keepAliveExecutor;
|
||||
private volatile boolean intentionalDisconnect = false;
|
||||
|
||||
private javax.net.ssl.SSLSession sslSession;
|
||||
|
||||
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||
@@ -91,6 +97,7 @@ public class TelnetConnection {
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
rawSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
applyExtendedSocketOptions(rawSocket);
|
||||
rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs());
|
||||
|
||||
// Perform proxy handshake if configured
|
||||
@@ -123,6 +130,7 @@ public class TelnetConnection {
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
applyExtendedSocketOptions(sslSocket);
|
||||
applyTlsSocketSettings(sslSocket);
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
@@ -143,10 +151,13 @@ public class TelnetConnection {
|
||||
|
||||
log.info("Connected to " + socket.getRemoteSocketAddress());
|
||||
|
||||
intentionalDisconnect = false;
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
running = true;
|
||||
readerThread = new Thread(this::readLoop, "TN3270-Reader");
|
||||
readerThread.setDaemon(true);
|
||||
readerThread.start();
|
||||
startKeepAlive();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,6 +178,7 @@ public class TelnetConnection {
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
applyExtendedSocketOptions(sslSocket);
|
||||
applyTlsSocketSettings(sslSocket);
|
||||
sslSocket.startHandshake();
|
||||
this.socket = sslSocket;
|
||||
@@ -200,6 +212,106 @@ public class TelnetConnection {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyExtendedSocketOptions(Socket s) {
|
||||
if (s == null) return;
|
||||
try {
|
||||
Class<?> extClass = Class.forName("jdk.net.ExtendedSocketOptions");
|
||||
// TCP_KEEPIDLE, TCP_KEEPINTERVAL, TCP_KEEPCOUNT
|
||||
if (config != null && config.isSoKeepAlive() && config.getKeepAliveIntervalSeconds() > 0) {
|
||||
try {
|
||||
java.lang.reflect.Field fIdle = extClass.getField("TCP_KEEPIDLE");
|
||||
@SuppressWarnings("unchecked")
|
||||
SocketOption<Integer> optIdle = (SocketOption<Integer>) fIdle.get(null);
|
||||
s.setOption(optIdle, config.getKeepAliveIntervalSeconds());
|
||||
} catch (Throwable ignored) {}
|
||||
try {
|
||||
java.lang.reflect.Field fIntv = extClass.getField("TCP_KEEPINTERVAL");
|
||||
@SuppressWarnings("unchecked")
|
||||
SocketOption<Integer> optIntv = (SocketOption<Integer>) fIntv.get(null);
|
||||
s.setOption(optIntv, Math.max(1, Math.min(10, config.getKeepAliveIntervalSeconds())));
|
||||
} catch (Throwable ignored) {}
|
||||
try {
|
||||
java.lang.reflect.Field fCnt = extClass.getField("TCP_KEEPCOUNT");
|
||||
@SuppressWarnings("unchecked")
|
||||
SocketOption<Integer> optCnt = (SocketOption<Integer>) fCnt.get(null);
|
||||
s.setOption(optCnt, 3);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
// TCP_USER_TIMEOUT
|
||||
if (config != null && config.getTcpUserTimeoutMs() > 0) {
|
||||
try {
|
||||
java.lang.reflect.Field fTimeout = extClass.getField("TCP_USER_TIMEOUT");
|
||||
@SuppressWarnings("unchecked")
|
||||
SocketOption<Integer> optTimeout = (SocketOption<Integer>) fTimeout.get(null);
|
||||
s.setOption(optTimeout, config.getTcpUserTimeoutMs());
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// Extended socket options not supported on this platform/runtime (e.g. macOS/Android)
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void startKeepAlive() {
|
||||
stopKeepAlive();
|
||||
if (config != null && config.isKeepAliveEnabled() && config.getKeepAliveIntervalSeconds() > 0) {
|
||||
keepAliveExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "TN3270-KeepAlive");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
int interval = config.getKeepAliveIntervalSeconds();
|
||||
long checkPeriod = Math.max(1, Math.min(5, interval));
|
||||
keepAliveExecutor.scheduleWithFixedDelay(this::checkAndSendKeepAlive, checkPeriod, checkPeriod, TimeUnit.SECONDS);
|
||||
log.fine("Keep-Alive heartbeat scheduled every " + interval + "s (check every " + checkPeriod + "s)");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stopKeepAlive() {
|
||||
if (keepAliveExecutor != null) {
|
||||
keepAliveExecutor.shutdownNow();
|
||||
keepAliveExecutor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndSendKeepAlive() {
|
||||
if (!running || !isConnected()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
long elapsed = now - lastActivityTime.get();
|
||||
long intervalMs = (config != null ? config.getKeepAliveIntervalSeconds() : 120) * 1000L;
|
||||
if (elapsed >= intervalMs) {
|
||||
try {
|
||||
sendKeepAliveHeartbeat();
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Failed to send keep-alive heartbeat", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void sendKeepAliveHeartbeat() throws IOException {
|
||||
if (!isConnected() || outputStream == null) {
|
||||
return;
|
||||
}
|
||||
String type = (config != null) ? config.getKeepAliveType() : "NOP";
|
||||
if ("TIMING-MARK".equalsIgnoreCase(type)) {
|
||||
log.fine("Transmitting Keep-Alive heartbeat: IAC DO TIMING-MARK");
|
||||
byte[] tm = new byte[] { (byte) IAC, (byte) DO, (byte) TELOPT_TM };
|
||||
outputStream.write(tm);
|
||||
outputStream.flush();
|
||||
} else {
|
||||
log.fine("Transmitting Keep-Alive heartbeat: IAC NOP");
|
||||
byte[] nop = new byte[] { (byte) IAC, (byte) NOP };
|
||||
outputStream.write(nop);
|
||||
outputStream.flush();
|
||||
}
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public long getLastActivityTime() {
|
||||
return lastActivityTime.get();
|
||||
}
|
||||
|
||||
private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
|
||||
OutputStream out = s.getOutputStream();
|
||||
InputStream in = s.getInputStream();
|
||||
@@ -411,6 +523,7 @@ public class TelnetConnection {
|
||||
if (outputStream == null) return;
|
||||
outputStream.write(data, offset, length);
|
||||
outputStream.flush();
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length));
|
||||
}
|
||||
@@ -432,13 +545,16 @@ public class TelnetConnection {
|
||||
byte[] escaped = out.toByteArray();
|
||||
outputStream.write(escaped, 0, escaped.length);
|
||||
outputStream.flush();
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
intentionalDisconnect = true;
|
||||
running = false;
|
||||
stopKeepAlive();
|
||||
try {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.shutdownInput();
|
||||
@@ -471,10 +587,12 @@ public class TelnetConnection {
|
||||
int n = inputStream.read(buf);
|
||||
if (n < 0) {
|
||||
log.info("Host disconnected (EOF)");
|
||||
fsm.onDisconnect();
|
||||
boolean unexpected = !intentionalDisconnect;
|
||||
fsm.onDisconnect(unexpected);
|
||||
break;
|
||||
}
|
||||
if (n > 0) {
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
||||
}
|
||||
@@ -486,14 +604,23 @@ public class TelnetConnection {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
if (running && !intentionalDisconnect) {
|
||||
log.warning("Socket read timeout (" + (config != null ? config.getSoTimeoutMs() : 0) + "ms): " + e.getMessage());
|
||||
fsm.onDisconnect(true);
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
if (running) {
|
||||
log.info("Socket closed: " + e.getMessage());
|
||||
fsm.onDisconnect();
|
||||
if (running && !intentionalDisconnect) {
|
||||
log.info("Socket closed unexpectedly: " + e.getMessage());
|
||||
fsm.onDisconnect(true);
|
||||
} else if (running) {
|
||||
fsm.onDisconnect(false);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
log.log(Level.WARNING, "Read error", e);
|
||||
if (running && !intentionalDisconnect) {
|
||||
log.log(Level.WARNING, "Read error: " + e.getMessage());
|
||||
fsm.onDisconnect(true);
|
||||
} else if (running) {
|
||||
fsm.onError("Read error: " + e.getMessage());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
@@ -501,6 +628,8 @@ public class TelnetConnection {
|
||||
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
|
||||
fsm.onError("Network loop error: " + t.getMessage());
|
||||
}
|
||||
} finally {
|
||||
stopKeepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public class TelnetFSM {
|
||||
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
|
||||
|
||||
// 3270 input buffer (accumulated between telnet framing)
|
||||
private final ByteArrayOutputStream ibuf = new ByteArrayOutputStream(32768);
|
||||
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
|
||||
|
||||
// Sub-negotiation buffer
|
||||
private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096);
|
||||
@@ -70,14 +70,25 @@ public class TelnetFSM {
|
||||
|
||||
private List<String> getCandidateTerminalTypes() {
|
||||
List<String> list = new ArrayList<>();
|
||||
String currentLu = null;
|
||||
List<String> lus = config.getLuNames();
|
||||
if (lus != null && !lus.isEmpty() && luIndex < lus.size()) {
|
||||
currentLu = lus.get(luIndex);
|
||||
} else if (config.getLuName() != null && !config.getLuName().trim().isEmpty()) {
|
||||
currentLu = config.getLuName().trim();
|
||||
}
|
||||
|
||||
if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) {
|
||||
list.add(config.getTerminalName().trim());
|
||||
String tName = config.getTerminalName().trim();
|
||||
if (currentLu != null && !currentLu.isEmpty() && !tName.contains("@")) {
|
||||
list.add(tName + "@" + currentLu);
|
||||
list.add(tName);
|
||||
} else {
|
||||
list.add(tName);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
if (config.isDynamicModel()) {
|
||||
if (config.isExtendedDataStream()) {
|
||||
list.add("IBM-DYNAMIC-E");
|
||||
}
|
||||
list.add("IBM-DYNAMIC");
|
||||
list.add("IBM-3279-4-E");
|
||||
list.add("IBM-3279-4");
|
||||
@@ -85,24 +96,41 @@ public class TelnetFSM {
|
||||
list.add("IBM-3279-2");
|
||||
list.add("IBM-3278-2");
|
||||
list.add("UNKNOWN");
|
||||
return list;
|
||||
} else {
|
||||
TerminalModel model = config.getModel();
|
||||
list.add(model.getTerminalType());
|
||||
list.add(model.getBaseTerminalType());
|
||||
if (model.isColor()) {
|
||||
try {
|
||||
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
|
||||
list.add(mono.getTerminalType());
|
||||
list.add(mono.getBaseTerminalType());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
if (model.getModelNumber() != 2) {
|
||||
list.add("IBM-3279-2-E");
|
||||
list.add("IBM-3279-2");
|
||||
list.add("IBM-3278-2");
|
||||
}
|
||||
list.add("UNKNOWN");
|
||||
}
|
||||
TerminalModel model = config.getModel();
|
||||
list.add(model.getTerminalType());
|
||||
list.add(model.getBaseTerminalType());
|
||||
if (model.isColor()) {
|
||||
try {
|
||||
TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false);
|
||||
list.add(mono.getTerminalType());
|
||||
list.add(mono.getBaseTerminalType());
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
if (currentLu != null && !currentLu.isEmpty()) {
|
||||
List<String> luCandidates = new ArrayList<>();
|
||||
for (String item : list) {
|
||||
if (!"UNKNOWN".equalsIgnoreCase(item) && !item.contains("@")) {
|
||||
luCandidates.add(item + "@" + currentLu);
|
||||
}
|
||||
}
|
||||
for (String item : list) {
|
||||
if (!"UNKNOWN".equalsIgnoreCase(item)) {
|
||||
luCandidates.add(item);
|
||||
}
|
||||
}
|
||||
luCandidates.add("UNKNOWN");
|
||||
return luCandidates;
|
||||
}
|
||||
if (model.getModelNumber() != 2) {
|
||||
list.add("IBM-3279-2-E");
|
||||
list.add("IBM-3279-2");
|
||||
list.add("IBM-3278-2");
|
||||
}
|
||||
list.add("UNKNOWN");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -123,6 +151,11 @@ public class TelnetFSM {
|
||||
private String connectedLu;
|
||||
private String connectedType;
|
||||
|
||||
// Phase 10: Contention Resolution & Auto-Unlock State
|
||||
private boolean sdi_flag = false;
|
||||
private boolean kri_flag = false;
|
||||
private boolean negotiateContentionResolution = true;
|
||||
|
||||
public enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
|
||||
|
||||
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
|
||||
@@ -193,7 +226,7 @@ public class TelnetFSM {
|
||||
eFuncs[FUNC_SYSREQ] = true;
|
||||
eFuncs[FUNC_SNA_SENSE] = true;
|
||||
eFuncs[FUNC_DATA_STREAM_CTL] = true;
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = negotiateContentionResolution;
|
||||
|
||||
statusDisplay(STATUS_CONNECTING, "Connecting to host");
|
||||
changeState(ConnectionState.TELNET_PENDING);
|
||||
@@ -587,6 +620,9 @@ public class TelnetFSM {
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
if (termType.contains("@")) {
|
||||
connectedLu = termType.substring(termType.indexOf('@') + 1).trim();
|
||||
}
|
||||
log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
|
||||
}
|
||||
}
|
||||
@@ -954,6 +990,10 @@ public class TelnetFSM {
|
||||
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
|
||||
log.info("TN3270E negotiation complete");
|
||||
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.setContentionResolution(isContentionResolutionNegotiated());
|
||||
}
|
||||
|
||||
// RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered
|
||||
// to be bound immediately upon completion of the FUNCTIONS negotiation.
|
||||
if (eFuncs[FUNC_BIND_IMAGE]) {
|
||||
@@ -966,6 +1006,7 @@ public class TelnetFSM {
|
||||
// Notify listeners
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
l.onTN3270ENegotiated(connectedType, connectedLu);
|
||||
l.onTN3270EFunctionsNegotiated(eFuncs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -983,9 +1024,9 @@ public class TelnetFSM {
|
||||
// ========== End of Record processing ==========
|
||||
|
||||
private void processEndOfRecord() {
|
||||
byte[] data = ibuf.toByteArray();
|
||||
ibuf.reset();
|
||||
if (data.length == 0) return;
|
||||
int dataLen = ibuf.size();
|
||||
if (dataLen == 0) return;
|
||||
byte[] data = ibuf.buffer();
|
||||
|
||||
if ((connectionState == ConnectionState.TELNET_PENDING ||
|
||||
connectionState == ConnectionState.CONNECTED_NVT ||
|
||||
@@ -996,28 +1037,89 @@ public class TelnetFSM {
|
||||
|
||||
if (tn3270eNegotiated) {
|
||||
// TN3270E mode: data starts with 5-byte header
|
||||
processTN3270ERecord(data);
|
||||
processTN3270ERecord(data, 0, dataLen);
|
||||
} else {
|
||||
// Plain TN3270 mode: data is raw 3270 data stream
|
||||
dsProcessor.processRecord(data, 0, data.length, true);
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.processRecord(data, 0, dataLen, false);
|
||||
}
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
ibuf.reset();
|
||||
|
||||
// Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR
|
||||
if (dsProcessor != null) {
|
||||
boolean crActive = isContentionResolutionNegotiated();
|
||||
if (crActive) {
|
||||
if (this.sdi_flag && !dsProcessor.isRcvdRead()) {
|
||||
if (dsProcessor.getInputProcessor() != null) {
|
||||
dsProcessor.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
this.sdi_flag = false;
|
||||
dsProcessor.setUnlockPending(false);
|
||||
if (dsProcessor.isUnlockSysPending() || this.kri_flag) {
|
||||
if (dsProcessor.getInputProcessor() != null && dsProcessor.getInputProcessor().getOIA() != null) {
|
||||
dsProcessor.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
this.kri_flag = false;
|
||||
dsProcessor.setUnlockSysPending(false);
|
||||
notifyKeyboardUnlocked();
|
||||
}
|
||||
this.kri_flag = false;
|
||||
} else {
|
||||
// Contention Resolution is not active
|
||||
boolean autoSysUnlock = (config != null) ? config.isAutoSysUnlock() : true;
|
||||
if (autoSysUnlock && !dsProcessor.isRcvdRead()) {
|
||||
boolean sysLocked = false;
|
||||
if (dsProcessor.getInputProcessor() != null) {
|
||||
if (dsProcessor.getInputProcessor().isKeyboardLocked() ||
|
||||
(dsProcessor.getInputProcessor().getOIA() != null && dsProcessor.getInputProcessor().getOIA().isXSystem())) {
|
||||
sysLocked = true;
|
||||
}
|
||||
}
|
||||
if (sysLocked) {
|
||||
if (dsProcessor.getInputProcessor() != null) {
|
||||
dsProcessor.getInputProcessor().setKeyboardLocked(false);
|
||||
if (dsProcessor.getInputProcessor().getOIA() != null) {
|
||||
dsProcessor.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
dsProcessor.setUnlockSysPending(false);
|
||||
notifyKeyboardUnlocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sdi_flag = false;
|
||||
this.kri_flag = false;
|
||||
dsProcessor.setRcvdRead(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void processTn3270eHeader(byte[] data) {
|
||||
processTN3270ERecord(data);
|
||||
processTN3270ERecord(data, 0, data != null ? data.length : 0);
|
||||
}
|
||||
|
||||
private void processTN3270ERecord(byte[] data) {
|
||||
if (data.length < EH_SIZE) {
|
||||
log.warning("TN3270E record too short: " + data.length);
|
||||
processTN3270ERecord(data, 0, data != null ? data.length : 0);
|
||||
}
|
||||
|
||||
private void processTN3270ERecord(byte[] data, int offset, int length) {
|
||||
if (data == null || length < EH_SIZE) {
|
||||
log.warning("TN3270E record too short: " + length);
|
||||
return;
|
||||
}
|
||||
|
||||
int dataType = data[0] & 0xFF;
|
||||
int requestFlag = data[1] & 0xFF;
|
||||
int responseFlag = data[2] & 0xFF;
|
||||
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF);
|
||||
int dataType = data[offset] & 0xFF;
|
||||
int requestFlag = data[offset + 1] & 0xFF;
|
||||
int responseFlag = data[offset + 2] & 0xFF;
|
||||
int seqNumber = ((data[offset + 3] & 0xFF) << 8) | (data[offset + 4] & 0xFF);
|
||||
|
||||
this.sdi_flag = (requestFlag & 0x01) != 0;
|
||||
this.kri_flag = (requestFlag & 0x02) != 0;
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.setContentionResolution(isContentionResolutionNegotiated());
|
||||
}
|
||||
|
||||
log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) +
|
||||
" rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber);
|
||||
@@ -1026,7 +1128,7 @@ public class TelnetFSM {
|
||||
|
||||
switch (dataType) {
|
||||
case DT_3270_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
if (length > EH_SIZE) {
|
||||
// Transition to 3270 mode from any non-3270 state (E_NVT, UNBOUND, SSCP)
|
||||
if (connectionState != ConnectionState.CONNECTED_TN3270E) {
|
||||
// Clear screen on transition to 3270 mode from unbound/SSCP/NVT
|
||||
@@ -1036,7 +1138,7 @@ public class TelnetFSM {
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
}
|
||||
try {
|
||||
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true);
|
||||
dsProcessor.processRecord(data, offset + EH_SIZE, length - EH_SIZE, false);
|
||||
notifyScreenUpdate();
|
||||
// Send positive response if required
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
@@ -1056,9 +1158,9 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case DT_SCS_DATA:
|
||||
if (data.length > EH_SIZE) {
|
||||
if (length > EH_SIZE) {
|
||||
try {
|
||||
processSCSInbound(data, EH_SIZE, data.length - EH_SIZE);
|
||||
processSCSInbound(data, offset + EH_SIZE, length - EH_SIZE);
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
@@ -1085,9 +1187,9 @@ public class TelnetFSM {
|
||||
changeState(ConnectionState.CONNECTED_SSCP);
|
||||
tn3270eSubmode = TN3270ESubmode.E_SSCP;
|
||||
}
|
||||
if (data.length > EH_SIZE) {
|
||||
if (length > EH_SIZE) {
|
||||
try {
|
||||
dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE);
|
||||
dsProcessor.processSscpLuData(data, offset + EH_SIZE, length - EH_SIZE);
|
||||
notifyScreenUpdate();
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
@@ -1106,11 +1208,15 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case DT_BIND_IMAGE:
|
||||
process_bind(data, responseFlag, seqNumber);
|
||||
{
|
||||
byte[] bindData = new byte[length];
|
||||
System.arraycopy(data, offset, bindData, 0, length);
|
||||
process_bind(bindData, responseFlag, seqNumber);
|
||||
}
|
||||
break;
|
||||
|
||||
case DT_UNBIND:
|
||||
int unbindReason = (data.length > EH_SIZE) ? (data[EH_SIZE] & 0xFF) : UNBIND_NORMAL;
|
||||
int unbindReason = (length > EH_SIZE) ? (data[offset + EH_SIZE] & 0xFF) : UNBIND_NORMAL;
|
||||
process_unbind(unbindReason, responseFlag, seqNumber);
|
||||
break;
|
||||
|
||||
@@ -1123,9 +1229,9 @@ public class TelnetFSM {
|
||||
if (dsProcessor != null && dsProcessor.getInputProcessor() != null) {
|
||||
dsProcessor.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
if (data.length > EH_SIZE) {
|
||||
if (length > EH_SIZE) {
|
||||
try {
|
||||
processNVTData(data, EH_SIZE, data.length - EH_SIZE);
|
||||
processNVTData(data, offset + EH_SIZE, length - EH_SIZE);
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
sendTN3270EPositiveResponse(seqNumber);
|
||||
}
|
||||
@@ -1180,12 +1286,12 @@ public class TelnetFSM {
|
||||
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
|
||||
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
|
||||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 ||
|
||||
dataType == 0x0D || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
|
||||
dataType == 0x0D || (length >= 2 && (data[offset] & 0xFF) == 0x11)) {
|
||||
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
|
||||
") in TN3270E mode — automatically switching to plain TN3270 mode");
|
||||
tn3270eNegotiated = false;
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
dsProcessor.processRecord(data, 0, data.length, true);
|
||||
dsProcessor.processRecord(data, offset, length, true);
|
||||
notifyScreenUpdate();
|
||||
} else {
|
||||
log.info("Unhandled TN3270E data type: " + dataType);
|
||||
@@ -1194,6 +1300,7 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void processSCSInbound(byte[] data) {
|
||||
if (data == null) return;
|
||||
processSCSInbound(data, 0, data.length);
|
||||
@@ -1226,7 +1333,7 @@ public class TelnetFSM {
|
||||
}
|
||||
|
||||
/**
|
||||
* HoD 5-byte send_response compatible signature (com.ibm.eNetwork.ECL.tn3270.Telnet3270E).
|
||||
* HoD 5-byte send_response compatible signature.
|
||||
*/
|
||||
public void send_response(short s, short s2, int n) {
|
||||
byte[] byArray = new byte[5];
|
||||
@@ -1426,8 +1533,49 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
public void setConnectionState(ConnectionState newState) {
|
||||
changeState(newState);
|
||||
}
|
||||
|
||||
public void onDisconnect() {
|
||||
changeState(ConnectionState.NOT_CONNECTED);
|
||||
onDisconnect(false);
|
||||
}
|
||||
|
||||
public void onDisconnect(boolean unexpected) {
|
||||
if (unexpected && config != null && config.isAutoReconnect()) {
|
||||
log.info("Unexpected connection loss — transitioning to RECONNECTING");
|
||||
changeState(ConnectionState.RECONNECTING);
|
||||
} else {
|
||||
changeState(ConnectionState.NOT_CONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanly reset Telnet and TN3270E session state prior to reconnecting.
|
||||
*/
|
||||
public synchronized void resetSessionState() {
|
||||
state = TNS_DATA;
|
||||
java.util.Arrays.fill(myOpts, false);
|
||||
java.util.Arrays.fill(hisOpts, false);
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
tn3270eNegotiated = false;
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
tn3270eBound = false;
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
eXmitSeq = 0;
|
||||
lastRcvSeq = 0;
|
||||
lastRespType = 0;
|
||||
lastRespCode = 0;
|
||||
responseRequired = RSF_NO_RESPONSE;
|
||||
deferredWillTtype = false;
|
||||
tn3270eDeviceTypeSent = false;
|
||||
ttypeIndex = 0;
|
||||
luIndex = 0;
|
||||
connectedLu = null;
|
||||
connectedType = null;
|
||||
sdi_flag = false;
|
||||
kri_flag = false;
|
||||
}
|
||||
|
||||
public void onError(String message) {
|
||||
@@ -1437,7 +1585,10 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyScreenUpdate() {
|
||||
public void notifyScreenUpdate() {
|
||||
if (screenBuffer != null) {
|
||||
screenBuffer.notifyScreenUpdate();
|
||||
}
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
@@ -1813,4 +1964,41 @@ public class TelnetFSM {
|
||||
public boolean isFunctionNegotiated(int func) {
|
||||
return func >= 0 && func < eFuncs.length && eFuncs[func];
|
||||
}
|
||||
|
||||
public boolean isContentionResolutionNegotiated() {
|
||||
return tn3270eNegotiated && eFuncs[FUNC_CONTENTION_RESOLUTION];
|
||||
}
|
||||
|
||||
public boolean isNegotiateContentionResolution() {
|
||||
return negotiateContentionResolution;
|
||||
}
|
||||
|
||||
public void setNegotiateContentionResolution(boolean neg) {
|
||||
this.negotiateContentionResolution = neg;
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = neg;
|
||||
}
|
||||
|
||||
public void setContentionResolutionNegotiated(boolean cr) {
|
||||
if (cr) {
|
||||
this.tn3270eNegotiated = true;
|
||||
}
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = cr;
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.setContentionResolution(cr);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSdi_flag() { return sdi_flag; }
|
||||
public void setSdi_flag(boolean sdi) { this.sdi_flag = sdi; }
|
||||
|
||||
public boolean isKri_flag() { return kri_flag; }
|
||||
public void setKri_flag(boolean kri) { this.kri_flag = kri; }
|
||||
|
||||
public void notifyKeyboardUnlocked() {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
try {
|
||||
l.onKeyboardUnlocked();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,14 @@ public class DS3270 {
|
||||
public boolean suppressClearEvent;
|
||||
public boolean wsfvalid = true;
|
||||
|
||||
// Phase 10: State tracking matching HoD DS3270
|
||||
protected boolean autoSysUnlock = true;
|
||||
protected boolean sdi_flag = false;
|
||||
protected boolean kri_flag = false;
|
||||
protected boolean unlock_pending = false;
|
||||
protected boolean unlock_sys_pending = false;
|
||||
protected boolean rcvdRead = false;
|
||||
|
||||
// Underlying lib3270j data stream processor
|
||||
private final DataStreamProcessor delegate;
|
||||
private ECLSession session;
|
||||
@@ -172,9 +180,15 @@ public class DS3270 {
|
||||
public DS3270(ECLSession session, ECLPS ps) {
|
||||
this.session = session;
|
||||
this.ps = ps;
|
||||
if (session != null) {
|
||||
this.autoSysUnlock = session.isAutoSysUnlock();
|
||||
}
|
||||
ScreenBuffer sb = (ps != null) ? ps.getScreenBuffer() : new ScreenBuffer();
|
||||
EbcdicTranslator trans = (ps != null) ? ps.getTranslator() : new EbcdicTranslator();
|
||||
this.delegate = new DataStreamProcessor(sb, trans);
|
||||
if (session != null) {
|
||||
this.delegate.setAutoSysUnlock(session.isAutoSysUnlock());
|
||||
}
|
||||
if (ps != null && ps.getInputProcessor() != null) {
|
||||
this.delegate.setInputProcessor(ps.getInputProcessor());
|
||||
}
|
||||
@@ -214,8 +228,135 @@ public class DS3270 {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() {
|
||||
return autoSysUnlock;
|
||||
}
|
||||
|
||||
public void setAutoSysUnlock(boolean autoSysUnlock) {
|
||||
this.autoSysUnlock = autoSysUnlock;
|
||||
if (delegate != null) {
|
||||
delegate.setAutoSysUnlock(autoSysUnlock);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSdi_flag() { return sdi_flag; }
|
||||
public void setSdi_flag(boolean sdi) { this.sdi_flag = sdi; }
|
||||
|
||||
public boolean isKri_flag() { return kri_flag; }
|
||||
public void setKri_flag(boolean kri) { this.kri_flag = kri; }
|
||||
|
||||
public boolean isUnlock_pending() { return unlock_pending; }
|
||||
public void setUnlock_pending(boolean pending) { this.unlock_pending = pending; }
|
||||
|
||||
public boolean isUnlock_sys_pending() { return unlock_sys_pending; }
|
||||
public void setUnlock_sys_pending(boolean pending) { this.unlock_sys_pending = pending; }
|
||||
|
||||
public boolean isRcvdRead() {
|
||||
return rcvdRead || (delegate != null && delegate.isRcvdRead());
|
||||
}
|
||||
public void setRcvdRead(boolean rcvd) {
|
||||
this.rcvdRead = rcvd;
|
||||
if (delegate != null) delegate.setRcvdRead(rcvd);
|
||||
}
|
||||
|
||||
public void receiveHeaderData(short s, short s2, short s3, int n) {
|
||||
this.sdi_flag = (s2 & request_bit_SDI) != 0;
|
||||
this.kri_flag = (s2 & request_bit_KRI) != 0;
|
||||
}
|
||||
|
||||
public void receiveHeader(short s, short s2, short s3, int n) {
|
||||
receiveHeaderData(s, s2, s3, n);
|
||||
}
|
||||
|
||||
public void endOfRecord() {
|
||||
log.fine("DS3270 endOfRecord");
|
||||
boolean crActive = (session != null && session.getContentionResolution());
|
||||
if (crActive) {
|
||||
if (this.sdi_flag && !isRcvdRead()) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(7);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
this.sdi_flag = false;
|
||||
this.unlock_pending = false;
|
||||
if (this.unlock_sys_pending || this.kri_flag) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(8);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
if (delegate.getInputProcessor().getOIA() != null) {
|
||||
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.kri_flag = false;
|
||||
this.unlock_sys_pending = false;
|
||||
}
|
||||
if (this.ps != null) {
|
||||
this.ps.dispatchEvent(new haus.nightmare.lib3270j.ecl.ECLPSEvent(this.ps, haus.nightmare.lib3270j.ecl.ECLPSEvent.EVENT_KEY_UNLOCKED));
|
||||
}
|
||||
} else {
|
||||
// Contention resolution is not active
|
||||
if (this.unlock_pending && !isRcvdRead()) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(7);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
if (this.autoSysUnlock) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(8);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
if (delegate.getInputProcessor().getOIA() != null) {
|
||||
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
this.unlock_sys_pending = false;
|
||||
}
|
||||
this.unlock_pending = false;
|
||||
}
|
||||
if (this.unlock_sys_pending && !isRcvdRead()) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(8);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
if (delegate.getInputProcessor().getOIA() != null) {
|
||||
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
this.unlock_sys_pending = false;
|
||||
}
|
||||
if (this.autoSysUnlock && !isRcvdRead()) {
|
||||
boolean sysLocked = false;
|
||||
if (this.ps != null && this.ps.islocked_SYSLOCK()) {
|
||||
sysLocked = true;
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
if (delegate.getInputProcessor().isKeyboardLocked() ||
|
||||
(delegate.getInputProcessor().getOIA() != null && delegate.getInputProcessor().getOIA().isXSystem())) {
|
||||
sysLocked = true;
|
||||
}
|
||||
}
|
||||
if (sysLocked) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(8);
|
||||
}
|
||||
if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
if (delegate.getInputProcessor().getOIA() != null) {
|
||||
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
this.unlock_sys_pending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.rcvdRead = false;
|
||||
if (delegate != null) delegate.setRcvdRead(false);
|
||||
this.unlock_pending = false;
|
||||
this.sdi_flag = false;
|
||||
this.kri_flag = false;
|
||||
}
|
||||
|
||||
public int receiveData(short[] sArray, int off, int len) {
|
||||
@@ -233,6 +374,10 @@ public class DS3270 {
|
||||
|
||||
public void processWCC(short wcc) {
|
||||
delegate.processWCC(wcc);
|
||||
if ((wcc & WCC_RESTORE) > 0) {
|
||||
this.unlock_pending = true;
|
||||
this.unlock_sys_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void processSBA(int baddr) {
|
||||
|
||||
@@ -26,8 +26,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Core 3270 File Transfer Controller conforming to IBM Host On-Demand
|
||||
* (com.ibm.eNetwork.ECL.xfer3270.Xfer3270).
|
||||
* Core 3270 File Transfer Controller conforming to Host On-Demand specifications.
|
||||
*
|
||||
* Implements FileTransferInterface and handles TSO/CMS/CICS IND$FILE options,
|
||||
* dynamic MTU buffering, host/local dataset name mappings, directory queries,
|
||||
@@ -37,6 +36,13 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
|
||||
private static final Logger log = Logger.getLogger(Xfer3270.class.getName());
|
||||
|
||||
private static final Pattern RECFM_PATTERN = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?");
|
||||
private static final Pattern LRECL_PATTERN = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern BLK_PATTERN = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern SPACE_PATTERN = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?");
|
||||
private static final Pattern AVB_PATTERN = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?");
|
||||
private static final Pattern MTU_PATTERN = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?");
|
||||
|
||||
public static final String UNICODE_UCS2_STR = "UCS2";
|
||||
public static final String UNICODE_UTF8_STR = "UTF8";
|
||||
public static final String UNICODE_UTF_8_STR = "UTF-8";
|
||||
@@ -175,20 +181,20 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
}
|
||||
|
||||
// Mainframe dataset parameters
|
||||
Matcher recfmMatcher = Pattern.compile("(?i)RECFM[\\s\\(]+([FVU]|FIXED|VARIABLE|UNDEFINED)\\)?").matcher(options);
|
||||
Matcher recfmMatcher = RECFM_PATTERN.matcher(options);
|
||||
if (recfmMatcher.find()) this.recfm = recfmMatcher.group(1).toUpperCase();
|
||||
|
||||
Matcher lreclMatcher = Pattern.compile("(?i)LRECL[\\s\\(]+(\\d+)\\)?").matcher(options);
|
||||
Matcher lreclMatcher = LRECL_PATTERN.matcher(options);
|
||||
if (lreclMatcher.find()) {
|
||||
try { this.lrecl = Integer.parseInt(lreclMatcher.group(1)); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher blkMatcher = Pattern.compile("(?i)(?:BLKSIZE|BLOCK)[\\s\\(]+(\\d+)\\)?").matcher(options);
|
||||
Matcher blkMatcher = BLK_PATTERN.matcher(options);
|
||||
if (blkMatcher.find()) {
|
||||
try { this.blksize = Integer.parseInt(blkMatcher.group(1)); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher spaceMatcher = Pattern.compile("(?i)SPACE[\\s\\(]+(\\d+)(?:[\\s,]+(\\d+))?\\)?").matcher(options);
|
||||
Matcher spaceMatcher = SPACE_PATTERN.matcher(options);
|
||||
if (spaceMatcher.find()) {
|
||||
try {
|
||||
this.primarySpace = Integer.parseInt(spaceMatcher.group(1));
|
||||
@@ -198,7 +204,7 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
Matcher avbMatcher = Pattern.compile("(?i)AVBLOCK[\\s\\(]+(\\d+)\\)?").matcher(options);
|
||||
Matcher avbMatcher = AVB_PATTERN.matcher(options);
|
||||
if (avbMatcher.find()) {
|
||||
try {
|
||||
this.avblock = Integer.parseInt(avbMatcher.group(1));
|
||||
@@ -212,7 +218,7 @@ public class Xfer3270 implements FileTransferInterface {
|
||||
this.spaceUnits = "CYLINDERS";
|
||||
}
|
||||
|
||||
Matcher mtuMatcher = Pattern.compile("(?i)(?:BUFFERSIZE|MTU|BUFSIZE)[\\s\\(]+(\\d+)\\)?").matcher(options);
|
||||
Matcher mtuMatcher = MTU_PATTERN.matcher(options);
|
||||
if (mtuMatcher.find()) {
|
||||
try {
|
||||
SetMTUSize(Integer.parseInt(mtuMatcher.group(1)));
|
||||
|
||||
+116
@@ -82,4 +82,120 @@ public class DataStreamProcessorTest {
|
||||
assertNotNull(sentData.get());
|
||||
assertEquals((byte) AID_ENTER, sentData.get()[0], "ReadBuffer must transmit operator's stored AID");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterAttributesPersistAcrossStartField() throws java.io.IOException {
|
||||
// User's exact ISPF Option 0 sequence:
|
||||
// SBA(7, 1) -> SF(prot,skip) -> SA(yellow) -> ' 4 ' -> SBA(7, 7) -> SF(prot,skip) -> 'DISPLAY'
|
||||
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||
stream.write(CMD_EW);
|
||||
stream.write(0xC3); // WCC
|
||||
|
||||
// SBA(7, 1) -> address 7*80 + 1 = 561
|
||||
byte[] addr1 = encodeAddress(561, 24, 80);
|
||||
stream.write(ORDER_SBA);
|
||||
stream.write(addr1);
|
||||
|
||||
// SF(prot, skip)
|
||||
stream.write(ORDER_SF);
|
||||
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
|
||||
|
||||
// SA(yellow) -> XA_FOREGROUND, COLOR_YELLOW (0xF6)
|
||||
stream.write(ORDER_SA);
|
||||
stream.write(XA_FOREGROUND);
|
||||
stream.write(0xF6);
|
||||
|
||||
// Data: ' 4 '
|
||||
stream.write(translator.stringToEbcdic(" 4 "));
|
||||
|
||||
// SBA(7, 7) -> address 7*80 + 7 = 567
|
||||
byte[] addr2 = encodeAddress(567, 24, 80);
|
||||
stream.write(ORDER_SBA);
|
||||
stream.write(addr2);
|
||||
|
||||
// SF(prot, skip)
|
||||
stream.write(ORDER_SF);
|
||||
stream.write(FA_PRINTABLE | FA_PROTECT | FA_NUMERIC);
|
||||
|
||||
// Data: 'DISPLAY'
|
||||
stream.write(translator.stringToEbcdic("DISPLAY"));
|
||||
|
||||
byte[] record = stream.toByteArray();
|
||||
processor.processRecord(record, 0, record.length, true);
|
||||
|
||||
// Positions 562..566 (' 4 ') must be yellow (0xF6)
|
||||
for (int i = 562; i <= 566; i++) {
|
||||
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should have yellow foreground (0xF6)");
|
||||
}
|
||||
|
||||
// Positions 568..574 ('DISPLAY') across the second SF must also retain yellow (0xF6)
|
||||
for (int i = 568; i <= 574; i++) {
|
||||
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " ('DISPLAY') should retain yellow foreground (0xF6)");
|
||||
}
|
||||
|
||||
// Now test that SA with XA_ALL resets character attributes to default (0)
|
||||
java.io.ByteArrayOutputStream resetStream = new java.io.ByteArrayOutputStream();
|
||||
resetStream.write(CMD_W);
|
||||
resetStream.write(0xC3);
|
||||
resetStream.write(ORDER_SA);
|
||||
resetStream.write(XA_ALL);
|
||||
resetStream.write(0x00);
|
||||
resetStream.write(translator.stringToEbcdic("TEST"));
|
||||
byte[] resetRecord = resetStream.toByteArray();
|
||||
processor.processRecord(resetRecord, 0, resetRecord.length, true);
|
||||
|
||||
for (int i = 575; i < 579; i++) {
|
||||
assertEquals((byte) 0, screen.getCell(i).fg, "Cell at " + i + " should have default foreground (0) after SA(XA_ALL)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterAttributesPersistAcrossStartFieldExtended() throws java.io.IOException {
|
||||
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||
stream.write(CMD_EW);
|
||||
stream.write(0xC3);
|
||||
|
||||
// SA(yellow)
|
||||
stream.write(ORDER_SA);
|
||||
stream.write(XA_FOREGROUND);
|
||||
stream.write(0xF6);
|
||||
|
||||
// SFE with 1 pair (3270 FA)
|
||||
stream.write(ORDER_SFE);
|
||||
stream.write(0x01); // 1 pair
|
||||
stream.write(XA_3270);
|
||||
stream.write(FA_PRINTABLE);
|
||||
|
||||
// Data 'HELLO'
|
||||
stream.write(translator.stringToEbcdic("HELLO"));
|
||||
|
||||
byte[] record = stream.toByteArray();
|
||||
processor.processRecord(record, 0, record.length, true);
|
||||
|
||||
// Check cells of HELLO (positions 1..5) have fg == 0xF6
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
assertEquals((byte) 0xF6, screen.getCell(i).fg, "Cell at " + i + " should retain yellow foreground across SFE");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterAttributesResetOnErase() throws java.io.IOException {
|
||||
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
||||
stream.write(CMD_EW);
|
||||
stream.write(0xC3);
|
||||
stream.write(ORDER_SA);
|
||||
stream.write(XA_FOREGROUND);
|
||||
stream.write(0xF6);
|
||||
stream.write(translator.stringToEbcdic("A"));
|
||||
processor.processRecord(stream.toByteArray(), 0, stream.size(), true);
|
||||
assertEquals((byte) 0xF6, screen.getCell(0).fg);
|
||||
|
||||
// New EW command resets attributes
|
||||
java.io.ByteArrayOutputStream ewStream = new java.io.ByteArrayOutputStream();
|
||||
ewStream.write(CMD_EW);
|
||||
ewStream.write(0xC3);
|
||||
ewStream.write(translator.stringToEbcdic("B"));
|
||||
processor.processRecord(ewStream.toByteArray(), 0, ewStream.size(), true);
|
||||
assertEquals((byte) 0, screen.getCell(0).fg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for high-throughput zero-allocation buffer primitives.
|
||||
*/
|
||||
public class FastByteBufferTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
ReusableByteBufferPool.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FastByteBuffer operations: write, grow, reset, slice, and direct array access")
|
||||
public void testFastByteBufferBasicOperations() {
|
||||
FastByteBuffer buf = new FastByteBuffer(16);
|
||||
assertEquals(0, buf.size());
|
||||
assertTrue(buf.buffer().length >= 16);
|
||||
|
||||
buf.write(0x11);
|
||||
buf.write(0x22);
|
||||
assertEquals(2, buf.size());
|
||||
assertEquals((byte) 0x11, buf.buffer()[0]);
|
||||
assertEquals((byte) 0x22, buf.buffer()[1]);
|
||||
|
||||
byte[] payload = new byte[]{0x01, 0x02, 0x03, 0x04, 0x05};
|
||||
buf.write(payload, 0, payload.length);
|
||||
assertEquals(7, buf.size());
|
||||
|
||||
buf.write(payload, 1, 3); // write 0x02, 0x03, 0x04
|
||||
assertEquals(10, buf.size());
|
||||
|
||||
// Test auto-growth
|
||||
byte[] largeData = new byte[100];
|
||||
for (int i = 0; i < largeData.length; i++) {
|
||||
largeData[i] = (byte) (i & 0xFF);
|
||||
}
|
||||
buf.write(largeData, 0, largeData.length);
|
||||
assertEquals(110, buf.size());
|
||||
assertTrue(buf.buffer().length >= 110);
|
||||
|
||||
// Verify direct array access
|
||||
byte[] raw = buf.buffer();
|
||||
assertNotNull(raw);
|
||||
assertEquals((byte) 0x11, raw[0]);
|
||||
assertEquals((byte) 0x22, raw[1]);
|
||||
|
||||
// Verify ByteBuffer view
|
||||
ByteBuffer readOnly = buf.asByteBuffer();
|
||||
assertEquals(110, readOnly.remaining());
|
||||
assertEquals((byte) 0x11, readOnly.get());
|
||||
|
||||
// Verify ByteBuffer slice
|
||||
ByteBuffer slice = buf.slice(2, 5);
|
||||
assertEquals(5, slice.remaining());
|
||||
assertEquals((byte) 0x01, slice.get(0));
|
||||
assertEquals((byte) 0x05, slice.get(4));
|
||||
|
||||
// Verify copied array
|
||||
byte[] copied = buf.toByteArray();
|
||||
assertEquals(110, copied.length);
|
||||
assertEquals((byte) 0x11, copied[0]);
|
||||
|
||||
// Test reset
|
||||
buf.reset();
|
||||
assertEquals(0, buf.size());
|
||||
// Buffer retained for zero-allocation reuse
|
||||
assertTrue(buf.buffer().length >= 110);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ReusableByteBufferPool acquires, releases, and recycles tiered buffers")
|
||||
public void testByteBufferPoolRecycling() {
|
||||
byte[] small1 = ReusableByteBufferPool.acquire(256);
|
||||
assertEquals(ReusableByteBufferPool.SIZE_SMALL, small1.length);
|
||||
|
||||
byte[] medium1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_MEDIUM);
|
||||
assertEquals(ReusableByteBufferPool.SIZE_MEDIUM, medium1.length);
|
||||
|
||||
byte[] large1 = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_LARGE);
|
||||
assertEquals(ReusableByteBufferPool.SIZE_LARGE, large1.length);
|
||||
|
||||
// Non-standard oversized buffer
|
||||
byte[] huge = ReusableByteBufferPool.acquire(65536);
|
||||
assertEquals(65536, huge.length);
|
||||
|
||||
// Release back to pool
|
||||
ReusableByteBufferPool.release(small1);
|
||||
ReusableByteBufferPool.release(medium1);
|
||||
ReusableByteBufferPool.release(large1);
|
||||
ReusableByteBufferPool.release(huge);
|
||||
|
||||
// Next acquire should reuse the released instances
|
||||
byte[] small2 = ReusableByteBufferPool.acquire(128);
|
||||
assertSame(small1, small2, "Small buffer should be recycled from pool");
|
||||
|
||||
byte[] medium2 = ReusableByteBufferPool.acquire(2048);
|
||||
assertSame(medium1, medium2, "Medium buffer should be recycled from pool");
|
||||
|
||||
byte[] large2 = ReusableByteBufferPool.acquire(30000);
|
||||
assertSame(large1, large2, "Large buffer should be recycled from pool");
|
||||
|
||||
ByteBuffer bb = ReusableByteBufferPool.acquireByteBuffer(ReusableByteBufferPool.SIZE_SMALL);
|
||||
assertNotNull(bb);
|
||||
assertEquals(ReusableByteBufferPool.SIZE_SMALL, bb.capacity());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ReusableByteBufferPool is safe under concurrent acquisition and release")
|
||||
public void testConcurrentPoolAccess() throws InterruptedException {
|
||||
int threads = 8;
|
||||
int iterations = 1000;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threads);
|
||||
CountDownLatch latch = new CountDownLatch(threads);
|
||||
AtomicInteger failures = new AtomicInteger(0);
|
||||
|
||||
for (int t = 0; t < threads; t++) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
byte[] buf = ReusableByteBufferPool.acquire(ReusableByteBufferPool.SIZE_SMALL);
|
||||
if (buf == null || buf.length != ReusableByteBufferPool.SIZE_SMALL) {
|
||||
failures.incrementAndGet();
|
||||
}
|
||||
buf[0] = (byte) 0xAA;
|
||||
buf[1] = (byte) 0xBB;
|
||||
ReusableByteBufferPool.release(buf);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failures.incrementAndGet();
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
executor.shutdown();
|
||||
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
|
||||
}
|
||||
}
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConnection;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPSEvent;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPSListener;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import haus.nightmare.lib3270j.tn3270.DS3270;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 10 Test Suite: DataStream Protocol Hardening & AUTO_SYS_UNLOCK Parity.
|
||||
*
|
||||
* Verifies:
|
||||
* 10.1 WCC Keyboard Restore Logic (intermediate writes without restore bit keep keyboard locked).
|
||||
* 10.2 AUTO_SYS_UNLOCK parity across ConnectionConfig, ECLSession, ECLConnection, DS3270, and TelnetFSM.
|
||||
* 10.3 Contention Resolution negotiation, SDI/KRI tracking, and ECLPSEvent.EVENT_KEY_UNLOCKED dispatch.
|
||||
*/
|
||||
public class Phase10ProtocolHardeningTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor inputProcessor;
|
||||
private DataStreamProcessor processor;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
inputProcessor = new InputProcessor(screen, translator, null);
|
||||
processor = new DataStreamProcessor(screen, translator);
|
||||
processor.setInputProcessor(inputProcessor);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.1 WCC Keyboard Restore Logic
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Write without WCC restore bit keeps keyboard locked")
|
||||
public void testWriteWithoutWccRestoreKeepsKeyboardLocked() {
|
||||
// Initially lock the keyboard
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// Construct 3270 Write Command (0x01) with WCC = 0xC0 (Reset MDT only, no keyboard restore 0x02)
|
||||
byte[] record = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0xC0, // WCC: bit 6 set (reset MDT), bit 1 (restore) NOT set
|
||||
(byte) 0x11, 0x40, 0x40, // SBA to 0
|
||||
(byte) 0xC1, (byte) 0xC2 // 'A', 'B'
|
||||
};
|
||||
|
||||
processor.processRecord(record, 0, record.length, false);
|
||||
|
||||
// Keyboard MUST remain locked because WCC restore bit 0x02 was not set
|
||||
assertTrue(inputProcessor.isKeyboardLocked(),
|
||||
"Keyboard must remain locked when WCC does not have restore bit (0x02) set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Write with WCC restore bit unlocks keyboard")
|
||||
public void testWriteWithWccRestoreUnlocksKeyboard() {
|
||||
// Initially lock the keyboard
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// Construct 3270 Write Command (0x01) with WCC = 0xC2 (Reset MDT + Keyboard Restore bit 0x02)
|
||||
byte[] record = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0xC2, // WCC: restore bit (0x02) set
|
||||
(byte) 0x11, 0x40, 0x40, // SBA to 0
|
||||
(byte) 0xC1, (byte) 0xC2 // 'A', 'B'
|
||||
};
|
||||
|
||||
processor.processRecord(record, 0, record.length, false);
|
||||
|
||||
// Keyboard MUST now be unlocked
|
||||
assertFalse(inputProcessor.isKeyboardLocked(),
|
||||
"Keyboard must be unlocked when WCC has restore bit (0x02) set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Intermediate writes without restore bit maintain keyboard lock across sequence")
|
||||
public void testIntermediateWritesKeepKeyboardLockedUntilFinalRestore() {
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// First intermediate write: WCC = 0x00 (no restore)
|
||||
byte[] write1 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0x00,
|
||||
(byte) 0x11, 0x40, 0x40,
|
||||
(byte) 0xC1
|
||||
};
|
||||
processor.processRecord(write1, 0, write1.length, false);
|
||||
assertTrue(inputProcessor.isKeyboardLocked(), "Write 1 without restore bit must leave keyboard locked");
|
||||
|
||||
// Second intermediate write: Erase / Write with WCC = 0x40 (Reset, no restore)
|
||||
byte[] write2 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_ERASE_WRITE,
|
||||
(byte) 0x40,
|
||||
(byte) 0x11, 0x40, 0x50,
|
||||
(byte) 0xC2
|
||||
};
|
||||
processor.processRecord(write2, 0, write2.length, false);
|
||||
assertTrue(inputProcessor.isKeyboardLocked(), "Write 2 without restore bit must leave keyboard locked");
|
||||
|
||||
// Final write: WCC = 0x42 (Reset + Restore)
|
||||
byte[] write3 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0x42,
|
||||
(byte) 0x11, 0x40, 0x60,
|
||||
(byte) 0xC3
|
||||
};
|
||||
processor.processRecord(write3, 0, write3.length, false);
|
||||
assertFalse(inputProcessor.isKeyboardLocked(), "Final write with restore bit must unlock keyboard");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.2 AUTO_SYS_UNLOCK Parity
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: autoSysUnlock configuration property propagation across ECL components")
|
||||
public void testAutoSysUnlockConfigurationPropagation() {
|
||||
// Default ConnectionConfig should have autoSysUnlock == true
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(config.isAutoSysUnlock(), "Default autoSysUnlock in ConnectionConfig must be true");
|
||||
|
||||
config.setAutoSysUnlock(false);
|
||||
assertFalse(config.isAutoSysUnlock());
|
||||
|
||||
// Telnet3270Client propagation
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
assertFalse(client.isAutoSysUnlock());
|
||||
|
||||
// ECLSession propagation
|
||||
ECLSession session = new ECLSession(client);
|
||||
assertFalse(session.isAutoSysUnlock());
|
||||
assertFalse(session.getConnection().isAutoSysUnlock());
|
||||
|
||||
// Modify via ECLSession
|
||||
session.setAutoSysUnlock(true);
|
||||
assertTrue(session.isAutoSysUnlock());
|
||||
assertTrue(client.isAutoSysUnlock());
|
||||
assertTrue(config.isAutoSysUnlock());
|
||||
|
||||
// Test ECLSession Properties parsing
|
||||
Properties props = new Properties();
|
||||
props.setProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK, "false");
|
||||
props.setProperty(ECLSession.SESSION_HOST, "mainframe.org");
|
||||
ECLSession propsSession = new ECLSession(props);
|
||||
assertFalse(propsSession.isAutoSysUnlock());
|
||||
assertFalse(propsSession.getConnection().isAutoSysUnlock());
|
||||
|
||||
// ECLConnection standalone properties
|
||||
ECLConnection standaloneConn = new ECLConnection(props);
|
||||
assertFalse(standaloneConn.isAutoSysUnlock());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: DS3270 endOfRecord unlocks keyboard and clears OIA when autoSysUnlock is true and in X SYSTEM")
|
||||
public void testDs3270AutoSysUnlockOnEor() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
config.setAutoSysUnlock(true);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
ECLOIA oia = session.GetOIA();
|
||||
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
assertTrue(ds.isAutoSysUnlock());
|
||||
|
||||
// Set connected state so OIA does not treat it as communication check
|
||||
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
|
||||
|
||||
// Put session in X SYSTEM lock
|
||||
ps.lockKeyboard(8); // reason 8: SYSLOCK
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
assertTrue(oia.isXSystem());
|
||||
|
||||
// Trigger endOfRecord() with autoSysUnlock=true
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard must be auto-unlocked and OIA cleared
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard should be unlocked on EOR when autoSysUnlock is true");
|
||||
assertFalse(oia.isXSystem(),
|
||||
"OIA X SYSTEM lock should be cleared on EOR when autoSysUnlock is true");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: DS3270 endOfRecord leaves keyboard locked when autoSysUnlock is false and in X SYSTEM")
|
||||
public void testDs3270AutoSysUnlockDisabledKeepsSystemLock() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
config.setAutoSysUnlock(false);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
assertFalse(ds.isAutoSysUnlock());
|
||||
|
||||
// Put session in X SYSTEM lock
|
||||
ps.lockKeyboard(8);
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
|
||||
// Trigger endOfRecord() with autoSysUnlock=false
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard MUST remain locked
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard must remain locked on EOR when autoSysUnlock is false");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.3 Contention Resolution & Pre-Data State Transitions
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution negotiation flag synchronization with ECLConnection")
|
||||
public void testContentionResolutionNegotiationSync() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLConnection conn = session.getConnection();
|
||||
TelnetFSM fsm = client.getTelnetFSM();
|
||||
|
||||
assertNotNull(fsm);
|
||||
assertTrue(fsm.isNegotiateContentionResolution(),
|
||||
"Default negotiateContentionResolution should be true");
|
||||
|
||||
// Test setter on FSM and reflection in ECLConnection
|
||||
fsm.setContentionResolutionNegotiated(true);
|
||||
|
||||
assertTrue(fsm.isContentionResolutionNegotiated(),
|
||||
"Contention resolution should be negotiated in FSM");
|
||||
assertTrue(conn.getContentionResolution(),
|
||||
"ECLConnection should reflect negotiated contention resolution");
|
||||
assertTrue(session.getContentionResolution(),
|
||||
"ECLSession should reflect negotiated contention resolution");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution SDI flag restores keyboard and fires EVENT_KEY_UNLOCKED on EOR")
|
||||
public void testContentionResolutionWithSdiRestoresKeyboardAndFiresEvent() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
// Enable Contention Resolution on session
|
||||
session.setContentionResolution(true);
|
||||
assertTrue(session.getContentionResolution());
|
||||
|
||||
// Lock keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(true);
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
|
||||
// Register listener for EVENT_KEY_UNLOCKED
|
||||
AtomicInteger eventTypeReceived = new AtomicInteger(-1);
|
||||
AtomicBoolean unlockedEventFired = new AtomicBoolean(false);
|
||||
ps.RegisterPSEvent(new ECLPSListener() {
|
||||
@Override
|
||||
public void PSNotifyEvent(ECLPSEvent event) {
|
||||
eventTypeReceived.set(event.getEventType());
|
||||
unlockedEventFired.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Inbound TN3270E header with SDI bit set:
|
||||
// s2 request flag has bit 0 (0x01) set -> Send Data Indicator (SDI)
|
||||
short s1 = 0;
|
||||
short s2 = DS3270.request_bit_SDI; // 1
|
||||
short s3 = 0;
|
||||
ds.receiveHeaderData(s1, s2, s3, 0);
|
||||
|
||||
assertTrue(ds.isSdi_flag(), "SDI flag should be tracked from TN3270E header");
|
||||
|
||||
// Trigger endOfRecord()
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard should be unlocked because SDI was received
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard should be restored on EOR when CR is active and SDI is set");
|
||||
assertFalse(ds.isSdi_flag(), "SDI flag should be reset after EOR");
|
||||
assertTrue(unlockedEventFired.get(), "ECLPSEvent must be dispatched on keyboard unlock");
|
||||
assertEquals(ECLPSEvent.EVENT_KEY_UNLOCKED, eventTypeReceived.get(),
|
||||
"Dispatched event must match EVENT_KEY_UNLOCKED");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution WITHOUT SDI flag keeps keyboard locked on EOR")
|
||||
public void testContentionResolutionWithoutSdiKeepsKeyboardLocked() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
session.setContentionResolution(true);
|
||||
assertTrue(session.getContentionResolution());
|
||||
|
||||
// Initially lock keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(true);
|
||||
|
||||
// Inbound TN3270E header WITHOUT SDI flag (s2 = 0)
|
||||
ds.receiveHeaderData((short) 0, (short) 0, (short) 0, 0);
|
||||
assertFalse(ds.isSdi_flag());
|
||||
|
||||
// Trigger EOR
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard MUST stay locked because host did not grant Send Data Indicator (turn)
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard must remain locked on EOR when Contention Resolution is active and SDI is not set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution KRI flag resets system lock pending on EOR")
|
||||
public void testContentionResolutionKriResetsSystemLock() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
session.setContentionResolution(true);
|
||||
|
||||
// Put in system lock
|
||||
ps.lockKeyboard(8);
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
|
||||
// Receive header with KRI flag (bit 1 = 0x02) and SDI flag (bit 0 = 0x01)
|
||||
short s2 = (short) (DS3270.request_bit_SDI | DS3270.request_bit_KRI);
|
||||
ds.receiveHeaderData((short) 0, s2, (short) 0, 0);
|
||||
|
||||
assertTrue(ds.isSdi_flag());
|
||||
assertTrue(ds.isKri_flag());
|
||||
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard restored and system lock reset
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked());
|
||||
assertFalse(ps.islocked_SYSLOCK());
|
||||
assertFalse(ds.isKri_flag());
|
||||
}
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConnection;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetConnection;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 11 Test Suite: Enterprise Connection Resilience & "Dirty" Connection Handling.
|
||||
*
|
||||
* Verifies:
|
||||
* 11.1 Application-Layer Telnet Keep-Alive / Heartbeat Engine (NOP and TIMING-MARK, activity reset, shutdown).
|
||||
* 11.2 Automatic Reconnection with Exponential Backoff (unexpected disconnect, state transitions, retry loop, recovery, exhaustion).
|
||||
* 11.3 Transport-Level Socket Tuning & Options (soTimeout, extended socket options, CLI parsing).
|
||||
* 11.4 IBM HoD ECL Compatibility Facade (SESSION_KEEPALIVE, keepAliveTimeout, SESSION_AUTORECONNECT, ECLConnection methods).
|
||||
*/
|
||||
public class Phase11ConnectionResilienceTest {
|
||||
|
||||
// =========================================================================
|
||||
// 11.1 Application-Layer Telnet Keep-Alive / Heartbeat Engine
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Keep-Alive transmits Telnet IAC NOP (0xFF 0xF1) when idle")
|
||||
public void testKeepAliveTransmitsIacNop() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setKeepAliveEnabled(true);
|
||||
config.setKeepAliveIntervalSeconds(1);
|
||||
config.setKeepAliveType("NOP");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch clientConnected = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> serverAccepted = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
serverAccepted.set(s);
|
||||
clientConnected.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
assertTrue(clientConnected.await(5, TimeUnit.SECONDS), "Server did not accept client connection");
|
||||
|
||||
Socket s = serverAccepted.get();
|
||||
assertNotNull(s);
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
// Explicitly trigger or wait for keepalive heartbeat
|
||||
client.getConnection().sendKeepAliveHeartbeat();
|
||||
|
||||
byte[] buf = new byte[2];
|
||||
int read = in.read(buf);
|
||||
assertEquals(2, read);
|
||||
assertEquals((byte) TelnetConstants.IAC, buf[0]);
|
||||
assertEquals((byte) TelnetConstants.NOP, buf[1]);
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Keep-Alive transmits Telnet IAC DO TIMING-MARK (0xFF 0xFD 0x06)")
|
||||
public void testKeepAliveTransmitsTimingMark() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setKeepAliveEnabled(true);
|
||||
config.setKeepAliveIntervalSeconds(1);
|
||||
config.setKeepAliveType("TIMING-MARK");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch clientConnected = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> serverAccepted = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
serverAccepted.set(s);
|
||||
clientConnected.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
assertTrue(clientConnected.await(5, TimeUnit.SECONDS));
|
||||
|
||||
Socket s = serverAccepted.get();
|
||||
assertNotNull(s);
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
client.getConnection().sendKeepAliveHeartbeat();
|
||||
|
||||
byte[] buf = new byte[3];
|
||||
int read = in.read(buf);
|
||||
assertEquals(3, read);
|
||||
assertEquals((byte) TelnetConstants.IAC, buf[0]);
|
||||
assertEquals((byte) TelnetConstants.DO, buf[1]);
|
||||
assertEquals((byte) TelnetConstants.TELOPT_TM, buf[2]);
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Transmission resets last activity timestamp")
|
||||
public void testTransmissionResetsActivityTimestamp() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
server.accept();
|
||||
} catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
TelnetConnection conn = client.getConnection();
|
||||
long initialTime = conn.getLastActivityTime();
|
||||
|
||||
Thread.sleep(15);
|
||||
conn.sendRaw(new byte[] { 0x01, 0x02 });
|
||||
|
||||
long updatedTime = conn.getLastActivityTime();
|
||||
assertTrue(updatedTime >= initialTime + 10, "Activity timestamp was not updated on sendRaw");
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.2 Automatic Reconnection with Exponential Backoff
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Unexpected EOF transitions to RECONNECTING state when autoReconnect is true")
|
||||
public void testUnexpectedDisconnectTransitionsToReconnecting() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CopyOnWriteArrayList<ConnectionState> stateHistory = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch reconnectingLatch = new CountDownLatch(1);
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
stateHistory.add(newState);
|
||||
if (newState == ConnectionState.RECONNECTING) {
|
||||
reconnectingLatch.countDown();
|
||||
}
|
||||
}
|
||||
@Override public void onConnectionError(String message) {}
|
||||
});
|
||||
|
||||
AtomicReference<Socket> acceptedSocket = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
acceptedSocket.set(s);
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
serverThread.join(2000);
|
||||
|
||||
assertNotNull(acceptedSocket.get());
|
||||
|
||||
// Abruptly sever the connection from the host side (EOF)
|
||||
acceptedSocket.get().close();
|
||||
|
||||
assertTrue(reconnectingLatch.await(4, TimeUnit.SECONDS), "Client did not enter RECONNECTING state on unexpected EOF");
|
||||
assertTrue(stateHistory.contains(ConnectionState.RECONNECTING), "State history must include RECONNECTING");
|
||||
assertTrue(client.isReconnecting());
|
||||
|
||||
// Check OIA message updated
|
||||
assertNotNull(client.getOIA());
|
||||
|
||||
client.disconnect();
|
||||
assertFalse(client.isReconnecting());
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Intentional client.disconnect() transitions directly to NOT_CONNECTED without auto-reconnect")
|
||||
public void testIntentionalDisconnectDoesNotTriggerAutoReconnect() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CopyOnWriteArrayList<ConnectionState> stateHistory = new CopyOnWriteArrayList<>();
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
stateHistory.add(newState);
|
||||
}
|
||||
@Override public void onConnectionError(String message) {}
|
||||
});
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
server.accept();
|
||||
} catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
Thread.sleep(100);
|
||||
|
||||
// Explicit client disconnect
|
||||
client.disconnect();
|
||||
|
||||
assertFalse(stateHistory.contains(ConnectionState.RECONNECTING),
|
||||
"Intentional disconnect should never transition to RECONNECTING");
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Auto-reconnect retry loop successfully re-establishes connection")
|
||||
public void testAutoReconnectSuccessfulReconnection() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch reconnectedLatch = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> firstConn = new AtomicReference<>();
|
||||
AtomicReference<Socket> secondConn = new AtomicReference<>();
|
||||
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s1 = server.accept();
|
||||
firstConn.set(s1);
|
||||
// Close first socket to trigger reconnect
|
||||
Thread.sleep(50);
|
||||
s1.close();
|
||||
|
||||
// Accept the reconnection attempt
|
||||
Socket s2 = server.accept();
|
||||
secondConn.set(s2);
|
||||
reconnectedLatch.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
|
||||
// Wait for reconnection to succeed
|
||||
assertTrue(reconnectedLatch.await(8, TimeUnit.SECONDS), "Server did not receive reconnection attempt");
|
||||
assertNotNull(secondConn.get());
|
||||
|
||||
// Wait for client to complete handshake / notify connected
|
||||
Thread.sleep(200);
|
||||
assertTrue(client.isConnected());
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.3 Transport-Level Socket Tuning & Options
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.3: Configurable SO_TIMEOUT is applied to TCP socket")
|
||||
public void testSoTimeoutApplied() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setSoTimeoutMs(750);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
new Thread(() -> {
|
||||
try { server.accept(); } catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
assertNotNull(client.getConnection());
|
||||
// Client socket should have SO_TIMEOUT applied
|
||||
assertEquals(750, config.getSoTimeoutMs());
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.3: parseHostString parses keepalive and autoreconnect flags")
|
||||
public void testParseHostStringFlags() {
|
||||
ConnectionConfig c1 = ConnectionConfig.parseHostString("mainframe.corp.com:23 --keepalive --autoreconnect", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c1.isKeepAliveEnabled());
|
||||
assertTrue(c1.isAutoReconnect());
|
||||
assertEquals("mainframe.corp.com", c1.getHost());
|
||||
assertEquals(23, c1.getPort());
|
||||
|
||||
ConnectionConfig c2 = ConnectionConfig.parseHostString("mainframe.corp.com:23 --no-keepalive --no-autoreconnect", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c2.isKeepAliveEnabled());
|
||||
assertFalse(c2.isAutoReconnect());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.4 IBM HoD ECL Compatibility Facade
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.4: ECLSession parses HoD properties for Keep-Alive and Auto-Reconnect")
|
||||
public void testEclSessionPropertiesParsing() {
|
||||
Properties props = new Properties();
|
||||
props.setProperty(ECLSession.SESSION_HOST, "mvs01.test.net");
|
||||
props.setProperty(ECLSession.SESSION_PORT, "23");
|
||||
props.setProperty(ECLSession.SESSION_KEEPALIVE, "true");
|
||||
props.setProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT, "180");
|
||||
props.setProperty(ECLSession.KEY_KEEPALIVE_TYPE, "TIMING-MARK");
|
||||
props.setProperty(ECLSession.SESSION_AUTORECONNECT, "true");
|
||||
props.setProperty(ECLSession.SESSION_RECONNECT_RETRIES, "8");
|
||||
|
||||
ECLSession session = new ECLSession(props);
|
||||
assertTrue(session.isKeepAlive());
|
||||
assertEquals(180, session.getKeepAliveTimeout());
|
||||
assertTrue(session.isAutoReconnect());
|
||||
|
||||
ECLConnection conn = session.getConnection();
|
||||
assertNotNull(conn);
|
||||
assertTrue(conn.isKeepAlive());
|
||||
assertEquals(180, conn.getKeepAliveTimeout());
|
||||
assertEquals("TIMING-MARK", conn.getKeepAliveType());
|
||||
assertTrue(conn.isAutoReconnect());
|
||||
assertEquals(8, conn.getMaxRetry());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.4: ECLConnection getters and setters synchronize with underlying client")
|
||||
public void testEclConnectionGettersSetters() {
|
||||
ConnectionConfig cfg = new ConnectionConfig("localhost", 23);
|
||||
Telnet3270Client client = new Telnet3270Client(cfg);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLConnection conn = session.getConnection();
|
||||
|
||||
conn.setKeepAlive(false);
|
||||
assertFalse(conn.isKeepAlive());
|
||||
assertFalse(client.getConfig().isKeepAliveEnabled());
|
||||
|
||||
conn.setKeepAliveTimeout(60);
|
||||
assertEquals(60, conn.getKeepAliveTimeout());
|
||||
assertEquals(60, client.getConfig().getKeepAliveIntervalSeconds());
|
||||
|
||||
conn.setKeepAliveType("TIMING-MARK");
|
||||
assertEquals("TIMING-MARK", conn.getKeepAliveType());
|
||||
assertEquals("TIMING-MARK", client.getConfig().getKeepAliveType());
|
||||
|
||||
conn.setAutoReconnect(true);
|
||||
assertTrue(conn.isAutoReconnect());
|
||||
assertTrue(client.getConfig().isAutoReconnect());
|
||||
|
||||
conn.setMaxRetry(10);
|
||||
assertEquals(10, conn.getMaxRetry());
|
||||
assertEquals(10, client.getConfig().getReconnectMaxRetries());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class EntryAssistFullModeTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor inputProcessor;
|
||||
private ECLOIA oia;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_2, translator); // 24x80
|
||||
inputProcessor = new InputProcessor(screen, translator, null);
|
||||
oia = new ECLOIA(screen, inputProcessor, null);
|
||||
inputProcessor.setOIA(oia);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDocModeCursorAdvanceWithoutWordWrap() {
|
||||
// Document Mode ON, Word Wrap OFF
|
||||
screen.setEntryAssistDOCmode(true);
|
||||
screen.setEntryAssistWordWrap(false);
|
||||
screen.setLeftMargin(0); // Col 1 (0-based 0)
|
||||
screen.setRightMargin(72); // Col 73 (0-based 72)
|
||||
|
||||
assertTrue(screen.isEntryAssistDOCmode());
|
||||
assertFalse(screen.isEntryAssistWordWrap());
|
||||
assertEquals(0, screen.getEntryAssistStartColumn());
|
||||
assertEquals(72, screen.getEntryAssistEndColumn());
|
||||
|
||||
// Setup open cursor position
|
||||
screen.setCursorPosition(0, 72); // Row 0, Col 72 (at right margin)
|
||||
inputProcessor.typeCharacter('X');
|
||||
|
||||
// Should advance to next row at left margin (Row 1, Col 0)
|
||||
assertEquals(1, screen.getCursorRow());
|
||||
assertEquals(0, screen.getCursorCol());
|
||||
assertEquals('X', (char) screen.getCell(72).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWordWrapTransfersPartialWord() {
|
||||
// Document Mode ON, Word Wrap ON
|
||||
screen.setEntryAssistDOCmode(true);
|
||||
screen.setEntryAssistWordWrap(true);
|
||||
screen.setLeftMargin(5); // Start col = 5
|
||||
screen.setRightMargin(20); // End col = 20
|
||||
|
||||
// Type "HELLO " starting at col 10
|
||||
screen.setCursorPosition(0, 10);
|
||||
for (char c : "HELLO ".toCharArray()) {
|
||||
inputProcessor.typeCharacter(c);
|
||||
}
|
||||
|
||||
// Now cursor is at col 16. Type "TEST" which crosses col 20
|
||||
screen.setCursorPosition(0, 18);
|
||||
inputProcessor.typeCharacter('W');
|
||||
inputProcessor.typeCharacter('O');
|
||||
inputProcessor.typeCharacter('R');
|
||||
inputProcessor.typeCharacter('D'); // At col 21, past end col 20
|
||||
|
||||
// Word wrap should have moved "WORD" to next line starting at left margin 5
|
||||
assertEquals(1, screen.getCursorRow());
|
||||
assertTrue(screen.getCursorCol() >= 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDocModeTabStopsNavigation() {
|
||||
screen.setEntryAssistDOCmode(true);
|
||||
screen.setLeftMargin(0);
|
||||
screen.setRightMargin(79);
|
||||
screen.setEntryAssistTabStops(new int[]{0, 10, 20, 30});
|
||||
|
||||
screen.setCursorPosition(0, 0);
|
||||
|
||||
// Word tab forward
|
||||
screen.processWordTab(true);
|
||||
assertEquals(10, screen.getCursorCol());
|
||||
assertEquals(0, screen.getCursorRow());
|
||||
|
||||
screen.processWordTab(true);
|
||||
assertEquals(20, screen.getCursorCol());
|
||||
|
||||
// Word back tab
|
||||
screen.processWordTab(false);
|
||||
assertEquals(10, screen.getCursorCol());
|
||||
|
||||
screen.processWordTab(false);
|
||||
assertEquals(0, screen.getCursorCol());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAudibleEndOfLineBell() {
|
||||
inputProcessor.setBellEnabled(true);
|
||||
inputProcessor.setBellColumn(74);
|
||||
|
||||
AtomicInteger bellCount = new AtomicInteger(0);
|
||||
inputProcessor.setBellListener(bellCount::incrementAndGet);
|
||||
|
||||
screen.setCursorPosition(0, 70);
|
||||
inputProcessor.typeCharacter('A'); // 71
|
||||
inputProcessor.typeCharacter('B'); // 72
|
||||
inputProcessor.typeCharacter('C'); // 73
|
||||
assertEquals(0, bellCount.get());
|
||||
|
||||
inputProcessor.typeCharacter('D'); // 74 -> triggers bell
|
||||
assertEquals(1, bellCount.get());
|
||||
|
||||
inputProcessor.typeCharacter('E'); // 75 -> does not retrigger on same line
|
||||
assertEquals(1, bellCount.get());
|
||||
|
||||
// Move to next line at col 73 and type across bell column
|
||||
screen.setCursorPosition(1, 73);
|
||||
inputProcessor.typeCharacter('Z'); // advances to 74 -> triggers bell on next line
|
||||
assertEquals(2, bellCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertModeResetOnAid() {
|
||||
inputProcessor.setInsertOffOnAid(true);
|
||||
inputProcessor.setInsertMode(true);
|
||||
assertTrue(inputProcessor.isInsertMode());
|
||||
|
||||
// Send AID_ENTER
|
||||
inputProcessor.sendAid(AID_ENTER);
|
||||
assertFalse(inputProcessor.isInsertMode(), "Insert mode should be reset after AID key");
|
||||
|
||||
// When insertOffOnAid is disabled
|
||||
inputProcessor.setInsertOffOnAid(false);
|
||||
inputProcessor.setInsertMode(true);
|
||||
inputProcessor.sendAid(AID_ENTER);
|
||||
assertTrue(inputProcessor.isInsertMode(), "Insert mode should be preserved when insertOffOnAid is false");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAplKeyboardModeAndOia() {
|
||||
inputProcessor.setAplKeyboardMode(true);
|
||||
assertTrue(inputProcessor.isAplKeyboardMode());
|
||||
|
||||
// OIA status flag includes STATE_APL
|
||||
long statusEx = oia.GetStatusFlagsEx();
|
||||
assertEquals(ECLOIA.STATE_APL, statusEx & ECLOIA.STATE_APL);
|
||||
assertTrue(oia.isApl());
|
||||
|
||||
// Test APL translation for key 'a' (alpha -> 0x41 with CS_GE)
|
||||
screen.setCursorPosition(0, 0);
|
||||
inputProcessor.typeCharacter('a');
|
||||
|
||||
var cell = screen.getCell(0);
|
||||
assertEquals(CS_GE, cell.cs, "APL character must be stored with CS_GE (Graphic Escape) charset");
|
||||
|
||||
// Toggle APL mode off
|
||||
inputProcessor.toggleAplKeyboardMode();
|
||||
assertFalse(inputProcessor.isAplKeyboardMode());
|
||||
assertFalse(oia.isApl());
|
||||
assertEquals(0, oia.GetStatusFlagsEx() & ECLOIA.STATE_APL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNumericFieldLockSetting() {
|
||||
// Field: pos 0 is numeric FA, pos 1..5 unprotected, pos 6 protect FA
|
||||
screen.setFieldAttribute(0, (byte) FA_NUMERIC);
|
||||
screen.setFieldAttribute(6, (byte) FA_PROTECT);
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
// Case 1: NumericFieldLock is true
|
||||
inputProcessor.setNumericFieldLock(true);
|
||||
inputProcessor.typeCharacter('X'); // invalid numeric character
|
||||
assertTrue(inputProcessor.isKeyboardLocked(), "Keyboard should be locked on non-numeric char");
|
||||
assertEquals(ECLConstants.INHIBIT_NUMERIC_ONLY, oia.getInputInhibited());
|
||||
|
||||
// Reset keyboard
|
||||
inputProcessor.reset();
|
||||
assertFalse(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// Case 2: NumericFieldLock is false
|
||||
inputProcessor.setNumericFieldLock(false);
|
||||
screen.setCursorAddress(1);
|
||||
inputProcessor.typeCharacter('X');
|
||||
assertFalse(inputProcessor.isKeyboardLocked(), "Keyboard should NOT be locked when NumericFieldLock is false");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOiaModeIndicators() {
|
||||
screen.setEntryAssistDOCmode(true);
|
||||
screen.setEntryAssistWordWrap(true);
|
||||
inputProcessor.setAplKeyboardMode(true);
|
||||
inputProcessor.setInsertMode(true);
|
||||
|
||||
long flags = oia.GetStatusFlagsEx();
|
||||
assertTrue((flags & ECLOIA.STATE_DOC_MODE) != 0);
|
||||
assertTrue((flags & ECLOIA.STATE_WORDWRAP) != 0);
|
||||
assertTrue((flags & ECLOIA.STATE_APL) != 0);
|
||||
assertTrue((flags & ECLOIA.STATE_INSERT) != 0);
|
||||
|
||||
assertTrue(oia.isDocMode());
|
||||
assertTrue(oia.isWordWrap());
|
||||
assertTrue(oia.isApl());
|
||||
assertTrue(oia.isInsertMode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 13.2: Interactive Entry Assist Engine.
|
||||
* Verifies interactive word wrap in DOC mode and audible bell alert
|
||||
* when typing reaches the configured bell column.
|
||||
*/
|
||||
public class InteractiveEntryAssistTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAudibleBellTriggersAtConfiguredBellColumn() {
|
||||
input.setBellEnabled(true);
|
||||
input.setBellColumn(74); // 0-based column 74 = column 75
|
||||
|
||||
AtomicInteger bellCount = new AtomicInteger(0);
|
||||
input.setBellListener(bellCount::incrementAndGet);
|
||||
|
||||
screen.setCursorPosition(0, 72);
|
||||
input.typeCharacter('A'); // col 73
|
||||
assertEquals(0, bellCount.get());
|
||||
|
||||
input.typeCharacter('B'); // col 74 -> triggers bell
|
||||
assertEquals(1, bellCount.get());
|
||||
|
||||
input.typeCharacter('C'); // col 75 -> remains 1 on current row
|
||||
assertEquals(1, bellCount.get());
|
||||
|
||||
// Moving to next row and typing across bell column triggers bell again
|
||||
screen.setCursorPosition(1, 73);
|
||||
input.typeCharacter('X'); // advances to col 74 on row 1
|
||||
assertEquals(2, bellCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDocModeWordWrapMovesPartialWordToNextRow() {
|
||||
screen.setEntryAssistDOCmode(true);
|
||||
screen.setEntryAssistWordWrap(true);
|
||||
screen.setLeftMargin(0); // Col 0
|
||||
screen.setRightMargin(15); // Col 15
|
||||
|
||||
assertTrue(screen.isEntryAssistDOCmode());
|
||||
assertTrue(screen.isEntryAssistWordWrap());
|
||||
|
||||
// Type "HELLO " (6 chars) starting at col 0
|
||||
screen.setCursorPosition(0, 0);
|
||||
for (char c : "HELLO ".toCharArray()) {
|
||||
input.typeCharacter(c);
|
||||
}
|
||||
assertEquals(0, screen.getCursorRow());
|
||||
assertEquals(6, screen.getCursorCol());
|
||||
|
||||
// Move cursor near right margin: Row 0, Col 13
|
||||
screen.setCursorPosition(0, 13);
|
||||
input.typeCharacter('P'); // col 14
|
||||
input.typeCharacter('A'); // col 15 (at right margin)
|
||||
input.typeCharacter('R'); // crosses right margin -> triggers word wrap
|
||||
|
||||
// After word wrap, the word "PAR" should be moved to Row 1, left margin (Col 0)
|
||||
assertEquals(1, screen.getCursorRow());
|
||||
assertTrue(screen.getCursorCol() >= 3);
|
||||
|
||||
// Verify content on Row 1
|
||||
assertEquals('P', (char) screen.getCell(80).ucs4);
|
||||
assertEquals('A', (char) screen.getCell(81).ucs4);
|
||||
assertEquals('R', (char) screen.getCell(82).ucs4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 12 Test Suite: Reactive Synchronization & Thread-Safety Hardening.
|
||||
*
|
||||
* Verifies:
|
||||
* 12.1 Condition-Based Synchronization for ECL Automation Calls:
|
||||
* - Sub-millisecond reactive wakeup for waitForScreen(ECLScreenDesc).
|
||||
* - Reactive wakeup for waitForCursor(row, col).
|
||||
* - Reactive wakeup for waitForString(text) and waitWhileScreen(ECLScreenDesc).
|
||||
* - Clean timeout handling without CPU busy-wait spinning.
|
||||
* - Graceful thread interruption handling.
|
||||
* - ECLOIA condition-based reactive waitForInput and waitForTransition.
|
||||
* - ScreenBuffer listener dispatch and cursor change tracking.
|
||||
* - Concurrency safety with multiple waiters.
|
||||
*/
|
||||
public class Phase12SynchronizationTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor inputProcessor;
|
||||
private ECLPS ps;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
inputProcessor = new InputProcessor(screen, translator, null);
|
||||
ps = new ECLPS(screen, inputProcessor, translator);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForScreen(ECLScreenDesc) wakes up reactively upon screen update")
|
||||
public void testReactiveWaitForScreenDesc() throws Exception {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("TSO/E LOGON", 5, 10);
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForScreen(desc, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50); // Give waiter time to block on condition
|
||||
|
||||
// Write matching text at 1-based (5, 10) -> 0-based row 4 col 9
|
||||
screen.setText("TSO/E LOGON", 4, 9);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForScreen should match");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively well before 5000ms timeout (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForCursor(row, col) wakes up reactively upon cursor position change")
|
||||
public void testReactiveWaitForCursor() throws Exception {
|
||||
screen.setCursorPosition(0, 0);
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForCursor(12, 34, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Move cursor
|
||||
screen.setCursorPosition(12, 34);
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForCursor should succeed");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively upon cursor move (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForString(text) wakes up reactively upon string appearance")
|
||||
public void testReactiveWaitForString() throws Exception {
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForString("COMMAND ===>", 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
screen.setText("COMMAND ===>", 20, 2);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForString should find the text");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitWhileScreen(desc) wakes up reactively when condition ceases to match")
|
||||
public void testReactiveWaitWhileScreen() throws Exception {
|
||||
// Place initial text
|
||||
screen.setText("HOLDING", 0, 0);
|
||||
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("HOLDING");
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean finished = ps.waitWhileScreen(desc, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(finished);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Erase screen so "HOLDING" is gone
|
||||
screen.erase(false);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitWhileScreen should return true once condition is gone");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively when text erased (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Clean timeout without false match or spinning")
|
||||
public void testCleanTimeout() {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("NON_EXISTENT_STRING");
|
||||
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForScreen(desc, 150);
|
||||
long elapsed = System.currentTimeMillis() - t0;
|
||||
|
||||
assertFalse(matched, "Should not match non-existent text");
|
||||
assertTrue(elapsed >= 100, "Should have waited for the full timeout window (elapsed=" + elapsed + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Graceful thread interruption during wait")
|
||||
public void testThreadInterruption() throws Exception {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("NEVER_APPEARS");
|
||||
|
||||
AtomicBoolean wasInterrupted = new AtomicBoolean(false);
|
||||
AtomicBoolean result = new AtomicBoolean(true);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
boolean r = ps.waitForScreen(desc, 10000);
|
||||
result.set(r);
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
wasInterrupted.set(true);
|
||||
}
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
waiter.interrupt();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertFalse(result.get(), "Interrupted wait should return false");
|
||||
assertTrue(wasInterrupted.get(), "Thread interrupt flag should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: ECLOIA reactive waitForInput and waitForTransition")
|
||||
public void testECLOIAReactiveWait() throws Exception {
|
||||
ECLOIA oia = new ECLOIA(screen, inputProcessor, null);
|
||||
oia.setInputInhibited(ECLOIA.INHIBIT_SYSTEMWAIT);
|
||||
|
||||
AtomicBoolean inputReady = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean ready = oia.waitForInput(5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
inputReady.set(ready);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Unlock OIA
|
||||
oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED);
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(inputReady.get(), "waitForInput should succeed");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively upon OIA unlock (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: ScreenBuffer listener dispatch and cursor movement tracking")
|
||||
public void testScreenBufferListenerDispatch() {
|
||||
AtomicBoolean screenUpdated = new AtomicBoolean(false);
|
||||
AtomicInteger cursorOld = new AtomicInteger(-1);
|
||||
AtomicInteger cursorNew = new AtomicInteger(-1);
|
||||
|
||||
ScreenUpdateListener listener = new ScreenUpdateListener() {
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
screenUpdated.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorMoved(int oldAddress, int newAddress) {
|
||||
cursorOld.set(oldAddress);
|
||||
cursorNew.set(newAddress);
|
||||
}
|
||||
};
|
||||
|
||||
screen.addUpdateListener(listener);
|
||||
|
||||
screen.notifyScreenUpdate();
|
||||
assertTrue(screenUpdated.get(), "Listener should receive onScreenUpdated");
|
||||
|
||||
screen.setCursorAddress(123);
|
||||
assertEquals(0, cursorOld.get(), "Old cursor should be 0");
|
||||
assertEquals(123, cursorNew.get(), "New cursor should be 123");
|
||||
|
||||
screen.removeUpdateListener(listener);
|
||||
screenUpdated.set(false);
|
||||
screen.notifyScreenUpdate();
|
||||
assertFalse(screenUpdated.get(), "Removed listener should not receive updates");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Concurrent waiters on the same ScreenBuffer")
|
||||
public void testConcurrentWaiters() throws Exception {
|
||||
int numWaiters = 5;
|
||||
CountDownLatch startLatch = new CountDownLatch(numWaiters);
|
||||
CountDownLatch doneLatch = new CountDownLatch(numWaiters);
|
||||
AtomicInteger successCount = new AtomicInteger(0);
|
||||
|
||||
for (int i = 0; i < numWaiters; i++) {
|
||||
new Thread(() -> {
|
||||
startLatch.countDown();
|
||||
boolean ok = ps.waitForString("BATCH_SIGNAL", 5000);
|
||||
if (ok) {
|
||||
successCount.incrementAndGet();
|
||||
}
|
||||
doneLatch.countDown();
|
||||
}).start();
|
||||
}
|
||||
|
||||
assertTrue(startLatch.await(2, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
screen.setText("BATCH_SIGNAL", 100);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(doneLatch.await(3, TimeUnit.SECONDS));
|
||||
assertEquals(numWaiters, successCount.get(), "All concurrent waiters should unblock reactively");
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import java.awt.Image;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -406,11 +406,12 @@ public class Phase5EclEventTest {
|
||||
}
|
||||
|
||||
// Test with image and rectangle
|
||||
Image testImage = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
PixelBuffer testImage = new DefaultPixelBuffer(32, 32);
|
||||
Rectangle rect = new Rectangle(0, 0, 32, 32);
|
||||
ECLPSGraphicsEvent fullGEvent = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED, testImage, rect);
|
||||
assertSame(testImage, fullGEvent.GetImage());
|
||||
assertSame(testImage, fullGEvent.getImage());
|
||||
assertSame(testImage, fullGEvent.getPixelBuffer());
|
||||
assertEquals(rect, fullGEvent.GetRectangle());
|
||||
assertEquals(rect, fullGEvent.getRectangle());
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 13.1: Excel & Tabular Clipboard Paste.
|
||||
* Verifies tab-delimited text navigation, newline row advancement,
|
||||
* and boundary truncation when pasteStopAtProtectedLine is set.
|
||||
*/
|
||||
public class TabularPasteTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
private ECLPS ps;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
ps = new ECLPS(screen, input, translator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcelPasteTabsAndNewlinesAdvanceAcrossFields() {
|
||||
// Setup 2 rows with 2 unprotected fields each
|
||||
// Row 0: pos 0 (unprotected), pos 10 (protected), pos 11 (unprotected), pos 30 (protected)
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(11, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Row 1: pos 80 (unprotected), pos 90 (protected), pos 91 (unprotected), pos 110 (protected)
|
||||
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(90, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(91, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(110, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Position cursor at first field (pos 1)
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
String tabularData = "ABC\tDEF\r\nGHI\tJKL";
|
||||
int pasted = input.pasteText(tabularData, true, false);
|
||||
assertEquals(12, pasted);
|
||||
|
||||
// Verify Field 1 (Row 0, Col 1-3)
|
||||
assertEquals("ABC", ps.getString(1, 3));
|
||||
// Verify Field 2 (Row 0, Col 12-14)
|
||||
assertEquals("DEF", ps.getString(12, 3));
|
||||
// Verify Field 3 (Row 1, Col 1-3 -> pos 81)
|
||||
assertEquals("GHI", ps.getString(81, 3));
|
||||
// Verify Field 4 (Row 1, Col 12-14 -> pos 92)
|
||||
assertEquals("JKL", ps.getString(92, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteStopAtProtectedBoundaryHaltsPaste() {
|
||||
// Field 1: pos 0 (unprotected), pos 5 (protected)
|
||||
// Data area is pos 1..4 (4 characters)
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(5, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
// Try to paste 6 characters when only 4 fit
|
||||
String overflow = "123456";
|
||||
int pasted = input.pasteText(overflow, false, true);
|
||||
|
||||
// Should paste 4 chars and halt at the protected boundary pos 5
|
||||
assertEquals(4, pasted);
|
||||
assertEquals("1234", ps.getString(1, 4));
|
||||
// Field at pos 10 should not be touched
|
||||
assertEquals(" ", ps.getString(11, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteStopAtProtectedLineHaltsAtProtectedRow() {
|
||||
// Row 0: unprotected field at pos 0
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
// Row 1: entirely protected starting at pos 80
|
||||
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
String multiRow = "DATA1\r\nDATA2";
|
||||
int pasted = input.pasteText(multiRow, true, true);
|
||||
|
||||
// "DATA1" (5 chars) pasted, then newline sees next line is protected and halts
|
||||
assertEquals(5, pasted);
|
||||
assertEquals("DATA1", ps.getString(1, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEclPsPasteFromExcelMethodsAndFlags() {
|
||||
assertTrue(ps.isEnablePasteFromExcel());
|
||||
assertFalse(ps.isPasteStopAtProtectedLine());
|
||||
|
||||
ps.setEnablePasteFromExcel(false);
|
||||
assertFalse(ps.isEnablePasteFromExcel());
|
||||
|
||||
ps.setPasteStopAtProtectedLine(true);
|
||||
assertTrue(ps.isPasteStopAtProtectedLine());
|
||||
|
||||
// Setup fields
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(11, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
ps.pasteFromExcel("HELLO\tWORLD", 0, 1);
|
||||
assertEquals("HELLO", ps.getString(1, 5));
|
||||
assertEquals("WORLD", ps.getString(12, 5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package haus.nightmare.lib3270j.epi;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class EpiDataStream3270Test {
|
||||
|
||||
private Screen screen;
|
||||
private DataStream3270 dataStream;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
screen = new Screen(80, 24);
|
||||
dataStream = new DataStream3270(screen);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeAndDecodeAddress() {
|
||||
int[] testAddresses = {0, 1, 79, 80, 1919, 2000, 4095};
|
||||
byte[] target = new byte[2];
|
||||
|
||||
for (int addr : testAddresses) {
|
||||
dataStream.encodeAddress(target, 0, addr);
|
||||
int decoded = dataStream.decodeAddress(target[0], target[1]);
|
||||
assertEquals(addr, decoded, "Address round-trip mismatch for " + addr);
|
||||
}
|
||||
|
||||
// Test invalid decode
|
||||
assertEquals(-1, dataStream.decodeAddress((byte) 0x00, (byte) 0x00));
|
||||
assertEquals(-1, dataStream.decodeAddress((byte) 0xFF, (byte) 0xFF));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharacterTranslations() {
|
||||
char ebcdicChar = dataStream.toEbcdic('A'); // ASCII 65 -> EBCDIC 0xC1
|
||||
assertEquals('\u00c1', ebcdicChar);
|
||||
byte asciiByte = dataStream.toAscii(ebcdicChar); // EBCDIC 0xC1 -> ASCII 65
|
||||
assertEquals('A', (char) asciiByte);
|
||||
|
||||
// Boundary cases
|
||||
char low = dataStream.toEbcdic(0x10);
|
||||
assertEquals((char) 0x10, low);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAnalyzeWriteAndFormat() throws Exception {
|
||||
// Build 3270 Write buffer:
|
||||
// CMD_W (49 / 0x31), WCC (0xC3), SBA (17), Addr(0, 0), SF (29), Attr (0xC1 = MDT set), Text "TEST"
|
||||
byte[] stream = new byte[11];
|
||||
stream[0] = 49; // Write
|
||||
stream[1] = (byte) 0xC3; // WCC
|
||||
stream[2] = 17; // SBA
|
||||
dataStream.encodeAddress(stream, 3, 0);
|
||||
stream[5] = 29; // SF
|
||||
stream[6] = (byte) 0xC1; // Attribute (MDT=1)
|
||||
stream[7] = (byte) 'T';
|
||||
stream[8] = (byte) 'E';
|
||||
stream[9] = (byte) 'S';
|
||||
stream[10] = (byte) 'T';
|
||||
|
||||
dataStream.analyze(stream, stream.length);
|
||||
|
||||
assertEquals(1, screen.fieldCount());
|
||||
Field f = screen.field(1);
|
||||
assertNotNull(f);
|
||||
assertTrue(f.hasAttribute());
|
||||
assertEquals(1, f.dataTag());
|
||||
|
||||
// Now format outbound buffer
|
||||
screen.setAID(AID.enter);
|
||||
screen.setCursor(1, 1);
|
||||
byte[] outBuf = new byte[100];
|
||||
int outLen = dataStream.format(outBuf);
|
||||
|
||||
assertTrue(outLen > 0);
|
||||
assertEquals(AID.enter.translate(), outBuf[0]);
|
||||
// Cursor addr at [1, 2]
|
||||
// SBA (17) at [3]
|
||||
assertEquals(17, outBuf[3]);
|
||||
// Data text starting at [6]
|
||||
assertEquals((byte) 'T', outBuf[6]);
|
||||
assertEquals((byte) 'E', outBuf[7]);
|
||||
assertEquals((byte) 'S', outBuf[8]);
|
||||
assertEquals((byte) 'T', outBuf[9]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadBufferSerialization() throws Exception {
|
||||
// Erase/Write (53), WCC (0), SBA (17), Addr(0), SF (29), Attr(0xC0), Text "OK"
|
||||
byte[] stream = new byte[9];
|
||||
stream[0] = 53; // Erase / Write
|
||||
stream[1] = 0; // WCC
|
||||
stream[2] = 17; // SBA
|
||||
dataStream.encodeAddress(stream, 3, 0);
|
||||
stream[5] = 29; // SF
|
||||
stream[6] = (byte) 0xC0; // Unmodified attribute
|
||||
stream[7] = (byte) 'O';
|
||||
stream[8] = (byte) 'K';
|
||||
|
||||
dataStream.analyze(stream, stream.length);
|
||||
assertEquals(1, screen.fieldCount());
|
||||
|
||||
byte[] outBuf = new byte[100];
|
||||
int len = dataStream.readBuffer(outBuf);
|
||||
assertTrue(len >= 8);
|
||||
assertEquals(screen.getAID().translate(), outBuf[0]);
|
||||
assertEquals(17, outBuf[3]); // SBA
|
||||
assertEquals(29, outBuf[6]); // SF
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEpiScreenBufferBridge() {
|
||||
screen.setWidth(80);
|
||||
screen.setDepth(24);
|
||||
screen.setCursor(2, 5);
|
||||
|
||||
Field f = new Field(screen, 80);
|
||||
f.setAttribute(true);
|
||||
f.setBaseAttribute((char) 0xC8);
|
||||
f.setBytes(0, new byte[]{(byte) 0xC1, (byte) 0xC2}, 2); // 'A', 'B'
|
||||
screen.insertField(f);
|
||||
|
||||
ScreenBuffer sb = new ScreenBuffer();
|
||||
EpiScreenBufferBridge.copyToScreenBuffer(screen, sb);
|
||||
|
||||
assertEquals(80, sb.getCols());
|
||||
assertEquals(24, sb.getRows());
|
||||
assertEquals(84, sb.getCursorAddress()); // row 2 (index 1) * 80 + col 5 (index 4) = 84
|
||||
|
||||
// Reverse copy
|
||||
Screen backScreen = new Screen();
|
||||
EpiScreenBufferBridge.copyFromScreenBuffer(sb, backScreen);
|
||||
assertEquals(80, backScreen.getWidth());
|
||||
assertEquals(24, backScreen.getDepth());
|
||||
assertEquals(2, backScreen.getCursorRow());
|
||||
assertEquals(5, backScreen.getCursorColumn());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user