Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0201c7c277
|
|||
|
b61702a57c
|
|||
|
bd4c1f3040
|
|||
|
0d6ab31f0a
|
|||
|
1f0e36c561
|
|||
|
8286af19e6
|
|||
|
3d3a939ece
|
|||
|
d05f1a4c45
|
|||
|
aa724fbcef
|
|||
|
8a30efe9c0
|
|||
|
5049d8df74
|
|||
|
badd026055
|
|||
|
ef70ea0f5a
|
|||
|
30b1a77b61
|
|||
|
9bd1676ec1
|
|||
|
cfb32bdca5
|
|||
|
754b1de7cb
|
|||
|
7892229a2c
|
|||
|
14bf7ba4b1
|
|||
|
bdfe6eec2a
|
|||
|
c28c097e25
|
|||
|
46a022d86b
|
|||
|
59925c2787
|
|||
|
f8fc0e4b22
|
|||
|
dbcb0aa1b6
|
|||
|
50a4279446
|
|||
|
6331e0108b
|
|||
|
79d493c24d
|
|||
|
74e30d1ead
|
|||
|
cbb310b889
|
|||
|
6296cf4341
|
|||
|
db6043cb39
|
|||
|
161364a95f
|
|||
|
a56aa5f059
|
|||
|
7b5bc7ec11
|
|||
|
a9956298e2
|
@@ -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
|
||||
|
||||
@@ -20,21 +20,21 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Java JDK (Temurin 17)
|
||||
- name: Set up Java JDK (Temurin 11)
|
||||
uses: actions/setup-java@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '11'
|
||||
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 11 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-11-jdk-headless || true
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache openjdk17 || true
|
||||
apk add --no-cache openjdk11 || true
|
||||
fi
|
||||
fi
|
||||
echo "Java compiler:"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="124 738 320 320"
|
||||
width="320"
|
||||
height="320"
|
||||
xml:space="preserve"
|
||||
id="svg_j3270_icon"
|
||||
version="1.1">
|
||||
<metadata id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
|
||||
<rect x="124" y="738" width="320" height="320" fill="#000000" />
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g id="g14">
|
||||
<!-- Green Terminal Screen Frame -->
|
||||
<path
|
||||
id="path28"
|
||||
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
|
||||
<!-- Blue Wave / Exponential Curve -->
|
||||
<path
|
||||
id="path30"
|
||||
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
|
||||
<!-- Red Circular Arc -->
|
||||
<path
|
||||
id="path32"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
|
||||
<!-- Yellow Smiley Head -->
|
||||
<path
|
||||
id="path34"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
|
||||
<!-- 'l' -->
|
||||
<path
|
||||
id="path36"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<path
|
||||
id="path38"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<!-- 'i' -->
|
||||
<path
|
||||
id="path40"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<path
|
||||
id="path42"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<!-- 'b' -->
|
||||
<path
|
||||
id="path44"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<path
|
||||
id="path46"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<!-- '3' -->
|
||||
<path
|
||||
id="path48"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<path
|
||||
id="path50"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<!-- '2' -->
|
||||
<path
|
||||
id="path52"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<path
|
||||
id="path54"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<!-- '7' -->
|
||||
<path
|
||||
id="path56"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<path
|
||||
id="path58"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<!-- '0' -->
|
||||
<path
|
||||
id="path60"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<path
|
||||
id="path62"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<!-- 'j' -->
|
||||
<path
|
||||
id="path64"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<path
|
||||
id="path66"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<!-- Smile -->
|
||||
<path
|
||||
id="path68"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
|
||||
<!-- Left Eye (cyan) -->
|
||||
<path
|
||||
id="path70"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
|
||||
<!-- Right Eye (cyan) -->
|
||||
<path
|
||||
id="path72"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
|
||||
<!-- Nose -->
|
||||
<path
|
||||
id="path74"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2892.03,1528.23 -29.21,-20.68" />
|
||||
<path
|
||||
id="path76"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2862.82,1507.55 h 32.86" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="144 814 280 169"
|
||||
width="280"
|
||||
height="169"
|
||||
xml:space="preserve"
|
||||
id="svg_lib3270j_cropped"
|
||||
version="1.1">
|
||||
<metadata id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
|
||||
<rect x="144" y="814" width="280" height="169" fill="#000000" />
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g id="g14">
|
||||
<!-- Green Terminal Screen Frame -->
|
||||
<path
|
||||
id="path28"
|
||||
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
|
||||
<!-- Blue Wave / Exponential Curve -->
|
||||
<path
|
||||
id="path30"
|
||||
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
|
||||
<!-- Red Circular Arc -->
|
||||
<path
|
||||
id="path32"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
|
||||
<!-- Yellow Smiley Head -->
|
||||
<path
|
||||
id="path34"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
|
||||
<!-- 'l' -->
|
||||
<path
|
||||
id="path36"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<path
|
||||
id="path38"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<!-- 'i' -->
|
||||
<path
|
||||
id="path40"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<path
|
||||
id="path42"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<!-- 'b' -->
|
||||
<path
|
||||
id="path44"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<path
|
||||
id="path46"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<!-- '3' -->
|
||||
<path
|
||||
id="path48"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<path
|
||||
id="path50"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<!-- '2' -->
|
||||
<path
|
||||
id="path52"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<path
|
||||
id="path54"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<!-- '7' -->
|
||||
<path
|
||||
id="path56"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<path
|
||||
id="path58"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<!-- '0' -->
|
||||
<path
|
||||
id="path60"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<path
|
||||
id="path62"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<!-- 'j' -->
|
||||
<path
|
||||
id="path64"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<path
|
||||
id="path66"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<!-- Smile -->
|
||||
<path
|
||||
id="path68"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
|
||||
<!-- Left Eye (cyan) -->
|
||||
<path
|
||||
id="path70"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
|
||||
<!-- Right Eye (cyan) -->
|
||||
<path
|
||||
id="path72"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
|
||||
<!-- Nose -->
|
||||
<path
|
||||
id="path74"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2892.03,1528.23 -29.21,-20.68" />
|
||||
<path
|
||||
id="path76"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2862.82,1507.55 h 32.86" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -118,7 +134,7 @@ public class TestRunner {
|
||||
public static void main(String[] args) {
|
||||
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
|
||||
.selectors(
|
||||
selectPackage("haus.nightmare.lib3270j")
|
||||
selectPackage("haus.nightmare")
|
||||
)
|
||||
.build();
|
||||
|
||||
@@ -131,9 +147,7 @@ public class TestRunner {
|
||||
summary.printTo(new PrintWriter(System.out));
|
||||
summary.printFailuresTo(new PrintWriter(System.err));
|
||||
|
||||
if (summary.getTotalFailureCount() > 0) {
|
||||
System.exit(1);
|
||||
}
|
||||
System.exit(summary.getTotalFailureCount() > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -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,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" "$@"
|
||||
|
||||
@@ -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
|
||||
@@ -3,6 +3,11 @@
|
||||
; or: ./run.sh -c j3270.ini
|
||||
|
||||
[appearance]
|
||||
; UI font settings (menus, dialogs, status bar)
|
||||
; uiFontFamily = SansSerif
|
||||
; uiFontSize = 13
|
||||
;
|
||||
; Terminal font settings (3270 screen display)
|
||||
; fontFamily = IBM 3270
|
||||
; fontSize = 18
|
||||
|
||||
@@ -10,6 +15,7 @@
|
||||
; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING
|
||||
; autoConnectHost = mainframe.example.com
|
||||
; autoConnectPort = 23
|
||||
; blockPaste = true
|
||||
|
||||
[colors]
|
||||
; Host colors 0-15 (hex RGB)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package haus.nightmare.j3270.config;
|
||||
|
||||
import haus.nightmare.j3270.ui.UITheme;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.awt.Color;
|
||||
import java.io.*;
|
||||
@@ -17,12 +18,22 @@ public class Settings {
|
||||
AUTO_CONNECT
|
||||
}
|
||||
|
||||
public static UITheme getJavaUiTheme() {
|
||||
String themeStr = prefs.get("javaUiTheme", UITheme.DARK.name());
|
||||
return UITheme.fromString(themeStr);
|
||||
}
|
||||
|
||||
public static void setJavaUiTheme(UITheme theme) {
|
||||
prefs.put("javaUiTheme", (theme != null ? theme : UITheme.DARK).name());
|
||||
}
|
||||
|
||||
public static String getFontFamily() {
|
||||
return prefs.get("fontFamily", "Monospaced");
|
||||
}
|
||||
|
||||
public static void setFontFamily(String family) {
|
||||
prefs.put("fontFamily", family);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static int getFontSize() {
|
||||
@@ -31,6 +42,32 @@ public class Settings {
|
||||
|
||||
public static void setFontSize(int size) {
|
||||
prefs.putInt("fontSize", size);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static String getUiFontFamily() {
|
||||
return prefs.get("uiFontFamily", "SansSerif");
|
||||
}
|
||||
|
||||
public static void setUiFontFamily(String family) {
|
||||
prefs.put("uiFontFamily", (family != null && !family.trim().isEmpty()) ? family : "SansSerif");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static int getUiFontSize() {
|
||||
return prefs.getInt("uiFontSize", 13);
|
||||
}
|
||||
|
||||
public static void setUiFontSize(int size) {
|
||||
prefs.putInt("uiFontSize", Math.max(8, Math.min(size, 48)));
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
private static void flushPrefs() {
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static StartupBehavior getStartupBehavior() {
|
||||
@@ -86,6 +123,94 @@ 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 String getDefaultHostType() {
|
||||
return prefs.get("defaultHostType", "TSO");
|
||||
}
|
||||
|
||||
public static void setDefaultHostType(String hostType) {
|
||||
prefs.put("defaultHostType", (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -103,6 +228,22 @@ public class Settings {
|
||||
prefs.put("codePage", (codePage != null && !codePage.trim().isEmpty()) ? codePage.trim() : "037");
|
||||
}
|
||||
|
||||
public static int getDynamicRows() {
|
||||
return prefs.getInt("dynamicRows", 62);
|
||||
}
|
||||
|
||||
public static void setDynamicRows(int rows) {
|
||||
prefs.putInt("dynamicRows", Math.max(1, rows));
|
||||
}
|
||||
|
||||
public static int getDynamicCols() {
|
||||
return prefs.getInt("dynamicCols", 160);
|
||||
}
|
||||
|
||||
public static void setDynamicCols(int cols) {
|
||||
prefs.putInt("dynamicCols", Math.max(1, cols));
|
||||
}
|
||||
|
||||
public static Color getColorOverride(int index, Color defaultColor) {
|
||||
String hex = prefs.get("color_" + index, null);
|
||||
try {
|
||||
@@ -129,8 +270,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) {
|
||||
@@ -223,12 +417,288 @@ public class Settings {
|
||||
prefs.putBoolean("blockSelectMode", block);
|
||||
}
|
||||
|
||||
// ========== Block paste ==========
|
||||
|
||||
public static boolean getBlockPaste() {
|
||||
return prefs.getBoolean("blockPaste", true);
|
||||
}
|
||||
|
||||
public static void setBlockPaste(boolean block) {
|
||||
prefs.putBoolean("blockPaste", block);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Clipboard & Tabular Paste ==========
|
||||
|
||||
public static boolean getEnablePasteFromExcel() {
|
||||
return prefs.getBoolean("enablePasteFromExcel", true);
|
||||
}
|
||||
|
||||
public static void setEnablePasteFromExcel(boolean val) {
|
||||
prefs.putBoolean("enablePasteFromExcel", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getPasteStopAtProtectedLine() {
|
||||
return prefs.getBoolean("pasteStopAtProtectedLine", false);
|
||||
}
|
||||
|
||||
public static void setPasteStopAtProtectedLine(boolean val) {
|
||||
prefs.putBoolean("pasteStopAtProtectedLine", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getTrimRectSizingHandles() {
|
||||
return prefs.getBoolean("trimRectSizingHandles", false);
|
||||
}
|
||||
|
||||
public static void setTrimRectSizingHandles(boolean val) {
|
||||
prefs.putBoolean("trimRectSizingHandles", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getTrimRectRemainAfterEdit() {
|
||||
return prefs.getBoolean("trimRectRemainAfterEdit", false);
|
||||
}
|
||||
|
||||
public static void setTrimRectRemainAfterEdit(boolean val) {
|
||||
prefs.putBoolean("trimRectRemainAfterEdit", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getPasteWordBreak() {
|
||||
return prefs.getBoolean("pasteWordBreak", true);
|
||||
}
|
||||
|
||||
public static void setPasteWordBreak(boolean val) {
|
||||
prefs.putBoolean("pasteWordBreak", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Crosshair Ruler ==========
|
||||
|
||||
public static boolean getCrosshairRuler() {
|
||||
return prefs.getBoolean("crosshairRuler", false);
|
||||
}
|
||||
|
||||
public static void setCrosshairRuler(boolean enabled) {
|
||||
prefs.putBoolean("crosshairRuler", enabled);
|
||||
}
|
||||
|
||||
// ========== Phase 25: Hotspots & Mouse Wheel Navigation ==========
|
||||
|
||||
public static boolean getHotspotsEnabled() {
|
||||
return prefs.getBoolean("hotspotsEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsUrlEnabled() {
|
||||
return prefs.getBoolean("hotspotsUrlEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsUrlEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsUrlEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsPfEnabled() {
|
||||
return prefs.getBoolean("hotspotsPfEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsPfEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsPfEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsMenuChoicesEnabled() {
|
||||
return prefs.getBoolean("hotspotsMenuChoicesEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsMenuChoicesEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsMenuChoicesEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getMouseWheelEnabled() {
|
||||
return prefs.getBoolean("mouseWheelEnabled", true);
|
||||
}
|
||||
|
||||
public static void setMouseWheelEnabled(boolean enabled) {
|
||||
prefs.putBoolean("mouseWheelEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static String getMouseWheelUpKey() {
|
||||
return prefs.get("mouseWheelUpKey", "[pf7]");
|
||||
}
|
||||
|
||||
public static void setMouseWheelUpKey(String key) {
|
||||
prefs.put("mouseWheelUpKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf7]");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static String getMouseWheelDownKey() {
|
||||
return prefs.get("mouseWheelDownKey", "[pf8]");
|
||||
}
|
||||
|
||||
public static void setMouseWheelDownKey(String key) {
|
||||
prefs.put("mouseWheelDownKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf8]");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Status Bar Visibility ==========
|
||||
|
||||
public static boolean getStatusBarVisible() {
|
||||
return prefs.getBoolean("statusBarVisible", true);
|
||||
}
|
||||
|
||||
public static void setStatusBarVisible(boolean visible) {
|
||||
prefs.putBoolean("statusBarVisible", visible);
|
||||
}
|
||||
|
||||
// ========== Cursor Style ==========
|
||||
|
||||
public static String getCursorStyle() {
|
||||
return prefs.get("cursorStyle", "BLOCK");
|
||||
}
|
||||
|
||||
public static void setCursorStyle(String style) {
|
||||
prefs.put("cursorStyle", style != null ? style.toUpperCase() : "BLOCK");
|
||||
}
|
||||
|
||||
// ========== Entry Assist & Modes ==========
|
||||
|
||||
public static boolean getEntryAssistDocMode() {
|
||||
return prefs.getBoolean("entryAssistDocMode", false);
|
||||
}
|
||||
public static void setEntryAssistDocMode(boolean docMode) {
|
||||
prefs.putBoolean("entryAssistDocMode", docMode);
|
||||
}
|
||||
|
||||
public static boolean getEntryAssistWordWrap() {
|
||||
return prefs.getBoolean("entryAssistWordWrap", false);
|
||||
}
|
||||
public static void setEntryAssistWordWrap(boolean wordWrap) {
|
||||
prefs.putBoolean("entryAssistWordWrap", wordWrap);
|
||||
}
|
||||
|
||||
public static int getEntryAssistStartCol() {
|
||||
return prefs.getInt("entryAssistStartCol", 1);
|
||||
}
|
||||
public static void setEntryAssistStartCol(int startCol) {
|
||||
prefs.putInt("entryAssistStartCol", startCol);
|
||||
}
|
||||
|
||||
public static int getEntryAssistEndCol() {
|
||||
return prefs.getInt("entryAssistEndCol", 80);
|
||||
}
|
||||
public static void setEntryAssistEndCol(int endCol) {
|
||||
prefs.putInt("entryAssistEndCol", endCol);
|
||||
}
|
||||
|
||||
public static boolean getEntryAssistBell() {
|
||||
return prefs.getBoolean("entryAssistBell", false);
|
||||
}
|
||||
public static void setEntryAssistBell(boolean bell) {
|
||||
prefs.putBoolean("entryAssistBell", bell);
|
||||
}
|
||||
|
||||
public static int getEntryAssistBellCol() {
|
||||
return prefs.getInt("entryAssistBellCol", 75);
|
||||
}
|
||||
public static void setEntryAssistBellCol(int col) {
|
||||
prefs.putInt("entryAssistBellCol", col);
|
||||
}
|
||||
|
||||
public static String getEntryAssistTabStops() {
|
||||
return prefs.get("entryAssistTabStops", "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75");
|
||||
}
|
||||
public static void setEntryAssistTabStops(String stops) {
|
||||
prefs.put("entryAssistTabStops", stops != null ? stops : "");
|
||||
}
|
||||
|
||||
public static int[] getEntryAssistTabStopsArray() {
|
||||
String s = getEntryAssistTabStops();
|
||||
if (s == null || s.trim().isEmpty()) return new int[0];
|
||||
String[] parts = s.split(",");
|
||||
java.util.List<Integer> list = new java.util.ArrayList<>();
|
||||
for (String p : parts) {
|
||||
try {
|
||||
int val = Integer.parseInt(p.trim());
|
||||
if (val > 0) list.add(val - 1);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
int[] res = new int[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) res[i] = list.get(i);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static boolean getInsertOffOnAid() {
|
||||
return prefs.getBoolean("insertOffOnAid", true);
|
||||
}
|
||||
public static void setInsertOffOnAid(boolean val) {
|
||||
prefs.putBoolean("insertOffOnAid", val);
|
||||
}
|
||||
|
||||
public static boolean getFourColorOverride() {
|
||||
return prefs.getBoolean("fourColorOverride", false);
|
||||
}
|
||||
public static void setFourColorOverride(boolean val) {
|
||||
prefs.putBoolean("fourColorOverride", val);
|
||||
}
|
||||
|
||||
public static boolean getNumericFieldLock() {
|
||||
return prefs.getBoolean("numericFieldLock", true);
|
||||
}
|
||||
public static void setNumericFieldLock(boolean val) {
|
||||
prefs.putBoolean("numericFieldLock", val);
|
||||
}
|
||||
|
||||
public static boolean getAutoSkipEnabled() {
|
||||
return prefs.getBoolean("autoSkipEnabled", true);
|
||||
}
|
||||
public static void setAutoSkipEnabled(boolean val) {
|
||||
prefs.putBoolean("autoSkipEnabled", val);
|
||||
}
|
||||
|
||||
private static void applyConfigEntry(String section, String key, String value) {
|
||||
switch (section) {
|
||||
case "appearance":
|
||||
switch (key) {
|
||||
case "fontFamily": setFontFamily(value); break;
|
||||
case "fontSize": setFontSize(Integer.parseInt(value)); break;
|
||||
case "uiFontFamily":
|
||||
case "uifontfamily":
|
||||
case "uifont":
|
||||
case "uiFont":
|
||||
setUiFontFamily(value);
|
||||
break;
|
||||
case "uiFontSize":
|
||||
case "uifontsize":
|
||||
setUiFontSize(Integer.parseInt(value));
|
||||
break;
|
||||
case "javaUiTheme":
|
||||
case "theme":
|
||||
case "uiTheme":
|
||||
setJavaUiTheme(UITheme.fromString(value));
|
||||
break;
|
||||
case "crosshairRuler":
|
||||
case "ruler":
|
||||
setCrosshairRuler(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "statusBarVisible":
|
||||
case "statusbar":
|
||||
case "statusBar":
|
||||
case "showStatusBar":
|
||||
setStatusBarVisible(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "cursorStyle":
|
||||
setCursorStyle(value);
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown appearance key: " + key);
|
||||
}
|
||||
@@ -269,12 +739,108 @@ public class Settings {
|
||||
case "charset":
|
||||
setCodePage(value);
|
||||
break;
|
||||
case "dynamicRows":
|
||||
case "dynamic_rows":
|
||||
setDynamicRows(Integer.parseInt(value));
|
||||
break;
|
||||
case "dynamicCols":
|
||||
case "dynamic_cols":
|
||||
setDynamicCols(Integer.parseInt(value));
|
||||
break;
|
||||
case "hostType":
|
||||
case "hosttype":
|
||||
case "host_type":
|
||||
setDefaultHostType(value);
|
||||
break;
|
||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
|
||||
case "autoReconnect":
|
||||
case "auto_reconnect":
|
||||
case "autoConnectAutoReconnect":
|
||||
setAutoConnectAutoReconnect(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "reconnectMaxRetries":
|
||||
case "autoConnectReconnectMaxRetries":
|
||||
setAutoConnectReconnectMaxRetries(Integer.parseInt(value));
|
||||
break;
|
||||
case "inputMask":
|
||||
case "input_mask":
|
||||
case "inputMaskEnabled":
|
||||
case "maskInput":
|
||||
setInputMask(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "inputMaskChar":
|
||||
case "input_mask_char":
|
||||
case "maskChar":
|
||||
setInputMaskChar(value);
|
||||
break;
|
||||
case "autoSysUnlock":
|
||||
case "auto_sys_unlock":
|
||||
setAutoSysUnlock(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "blockPasteMode":
|
||||
setBlockPaste(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "enablePasteFromExcel":
|
||||
case "pasteFromExcel":
|
||||
case "excelPaste":
|
||||
setEnablePasteFromExcel(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteStopAtProtectedLine":
|
||||
case "stopAtProtected":
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectSizingHandles":
|
||||
case "sizingHandles":
|
||||
setTrimRectSizingHandles(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectRemainAfterEdit":
|
||||
case "remainAfterEdit":
|
||||
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteWordBreak":
|
||||
case "wordBreak":
|
||||
setPasteWordBreak(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown behavior/connection key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "clipboard":
|
||||
switch (key) {
|
||||
case "blockPaste":
|
||||
case "blockPasteMode":
|
||||
setBlockPaste(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "enablePasteFromExcel":
|
||||
case "pasteFromExcel":
|
||||
case "excelPaste":
|
||||
setEnablePasteFromExcel(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteStopAtProtectedLine":
|
||||
case "stopAtProtected":
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectSizingHandles":
|
||||
case "sizingHandles":
|
||||
setTrimRectSizingHandles(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectRemainAfterEdit":
|
||||
case "remainAfterEdit":
|
||||
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteWordBreak":
|
||||
case "wordBreak":
|
||||
setPasteWordBreak(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown clipboard key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "colors":
|
||||
if (key.startsWith("color_")) {
|
||||
int index = Integer.parseInt(key.substring(6));
|
||||
@@ -299,6 +865,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);
|
||||
@@ -322,8 +937,14 @@ public class Settings {
|
||||
|
||||
// [appearance]
|
||||
w.println("[appearance]");
|
||||
w.println("javaUiTheme = " + getJavaUiTheme().name());
|
||||
w.println("uiFontFamily = " + getUiFontFamily());
|
||||
w.println("uiFontSize = " + getUiFontSize());
|
||||
w.println("fontFamily = " + getFontFamily());
|
||||
w.println("fontSize = " + getFontSize());
|
||||
w.println("crosshairRuler = " + getCrosshairRuler());
|
||||
w.println("statusBarVisible = " + getStatusBarVisible());
|
||||
w.println("cursorStyle = " + getCursorStyle());
|
||||
w.println();
|
||||
|
||||
// [behavior]
|
||||
@@ -338,7 +959,40 @@ public class Settings {
|
||||
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
|
||||
}
|
||||
w.println("codePage = " + getCodePage());
|
||||
w.println("hostType = " + getDefaultHostType());
|
||||
w.println("dynamicRows = " + getDynamicRows());
|
||||
w.println("dynamicCols = " + getDynamicCols());
|
||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||
w.println("blockPaste = " + getBlockPaste());
|
||||
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
|
||||
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
|
||||
w.println("inputMask = " + getInputMask());
|
||||
w.println("inputMaskChar = " + getInputMaskChar());
|
||||
w.println("autoSysUnlock = " + getAutoSysUnlock());
|
||||
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
|
||||
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
|
||||
w.println("trimRectSizingHandles = " + getTrimRectSizingHandles());
|
||||
w.println("trimRectRemainAfterEdit = " + getTrimRectRemainAfterEdit());
|
||||
w.println("pasteWordBreak = " + getPasteWordBreak());
|
||||
w.println();
|
||||
|
||||
// [entryassist]
|
||||
w.println("[entryassist]");
|
||||
w.println("docMode = " + getEntryAssistDocMode());
|
||||
w.println("wordWrap = " + getEntryAssistWordWrap());
|
||||
w.println("startCol = " + getEntryAssistStartCol());
|
||||
w.println("endCol = " + getEntryAssistEndCol());
|
||||
w.println("bell = " + getEntryAssistBell());
|
||||
w.println("bellCol = " + getEntryAssistBellCol());
|
||||
w.println("tabStops = " + getEntryAssistTabStops());
|
||||
w.println();
|
||||
|
||||
// [modes]
|
||||
w.println("[modes]");
|
||||
w.println("insertOffOnAid = " + getInsertOffOnAid());
|
||||
w.println("fourColorOverride = " + getFourColorOverride());
|
||||
w.println("numericFieldLock = " + getNumericFieldLock());
|
||||
w.println("autoSkip = " + getAutoSkipEnabled());
|
||||
w.println();
|
||||
|
||||
// [graphics]
|
||||
@@ -367,9 +1021,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) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.ui.AppIcon;
|
||||
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.j3270.ui.ThemeManager;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConstants;
|
||||
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
@@ -11,6 +14,52 @@ 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 FTConfig.AllocationUnit units = FTConfig.AllocationUnit.TRACKS;
|
||||
public String dirBlocks = "";
|
||||
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;
|
||||
|
||||
@@ -36,6 +85,8 @@ public class FileTransferDialog extends JDialog {
|
||||
private JTextField lreclField;
|
||||
private JTextField blksizeField;
|
||||
private JTextField spaceField;
|
||||
private JComboBox<FTConfig.AllocationUnit> unitsCombo;
|
||||
private JTextField dirField;
|
||||
|
||||
// VM specific
|
||||
private JTextField optionsField;
|
||||
@@ -47,6 +98,7 @@ public class FileTransferDialog extends JDialog {
|
||||
super(owner, "File Transfer (IND$FILE)", false);
|
||||
this.owner = owner;
|
||||
this.coordinator = coordinator;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI();
|
||||
pack();
|
||||
@@ -65,6 +117,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
|
||||
@@ -87,7 +149,17 @@ 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());
|
||||
JPanel localPanel = new JPanel(new BorderLayout(5, 0));
|
||||
localPanel.setOpaque(false);
|
||||
@@ -98,6 +170,7 @@ public class FileTransferDialog extends JDialog {
|
||||
// Host File
|
||||
hostFileField = new JTextField(20);
|
||||
browseHostButton = new JButton("Browse Host...");
|
||||
ThemeManager.styleButton(browseHostButton, ThemeManager.ButtonVariant.DEFAULT);
|
||||
browseHostButton.addActionListener(e -> browseHostDirectory());
|
||||
JPanel hostPanel = new JPanel(new BorderLayout(5, 0));
|
||||
hostPanel.setOpaque(false);
|
||||
@@ -152,12 +225,9 @@ public class FileTransferDialog extends JDialog {
|
||||
formPanel.add(createRow("File Options:", optionsPanel2));
|
||||
|
||||
// Host-specific options panel (TSO dataset allocation)
|
||||
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
|
||||
JPanel hostOptsPanel = new JPanel(new GridLayout(3, 4, 5, 5));
|
||||
hostOptsPanel.setOpaque(false);
|
||||
hostOptsPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
"TSO Allocation Options (Send Only)"));
|
||||
((javax.swing.border.TitledBorder)hostOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
|
||||
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
|
||||
|
||||
hostOptsPanel.add(new JLabel("RECFM:"));
|
||||
recfmField = new JTextField(5);
|
||||
@@ -171,20 +241,31 @@ public class FileTransferDialog extends JDialog {
|
||||
blksizeField = new JTextField(5);
|
||||
hostOptsPanel.add(blksizeField);
|
||||
|
||||
hostOptsPanel.add(new JLabel("SPACE:"));
|
||||
hostOptsPanel.add(new JLabel("Units:"));
|
||||
unitsCombo = new JComboBox<>(new FTConfig.AllocationUnit[]{
|
||||
FTConfig.AllocationUnit.TRACKS,
|
||||
FTConfig.AllocationUnit.CYLINDERS,
|
||||
FTConfig.AllocationUnit.AVBLOCK,
|
||||
FTConfig.AllocationUnit.DEFAULT
|
||||
});
|
||||
unitsCombo.setSelectedItem(FTConfig.AllocationUnit.TRACKS);
|
||||
hostOptsPanel.add(unitsCombo);
|
||||
|
||||
hostOptsPanel.add(new JLabel("SPACE (Pri,Sec):"));
|
||||
spaceField = new JTextField(15);
|
||||
hostOptsPanel.add(spaceField);
|
||||
|
||||
hostOptsPanel.add(new JLabel("Directory (PDS):"));
|
||||
dirField = new JTextField(5);
|
||||
hostOptsPanel.add(dirField);
|
||||
|
||||
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
formPanel.add(hostOptsPanel);
|
||||
|
||||
// CMS options
|
||||
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
|
||||
vmOptsPanel.setOpaque(false);
|
||||
vmOptsPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(new Color(60, 60, 60)),
|
||||
"CMS Options"));
|
||||
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
|
||||
vmOptsPanel.setBorder(ThemeManager.createTitledBorder("CMS Options"));
|
||||
|
||||
optionsField = new JTextField(20);
|
||||
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
|
||||
@@ -200,9 +281,11 @@ public class FileTransferDialog extends JDialog {
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
transferButton = new JButton("Start Transfer");
|
||||
ThemeManager.styleButton(transferButton, ThemeManager.ButtonVariant.PRIMARY);
|
||||
transferButton.addActionListener(e -> startTransfer());
|
||||
|
||||
cancelButton = new JButton("Close");
|
||||
ThemeManager.styleButton(cancelButton, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelButton.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(cancelButton);
|
||||
@@ -212,13 +295,63 @@ public class FileTransferDialog extends JDialog {
|
||||
|
||||
setContentPane(mainPanel);
|
||||
|
||||
applyTheme(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.units != null) unitsCombo.setSelectedItem(lastTransferState.units);
|
||||
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
|
||||
if (lastTransferState.dirBlocks != null) dirField.setText(lastTransferState.dirBlocks);
|
||||
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
|
||||
}
|
||||
|
||||
// When using a saved connection (or when no previous transfer state exists for this connection),
|
||||
// use the configured host type (CMS/TSO/CICS) for File Transfers
|
||||
if (coordinator != null && coordinator.getClient() != null) {
|
||||
haus.nightmare.lib3270j.ConnectionConfig cfg = coordinator.getClient().getConfig();
|
||||
if (cfg != null) {
|
||||
FTConfig.HostType configuredType = cfg.getFtHostType();
|
||||
if (configuredType != null) {
|
||||
if (cfg.isSavedConnection() || lastTransferState == null) {
|
||||
hostTypeCombo.setSelectedItem(configuredType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -226,10 +359,13 @@ 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);
|
||||
unitsCombo.setEnabled(isSend && isTso);
|
||||
spaceField.setEnabled(isSend && isTso);
|
||||
dirField.setEnabled(isSend && isTso);
|
||||
optionsField.setEnabled(isCms);
|
||||
}
|
||||
|
||||
private JPanel createRow(String labelText, Component comp) {
|
||||
@@ -246,9 +382,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,15 +396,41 @@ public class FileTransferDialog extends JDialog {
|
||||
private void browseHostDirectory() {
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
String initialQuery = hostFileField.getText().trim();
|
||||
if (hostType == FTConfig.HostType.CMS && (initialQuery.isEmpty() || initialQuery.equals("* * *") || initialQuery.equals("*"))) {
|
||||
initialQuery = "* * A";
|
||||
}
|
||||
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
||||
hostFileField.setText(dialog.getSelectedHostFile());
|
||||
if (localFileField.getText().trim().isEmpty()) {
|
||||
HostDirectoryEntry entry = dialog.getSelectedEntry();
|
||||
if (entry != null) {
|
||||
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
|
||||
recfmField.setText(entry.getRecfm().trim());
|
||||
}
|
||||
if (entry.getLrecl() > 0) {
|
||||
lreclField.setText(String.valueOf(entry.getLrecl()));
|
||||
}
|
||||
if (entry.getBlksize() > 0) {
|
||||
blksizeField.setText(String.valueOf(entry.getBlksize()));
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -296,9 +462,14 @@ public class FileTransferDialog extends JDialog {
|
||||
config.setRecfm(recfmField.getText().trim());
|
||||
config.setLrecl(lreclField.getText().trim());
|
||||
config.setBlksize(blksizeField.getText().trim());
|
||||
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
|
||||
config.setSpace(spaceField.getText().trim());
|
||||
config.setDirectoryBlocks(dirField.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);
|
||||
@@ -307,6 +478,49 @@ 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.units = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||
state.space = spaceField.getText().trim();
|
||||
state.dirBlocks = dirField.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 JComboBox<FTConfig.AllocationUnit> getUnitsCombo() { return unitsCombo; }
|
||||
public JTextField getSpaceField() { return spaceField; }
|
||||
public JTextField getDirField() { return dirField; }
|
||||
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,695 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Storage manager for saved host profiles, maintaining 1:1 format compatibility with a3270.
|
||||
* Persists profiles in Java user preferences with JSON serialization.
|
||||
*/
|
||||
public class HostStorage {
|
||||
|
||||
private static final Logger log = Logger.getLogger(HostStorage.class.getName());
|
||||
private static final Preferences prefs = Preferences.userNodeForPackage(HostStorage.class);
|
||||
public static final String KEY_SAVED_HOSTS = "saved_hosts";
|
||||
|
||||
public static synchronized List<SavedHost> getSavedHosts() {
|
||||
String jsonStr = prefs.get(KEY_SAVED_HOSTS, null);
|
||||
if (jsonStr == null || jsonStr.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return parseHostsJson(jsonStr);
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Failed to parse saved hosts JSON", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void saveHost(SavedHost host) {
|
||||
if (host == null) return;
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
int index = -1;
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
if (Objects.equals(current.get(i).getId(), host.getId())) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If this host is set to autoConnect, clear autoConnect on all others
|
||||
SavedHost hostToSave = host.copy();
|
||||
if (hostToSave.isAutoConnect()) {
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
current.get(i).setAutoConnect(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
current.set(index, hostToSave);
|
||||
} else {
|
||||
current.add(hostToSave);
|
||||
}
|
||||
saveAll(current);
|
||||
}
|
||||
|
||||
public static synchronized void deleteHost(String hostId) {
|
||||
if (hostId == null) return;
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
current.removeIf(h -> Objects.equals(h.getId(), hostId));
|
||||
saveAll(current);
|
||||
}
|
||||
|
||||
public static synchronized SavedHost getAutoConnectHost() {
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (h.isAutoConnect()) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized SavedHost findById(String id) {
|
||||
if (id == null) return null;
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (id.equals(h.getId())) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized SavedHost findByName(String name) {
|
||||
if (name == null) return null;
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (name.equalsIgnoreCase(h.getName())) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized void saveAll(List<SavedHost> hosts) {
|
||||
if (hosts == null) return;
|
||||
String json = serializeHostsJson(hosts);
|
||||
prefs.put(KEY_SAVED_HOSTS, json);
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void clearAll() {
|
||||
prefs.remove(KEY_SAVED_HOSTS);
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ================= INI File Export / Import =================
|
||||
|
||||
/**
|
||||
* Export all currently saved host connections to an INI file.
|
||||
*/
|
||||
public static synchronized void exportToIniFile(File file) throws IOException {
|
||||
List<SavedHost> hosts = getSavedHosts();
|
||||
exportToIniFile(file, hosts);
|
||||
}
|
||||
|
||||
public static synchronized void exportToIniFile(String path) throws IOException {
|
||||
exportToIniFile(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a specified list of host connections to an INI file.
|
||||
*/
|
||||
public static void exportToIniFile(File file, List<SavedHost> hosts) throws IOException {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("Target file cannot be null");
|
||||
}
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
|
||||
w.println("; j3270 Saved Connections Configuration File");
|
||||
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
|
||||
w.println("; Connections count: " + (hosts != null ? hosts.size() : 0));
|
||||
w.println();
|
||||
|
||||
if (hosts != null) {
|
||||
for (SavedHost h : hosts) {
|
||||
String section = (h.getName() != null && !h.getName().trim().isEmpty())
|
||||
? h.getName().trim()
|
||||
: (h.getHost() != null && !h.getHost().trim().isEmpty() ? h.getHost().trim() : "Connection");
|
||||
w.println("[" + section + "]");
|
||||
if (h.getId() != null && !h.getId().trim().isEmpty()) {
|
||||
w.println("id = " + h.getId());
|
||||
}
|
||||
w.println("name = " + (h.getName() != null ? h.getName() : ""));
|
||||
w.println("host = " + (h.getHost() != null ? h.getHost() : ""));
|
||||
w.println("port = " + h.getPort());
|
||||
w.println("model = " + h.getModel());
|
||||
w.println("dynamicRows = " + h.getDynamicRows());
|
||||
w.println("dynamicCols = " + h.getDynamicCols());
|
||||
w.println("luName = " + (h.getLuName() != null ? h.getLuName() : ""));
|
||||
w.println("autoConnect = " + h.isAutoConnect());
|
||||
w.println("hostType = " + (h.getHostType() != null ? h.getHostType() : "TSO"));
|
||||
w.println("useTls = " + h.isUseTls());
|
||||
w.println("tlsVerifyCert = " + h.isTlsVerifyCert());
|
||||
w.println("tn3270e = " + h.isTn3270e());
|
||||
w.println("graphicsMode = " + (h.getGraphicsMode() != null ? h.getGraphicsMode() : "BOTH"));
|
||||
w.println("codePage = " + (h.getCodePage() != null ? h.getCodePage() : "037"));
|
||||
w.println("proxyType = " + (h.getProxyType() != null ? h.getProxyType() : "NONE"));
|
||||
w.println("proxyHost = " + (h.getProxyHost() != null ? h.getProxyHost() : ""));
|
||||
w.println("proxyPort = " + h.getProxyPort());
|
||||
w.println("proxyUsername = " + (h.getProxyUsername() != null ? h.getProxyUsername() : ""));
|
||||
w.println("proxyPassword = " + (h.getProxyPassword() != null ? h.getProxyPassword() : ""));
|
||||
w.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Saved connections exported to: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load connections from an INI file without saving them to persistent storage.
|
||||
*/
|
||||
public static List<SavedHost> loadFromIniFile(File file) throws IOException {
|
||||
if (file == null || !file.exists()) {
|
||||
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
|
||||
}
|
||||
List<SavedHost> hosts = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
SavedHost current = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
if (current != null) {
|
||||
hosts.add(current);
|
||||
}
|
||||
String section = line.substring(1, line.length() - 1).trim();
|
||||
if (section.toLowerCase().startsWith("connection:")) {
|
||||
section = section.substring("connection:".length()).trim();
|
||||
} else if (section.toLowerCase().startsWith("host:")) {
|
||||
section = section.substring("host:".length()).trim();
|
||||
}
|
||||
current = new SavedHost();
|
||||
current.setName(section);
|
||||
continue;
|
||||
}
|
||||
int eq = line.indexOf('=');
|
||||
if (eq < 0) {
|
||||
continue;
|
||||
}
|
||||
String key = line.substring(0, eq).trim();
|
||||
String value = line.substring(eq + 1).trim();
|
||||
if (current == null) {
|
||||
current = new SavedHost();
|
||||
current.setName("Default");
|
||||
}
|
||||
applyIniEntry(current, key, value);
|
||||
}
|
||||
if (current != null) {
|
||||
hosts.add(current);
|
||||
}
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
public static List<SavedHost> loadFromIniFile(String path) throws IOException {
|
||||
return loadFromIniFile(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Import connections from an INI file and merge them into persistent storage.
|
||||
* Updates existing profiles matching by ID or Name, and appends new profiles.
|
||||
*/
|
||||
public static synchronized List<SavedHost> importFromIniFile(File file) throws IOException {
|
||||
List<SavedHost> imported = loadFromIniFile(file);
|
||||
if (imported == null || imported.isEmpty()) {
|
||||
return getSavedHosts();
|
||||
}
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
for (SavedHost host : imported) {
|
||||
int index = -1;
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
SavedHost existing = current.get(i);
|
||||
if ((host.getId() != null && !host.getId().trim().isEmpty() && Objects.equals(existing.getId(), host.getId())) ||
|
||||
(host.getName() != null && !host.getName().trim().isEmpty() &&
|
||||
host.getName().equalsIgnoreCase(existing.getName()))) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (host.isAutoConnect()) {
|
||||
for (SavedHost c : current) {
|
||||
c.setAutoConnect(false);
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
if (host.getId() == null || host.getId().trim().isEmpty()) {
|
||||
host.setId(current.get(index).getId());
|
||||
}
|
||||
current.set(index, host);
|
||||
} else {
|
||||
current.add(host);
|
||||
}
|
||||
}
|
||||
saveAll(current);
|
||||
return current;
|
||||
}
|
||||
|
||||
public static synchronized List<SavedHost> importFromIniFile(String path) throws IOException {
|
||||
return importFromIniFile(new File(path));
|
||||
}
|
||||
|
||||
private static void applyIniEntry(SavedHost h, String key, String value) {
|
||||
String lowerKey = key.toLowerCase();
|
||||
switch (lowerKey) {
|
||||
case "id":
|
||||
if (!value.isEmpty()) h.setId(value);
|
||||
break;
|
||||
case "name":
|
||||
case "profilename":
|
||||
case "profile":
|
||||
if (!value.isEmpty()) h.setName(value);
|
||||
break;
|
||||
case "host":
|
||||
case "hostname":
|
||||
case "address":
|
||||
h.setHost(value);
|
||||
break;
|
||||
case "port":
|
||||
h.setPort(parseInt(value, 23));
|
||||
break;
|
||||
case "model":
|
||||
case "terminalmodel":
|
||||
h.setModel(parseInt(value, 4));
|
||||
break;
|
||||
case "dynamicrows":
|
||||
case "dynamic_rows":
|
||||
case "rows":
|
||||
h.setDynamicRows(parseInt(value, 62));
|
||||
break;
|
||||
case "dynamiccols":
|
||||
case "dynamic_cols":
|
||||
case "cols":
|
||||
h.setDynamicCols(parseInt(value, 160));
|
||||
break;
|
||||
case "luname":
|
||||
case "lu_name":
|
||||
case "lu":
|
||||
h.setLuName(value);
|
||||
break;
|
||||
case "autoconnect":
|
||||
case "auto_connect":
|
||||
h.setAutoConnect(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "hosttype":
|
||||
case "host_type":
|
||||
case "type":
|
||||
h.setHostType(value.toUpperCase());
|
||||
break;
|
||||
case "usetls":
|
||||
case "use_tls":
|
||||
case "tls":
|
||||
case "ssl":
|
||||
h.setUseTls(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "tlsverifycert":
|
||||
case "verifycert":
|
||||
case "tls_verify_cert":
|
||||
h.setTlsVerifyCert(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "tn3270e":
|
||||
case "enabletn3270e":
|
||||
h.setTn3270e(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "graphicsmode":
|
||||
case "graphics_mode":
|
||||
case "graphics":
|
||||
h.setGraphicsMode(value.toUpperCase());
|
||||
break;
|
||||
case "codepage":
|
||||
case "code_page":
|
||||
case "charset":
|
||||
h.setCodePage(value);
|
||||
break;
|
||||
case "proxytype":
|
||||
case "proxy_type":
|
||||
h.setProxyType(value.toUpperCase());
|
||||
break;
|
||||
case "proxyhost":
|
||||
case "proxy_host":
|
||||
h.setProxyHost(value);
|
||||
break;
|
||||
case "proxyport":
|
||||
case "proxy_port":
|
||||
h.setProxyPort(parseInt(value, 0));
|
||||
break;
|
||||
case "proxyusername":
|
||||
case "proxy_username":
|
||||
case "proxyuser":
|
||||
h.setProxyUsername(value);
|
||||
break;
|
||||
case "proxypassword":
|
||||
case "proxy_password":
|
||||
case "proxypass":
|
||||
h.setProxyPassword(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ================= JSON Serialization =================
|
||||
|
||||
public static String serializeHostsJson(List<SavedHost> hosts) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[\n");
|
||||
for (int i = 0; i < hosts.size(); i++) {
|
||||
SavedHost h = hosts.get(i);
|
||||
sb.append(" ").append(serializeHostJson(h));
|
||||
if (i < hosts.size() - 1) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String serializeHostJson(SavedHost h) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{");
|
||||
appendField(sb, "id", h.getId(), true);
|
||||
appendField(sb, "name", h.getName(), true);
|
||||
appendField(sb, "host", h.getHost(), true);
|
||||
appendField(sb, "port", h.getPort(), true);
|
||||
appendField(sb, "model", h.getModel(), true);
|
||||
appendField(sb, "dynamicRows", h.getDynamicRows(), true);
|
||||
appendField(sb, "dynamicCols", h.getDynamicCols(), true);
|
||||
appendField(sb, "luName", h.getLuName(), true);
|
||||
appendField(sb, "autoConnect", h.isAutoConnect(), true);
|
||||
appendField(sb, "hostType", h.getHostType(), true);
|
||||
appendField(sb, "useTls", h.isUseTls(), true);
|
||||
appendField(sb, "tlsVerifyCert", h.isTlsVerifyCert(), true);
|
||||
appendField(sb, "tn3270e", h.isTn3270e(), true);
|
||||
appendField(sb, "graphicsMode", h.getGraphicsMode(), true);
|
||||
appendField(sb, "codePage", h.getCodePage(), true);
|
||||
appendField(sb, "proxyType", h.getProxyType(), true);
|
||||
appendField(sb, "proxyHost", h.getProxyHost(), true);
|
||||
appendField(sb, "proxyPort", h.getProxyPort(), true);
|
||||
appendField(sb, "proxyUsername", h.getProxyUsername(), true);
|
||||
appendField(sb, "proxyPassword", h.getProxyPassword(), false);
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, String val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":\"").append(escapeJson(val != null ? val : "")).append("\"");
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, int val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":").append(val);
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, boolean val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":").append(val);
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '"': sb.append("\\\""); break;
|
||||
case '\\': sb.append("\\\\"); break;
|
||||
case '\b': sb.append("\\b"); break;
|
||||
case '\f': sb.append("\\f"); break;
|
||||
case '\n': sb.append("\\n"); break;
|
||||
case '\r': sb.append("\\r"); break;
|
||||
case '\t': sb.append("\\t"); break;
|
||||
default:
|
||||
if (c < 32) {
|
||||
sb.append(String.format("\\u%04x", (int) c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ================= JSON Deserialization =================
|
||||
|
||||
public static List<SavedHost> parseHostsJson(String json) {
|
||||
List<SavedHost> list = new ArrayList<>();
|
||||
if (json == null || json.trim().isEmpty()) return list;
|
||||
|
||||
SimpleJsonParser parser = new SimpleJsonParser(json);
|
||||
Object root = parser.parseValue();
|
||||
if (root instanceof List) {
|
||||
for (Object item : (List<?>) root) {
|
||||
if (item instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) item;
|
||||
list.add(fromMap(map));
|
||||
}
|
||||
}
|
||||
} else if (root instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) root;
|
||||
list.add(fromMap(map));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static SavedHost fromMap(Map<String, Object> map) {
|
||||
SavedHost h = new SavedHost();
|
||||
if (map.containsKey("id")) h.setId(String.valueOf(map.get("id")));
|
||||
if (map.containsKey("name")) h.setName(String.valueOf(map.get("name")));
|
||||
if (map.containsKey("host")) h.setHost(String.valueOf(map.get("host")));
|
||||
if (map.containsKey("port")) h.setPort(parseInt(map.get("port"), 23));
|
||||
if (map.containsKey("model")) h.setModel(parseInt(map.get("model"), 4));
|
||||
if (map.containsKey("dynamicRows")) h.setDynamicRows(parseInt(map.get("dynamicRows"), 62));
|
||||
if (map.containsKey("dynamicCols")) h.setDynamicCols(parseInt(map.get("dynamicCols"), 160));
|
||||
if (map.containsKey("luName")) h.setLuName(String.valueOf(map.get("luName")));
|
||||
if (map.containsKey("autoConnect")) h.setAutoConnect(parseBool(map.get("autoConnect"), false));
|
||||
if (map.containsKey("hostType")) h.setHostType(String.valueOf(map.get("hostType")));
|
||||
if (map.containsKey("useTls")) h.setUseTls(parseBool(map.get("useTls"), false));
|
||||
if (map.containsKey("tlsVerifyCert")) h.setTlsVerifyCert(parseBool(map.get("tlsVerifyCert"), true));
|
||||
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
|
||||
if (map.containsKey("graphicsMode")) {
|
||||
String gm = String.valueOf(map.get("graphicsMode"));
|
||||
h.setGraphicsMode(gm);
|
||||
}
|
||||
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
|
||||
if (map.containsKey("proxyType")) h.setProxyType(String.valueOf(map.get("proxyType")));
|
||||
if (map.containsKey("proxyHost")) h.setProxyHost(String.valueOf(map.get("proxyHost")));
|
||||
if (map.containsKey("proxyPort")) h.setProxyPort(parseInt(map.get("proxyPort"), 0));
|
||||
if (map.containsKey("proxyUsername")) h.setProxyUsername(String.valueOf(map.get("proxyUsername")));
|
||||
if (map.containsKey("proxyPassword")) h.setProxyPassword(String.valueOf(map.get("proxyPassword")));
|
||||
return h;
|
||||
}
|
||||
|
||||
private static int parseInt(Object obj, int def) {
|
||||
if (obj == null) return def;
|
||||
if (obj instanceof Number) return ((Number) obj).intValue();
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(obj).trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean parseBool(Object obj, boolean def) {
|
||||
if (obj == null) return def;
|
||||
if (obj instanceof Boolean) return (Boolean) obj;
|
||||
return Boolean.parseBoolean(String.valueOf(obj).trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal pure-Java JSON parser for nested Maps, Lists, Strings, Numbers, and Booleans.
|
||||
*/
|
||||
static class SimpleJsonParser {
|
||||
private final String src;
|
||||
private int pos = 0;
|
||||
|
||||
SimpleJsonParser(String src) {
|
||||
this.src = src;
|
||||
}
|
||||
|
||||
Object parseValue() {
|
||||
skipWhitespace();
|
||||
if (pos >= src.length()) return null;
|
||||
char c = src.charAt(pos);
|
||||
if (c == '{') return parseObject();
|
||||
if (c == '[') return parseArray();
|
||||
if (c == '"') return parseString();
|
||||
if (c == 't' || c == 'f') return parseBoolean();
|
||||
if (c == 'n') return parseNull();
|
||||
if (c == '-' || Character.isDigit(c)) return parseNumber();
|
||||
throw new IllegalArgumentException("Unexpected char '" + c + "' at position " + pos);
|
||||
}
|
||||
|
||||
private Map<String, Object> parseObject() {
|
||||
match('{');
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
skipWhitespace();
|
||||
if (peek() == '}') {
|
||||
pos++;
|
||||
return map;
|
||||
}
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
String key = parseString();
|
||||
skipWhitespace();
|
||||
match(':');
|
||||
Object val = parseValue();
|
||||
map.put(key, val);
|
||||
skipWhitespace();
|
||||
char c = peek();
|
||||
if (c == ',') {
|
||||
pos++;
|
||||
} else if (c == '}') {
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<Object> parseArray() {
|
||||
match('[');
|
||||
List<Object> list = new ArrayList<>();
|
||||
skipWhitespace();
|
||||
if (peek() == ']') {
|
||||
pos++;
|
||||
return list;
|
||||
}
|
||||
while (true) {
|
||||
Object val = parseValue();
|
||||
list.add(val);
|
||||
skipWhitespace();
|
||||
char c = peek();
|
||||
if (c == ',') {
|
||||
pos++;
|
||||
} else if (c == ']') {
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String parseString() {
|
||||
match('"');
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (pos < src.length()) {
|
||||
char c = src.charAt(pos++);
|
||||
if (c == '"') {
|
||||
return sb.toString();
|
||||
} else if (c == '\\') {
|
||||
if (pos >= src.length()) break;
|
||||
char esc = src.charAt(pos++);
|
||||
switch (esc) {
|
||||
case '"': sb.append('"'); break;
|
||||
case '\\': sb.append('\\'); break;
|
||||
case '/': sb.append('/'); break;
|
||||
case 'b': sb.append('\b'); break;
|
||||
case 'f': sb.append('\f'); break;
|
||||
case 'n': sb.append('\n'); break;
|
||||
case 'r': sb.append('\r'); break;
|
||||
case 't': sb.append('\t'); break;
|
||||
case 'u':
|
||||
if (pos + 4 <= src.length()) {
|
||||
String hex = src.substring(pos, pos + 4);
|
||||
pos += 4;
|
||||
try {
|
||||
sb.append((char) Integer.parseInt(hex, 16));
|
||||
} catch (NumberFormatException e) {
|
||||
sb.append("?");
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sb.append(esc);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private Boolean parseBoolean() {
|
||||
if (src.startsWith("true", pos)) {
|
||||
pos += 4;
|
||||
return true;
|
||||
} else if (src.startsWith("false", pos)) {
|
||||
pos += 5;
|
||||
return false;
|
||||
}
|
||||
throw new IllegalArgumentException("Expected boolean at " + pos);
|
||||
}
|
||||
|
||||
private Object parseNull() {
|
||||
if (src.startsWith("null", pos)) {
|
||||
pos += 4;
|
||||
return null;
|
||||
}
|
||||
throw new IllegalArgumentException("Expected null at " + pos);
|
||||
}
|
||||
|
||||
private Number parseNumber() {
|
||||
int start = pos;
|
||||
if (src.charAt(pos) == '-') pos++;
|
||||
while (pos < src.length() && (Character.isDigit(src.charAt(pos)) || src.charAt(pos) == '.' || src.charAt(pos) == 'e' || src.charAt(pos) == 'E' || src.charAt(pos) == '+' || src.charAt(pos) == '-')) {
|
||||
pos++;
|
||||
}
|
||||
String numStr = src.substring(start, pos);
|
||||
if (numStr.contains(".")) {
|
||||
return Double.parseDouble(numStr);
|
||||
}
|
||||
return Long.parseLong(numStr);
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
private char peek() {
|
||||
return pos < src.length() ? src.charAt(pos) : '\0';
|
||||
}
|
||||
|
||||
private void match(char expected) {
|
||||
skipWhitespace();
|
||||
if (pos >= src.length() || src.charAt(pos) != expected) {
|
||||
throw new IllegalArgumentException("Expected '" + expected + "' at " + pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Data model for a saved 3270 connection profile, matching the schema of a3270 SavedHost.
|
||||
* Includes per-connection host, model, protocol, and proxy configuration.
|
||||
*/
|
||||
public class SavedHost {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String host;
|
||||
private int port;
|
||||
private int model;
|
||||
private int dynamicRows;
|
||||
private int dynamicCols;
|
||||
private String luName;
|
||||
private boolean autoConnect;
|
||||
private String hostType;
|
||||
private boolean useTls;
|
||||
private boolean tlsVerifyCert;
|
||||
private boolean tn3270e;
|
||||
private String graphicsMode;
|
||||
private String codePage;
|
||||
|
||||
// Per-connection proxy configuration
|
||||
private String proxyType;
|
||||
private String proxyHost;
|
||||
private int proxyPort;
|
||||
private String proxyUsername;
|
||||
private String proxyPassword;
|
||||
|
||||
public SavedHost() {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.name = "Mainframe";
|
||||
this.host = "127.0.0.1";
|
||||
this.port = 23;
|
||||
this.model = 4;
|
||||
this.dynamicRows = 62;
|
||||
this.dynamicCols = 160;
|
||||
this.luName = "";
|
||||
this.autoConnect = false;
|
||||
this.hostType = "TSO";
|
||||
this.useTls = false;
|
||||
this.tlsVerifyCert = true;
|
||||
this.tn3270e = true;
|
||||
this.graphicsMode = "BOTH";
|
||||
this.codePage = "037";
|
||||
this.proxyType = "NONE";
|
||||
this.proxyHost = "";
|
||||
this.proxyPort = 0;
|
||||
this.proxyUsername = "";
|
||||
this.proxyPassword = "";
|
||||
}
|
||||
|
||||
public SavedHost(String id, String name, String host, int port, int model,
|
||||
int dynamicRows, int dynamicCols, String luName, boolean autoConnect,
|
||||
String hostType, boolean useTls, boolean tlsVerifyCert, boolean tn3270e,
|
||||
String graphicsMode, String codePage, String proxyType, String proxyHost,
|
||||
int proxyPort, String proxyUsername, String proxyPassword) {
|
||||
this.id = (id != null && !id.trim().isEmpty()) ? id : UUID.randomUUID().toString();
|
||||
this.name = name != null ? name : "";
|
||||
this.host = host != null ? host : "127.0.0.1";
|
||||
this.port = port > 0 ? port : 23;
|
||||
this.model = model;
|
||||
this.dynamicRows = dynamicRows > 0 ? dynamicRows : 62;
|
||||
this.dynamicCols = dynamicCols > 0 ? dynamicCols : 160;
|
||||
this.luName = luName != null ? luName : "";
|
||||
this.autoConnect = autoConnect;
|
||||
this.hostType = hostType != null ? hostType : "TSO";
|
||||
this.useTls = useTls;
|
||||
this.tlsVerifyCert = tlsVerifyCert;
|
||||
this.tn3270e = tn3270e;
|
||||
this.graphicsMode = graphicsMode != null ? graphicsMode : "BOTH";
|
||||
this.codePage = codePage != null ? codePage : "037";
|
||||
this.proxyType = (proxyType != null && !proxyType.trim().isEmpty()) ? proxyType.toUpperCase() : "NONE";
|
||||
this.proxyHost = proxyHost != null ? proxyHost : "";
|
||||
this.proxyPort = proxyPort;
|
||||
this.proxyUsername = proxyUsername != null ? proxyUsername : "";
|
||||
this.proxyPassword = proxyPassword != null ? proxyPassword : "";
|
||||
}
|
||||
|
||||
public SavedHost copy() {
|
||||
return new SavedHost(id, name, host, port, model, dynamicRows, dynamicCols, luName,
|
||||
autoConnect, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, codePage,
|
||||
proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this saved host profile into an active ConnectionConfig.
|
||||
*/
|
||||
public ConnectionConfig toConnectionConfig() {
|
||||
TerminalModel tm;
|
||||
if (model == 0) {
|
||||
tm = TerminalModel.IBM_DYNAMIC;
|
||||
} else {
|
||||
try {
|
||||
tm = TerminalModel.forModel(model, true);
|
||||
} catch (Exception e) {
|
||||
tm = TerminalModel.IBM_3279_4;
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId(this.id);
|
||||
config.setSavedProfileName(this.name);
|
||||
config.setHostType(this.hostType != null ? this.hostType : "TSO");
|
||||
if (model == 0 || tm.isDynamic()) {
|
||||
config.setDynamicDimensions(dynamicRows, dynamicCols);
|
||||
}
|
||||
if (luName != null && !luName.trim().isEmpty()) {
|
||||
config.setLuName(luName.trim());
|
||||
}
|
||||
config.setTlsVerifyCert(tlsVerifyCert);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
if (graphicsMode != null && !graphicsMode.isEmpty()) {
|
||||
config.setGraphicsMode(GraphicsMode.fromString(graphicsMode));
|
||||
}
|
||||
if (codePage != null && !codePage.isEmpty()) {
|
||||
config.setCodePage(codePage);
|
||||
}
|
||||
|
||||
// Apply per-connection proxy if configured
|
||||
if (proxyType != null && !proxyType.equalsIgnoreCase("NONE") && proxyHost != null && !proxyHost.trim().isEmpty()) {
|
||||
try {
|
||||
ConnectionConfig.ProxyType pt = ConnectionConfig.ProxyType.valueOf(proxyType.toUpperCase());
|
||||
int pPort = proxyPort > 0 ? proxyPort : (pt == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080);
|
||||
config.setProxy(pt, proxyHost.trim(), pPort,
|
||||
(proxyUsername != null && !proxyUsername.trim().isEmpty()) ? proxyUsername.trim() : null,
|
||||
(proxyPassword != null && !proxyPassword.isEmpty()) ? proxyPassword : null);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
|
||||
public int getModel() { return model; }
|
||||
public void setModel(int model) { this.model = model; }
|
||||
|
||||
public int getDynamicRows() { return dynamicRows; }
|
||||
public void setDynamicRows(int dynamicRows) { this.dynamicRows = dynamicRows; }
|
||||
|
||||
public int getDynamicCols() { return dynamicCols; }
|
||||
public void setDynamicCols(int dynamicCols) { this.dynamicCols = dynamicCols; }
|
||||
|
||||
public String getLuName() { return luName; }
|
||||
public void setLuName(String luName) { this.luName = luName; }
|
||||
|
||||
public boolean isAutoConnect() { return autoConnect; }
|
||||
public void setAutoConnect(boolean autoConnect) { this.autoConnect = autoConnect; }
|
||||
|
||||
public String getHostType() { return hostType; }
|
||||
public void setHostType(String hostType) { this.hostType = hostType; }
|
||||
|
||||
public boolean isUseTls() { return useTls; }
|
||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||
|
||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
|
||||
|
||||
public boolean isTn3270e() { return tn3270e; }
|
||||
public void setTn3270e(boolean tn3270e) { this.tn3270e = tn3270e; }
|
||||
|
||||
public String getGraphicsMode() { return graphicsMode; }
|
||||
public void setGraphicsMode(String graphicsMode) { this.graphicsMode = graphicsMode; }
|
||||
|
||||
public String getCodePage() { return codePage; }
|
||||
public void setCodePage(String codePage) { this.codePage = codePage; }
|
||||
|
||||
public String getProxyType() { return proxyType != null ? proxyType : "NONE"; }
|
||||
public void setProxyType(String proxyType) { this.proxyType = (proxyType != null) ? proxyType.toUpperCase() : "NONE"; }
|
||||
|
||||
public String getProxyHost() { return proxyHost != null ? proxyHost : ""; }
|
||||
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
|
||||
|
||||
public int getProxyPort() { return proxyPort; }
|
||||
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
|
||||
|
||||
public String getProxyUsername() { return proxyUsername != null ? proxyUsername : ""; }
|
||||
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
|
||||
|
||||
public String getProxyPassword() { return proxyPassword != null ? proxyPassword : ""; }
|
||||
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (name != null && !name.trim().isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
return host + (port > 0 ? ":" + port : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SavedHost savedHost = (SavedHost) o;
|
||||
return Objects.equals(id, savedHost.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Utility for loading and applying application window and taskbar icons.
|
||||
* Loads multi-resolution lib3270j icons from resources.
|
||||
*/
|
||||
public final class AppIcon {
|
||||
|
||||
private static final Logger log = Logger.getLogger(AppIcon.class.getName());
|
||||
|
||||
private static final int[] ICON_SIZES = {16, 24, 32, 48, 64, 128, 256, 512};
|
||||
private static List<Image> cachedIcons = null;
|
||||
private static Image defaultIcon = null;
|
||||
|
||||
private AppIcon() {}
|
||||
|
||||
/**
|
||||
* Retrieves the list of available application icon images in various resolutions.
|
||||
*
|
||||
* @return an unmodifiable list of Image objects, or empty list if none could be loaded.
|
||||
*/
|
||||
public static synchronized List<Image> getAppIcons() {
|
||||
if (cachedIcons == null) {
|
||||
loadIcons();
|
||||
}
|
||||
return cachedIcons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default application icon image (256x256 or best available).
|
||||
*
|
||||
* @return the default Image, or null if no icon is available.
|
||||
*/
|
||||
public static synchronized Image getDefaultIcon() {
|
||||
if (cachedIcons == null) {
|
||||
loadIcons();
|
||||
}
|
||||
return defaultIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the application icons to a target Window (JFrame, JDialog, etc.)
|
||||
* and updates the desktop Taskbar icon if supported.
|
||||
*
|
||||
* @param window the Window to set icon images for.
|
||||
*/
|
||||
public static void applyTo(Window window) {
|
||||
if (GraphicsEnvironment.isHeadless()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Image> icons = getAppIcons();
|
||||
if (icons.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window != null) {
|
||||
window.setIconImages(icons);
|
||||
}
|
||||
|
||||
// Apply to OS Taskbar / Dock if supported (Java 9+)
|
||||
try {
|
||||
if (Taskbar.isTaskbarSupported()) {
|
||||
Taskbar taskbar = Taskbar.getTaskbar();
|
||||
if (taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) {
|
||||
Image bestIcon = defaultIcon != null ? defaultIcon : icons.get(icons.size() - 1);
|
||||
taskbar.setIconImage(bestIcon);
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
log.log(Level.FINE, "Taskbar icon could not be set: {0}", t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadIcons() {
|
||||
List<Image> icons = new ArrayList<>();
|
||||
|
||||
for (int size : ICON_SIZES) {
|
||||
String path = "/icons/lib3270j_" + size + ".png";
|
||||
Image img = loadImage(path);
|
||||
if (img != null) {
|
||||
icons.add(img);
|
||||
}
|
||||
}
|
||||
|
||||
if (icons.isEmpty()) {
|
||||
Image img = loadImage("/icons/lib3270j.png");
|
||||
if (img != null) {
|
||||
icons.add(img);
|
||||
}
|
||||
}
|
||||
|
||||
if (!icons.isEmpty()) {
|
||||
for (Image img : icons) {
|
||||
if (img.getWidth(null) == 256) {
|
||||
defaultIcon = img;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultIcon == null) {
|
||||
defaultIcon = icons.get(icons.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
cachedIcons = Collections.unmodifiableList(icons);
|
||||
}
|
||||
|
||||
private static Image loadImage(String resourcePath) {
|
||||
try {
|
||||
URL url = AppIcon.class.getResource(resourcePath);
|
||||
if (url != null) {
|
||||
BufferedImage img = ImageIO.read(url);
|
||||
if (img != null) {
|
||||
return img;
|
||||
}
|
||||
}
|
||||
try (InputStream in = AppIcon.class.getResourceAsStream(resourcePath)) {
|
||||
if (in != null) {
|
||||
return ImageIO.read(in);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Failed to load icon from " + resourcePath, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBuffer;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.awt.image.DirectColorModel;
|
||||
import java.awt.image.Raster;
|
||||
import java.awt.image.SinglePixelPackedSampleModel;
|
||||
import java.awt.image.WritableRaster;
|
||||
|
||||
/**
|
||||
* Desktop Swing/AWT bridge for converting between lib3270j PixelBuffer and java.awt BufferedImage.
|
||||
*/
|
||||
public final class AwtPixelBufferBridge {
|
||||
|
||||
private AwtPixelBufferBridge() {}
|
||||
|
||||
/**
|
||||
* Converts a PixelBuffer to a BufferedImage.
|
||||
* Uses TYPE_INT_ARGB with backed raster for optimal Java2D hardware acceleration.
|
||||
*/
|
||||
public static BufferedImage toBufferedImage(PixelBuffer buffer) {
|
||||
if (buffer == null) return null;
|
||||
int w = buffer.getWidth();
|
||||
int h = buffer.getHeight();
|
||||
if (w <= 0 || h <= 0) {
|
||||
return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
|
||||
}
|
||||
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
copyIntoBufferedImage(buffer, img);
|
||||
return img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies pixels from a PixelBuffer directly into an existing BufferedImage.
|
||||
*/
|
||||
public static void copyIntoBufferedImage(PixelBuffer buffer, BufferedImage target) {
|
||||
if (buffer == null || target == null) return;
|
||||
int w = Math.min(buffer.getWidth(), target.getWidth());
|
||||
int h = Math.min(buffer.getHeight(), target.getHeight());
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
int[] srcPixels = buffer.getPixels();
|
||||
if (srcPixels == null) return;
|
||||
|
||||
if (target.getType() == BufferedImage.TYPE_INT_ARGB || target.getType() == BufferedImage.TYPE_INT_RGB) {
|
||||
if (target.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
||||
int[] dstPixels = ((DataBufferInt) target.getRaster().getDataBuffer()).getData();
|
||||
int srcW = buffer.getWidth();
|
||||
int dstW = target.getWidth();
|
||||
if (srcW == dstW && srcW == w && srcPixels.length >= w * h && dstPixels.length >= w * h) {
|
||||
System.arraycopy(srcPixels, 0, dstPixels, 0, w * h);
|
||||
return;
|
||||
}
|
||||
for (int y = 0; y < h; y++) {
|
||||
System.arraycopy(srcPixels, y * srcW, dstPixels, y * dstW, w);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for custom or incompatible image formats
|
||||
target.setRGB(0, 0, w, h, srcPixels, 0, buffer.getWidth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps or converts a java.awt BufferedImage into a PixelBuffer.
|
||||
*/
|
||||
public static PixelBuffer toPixelBuffer(BufferedImage img) {
|
||||
if (img == null) return null;
|
||||
int w = img.getWidth();
|
||||
int h = img.getHeight();
|
||||
int[] pixels;
|
||||
if ((img.getType() == BufferedImage.TYPE_INT_ARGB || img.getType() == BufferedImage.TYPE_INT_RGB)
|
||||
&& img.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
||||
pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||
} else {
|
||||
pixels = new int[w * h];
|
||||
img.getRGB(0, 0, w, h, pixels, 0, w);
|
||||
}
|
||||
return new DefaultPixelBuffer(w, h, pixels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
/**
|
||||
* Dialogue shown when a connection is initiated while j3270 is already connected.
|
||||
* Asks the user whether the connection should be opened in a new window or
|
||||
* if the current connection should be aborted.
|
||||
*/
|
||||
public class ConnectChoiceDialog extends JDialog {
|
||||
|
||||
public enum Choice {
|
||||
NEW_WINDOW,
|
||||
ABORT_CURRENT,
|
||||
CANCEL
|
||||
}
|
||||
|
||||
private Choice result = Choice.CANCEL;
|
||||
private final JButton newWindowBtn;
|
||||
private final JButton abortBtn;
|
||||
private final JButton cancelBtn;
|
||||
private final JLabel titleLabel;
|
||||
private final JLabel line1;
|
||||
private final JLabel line2;
|
||||
|
||||
public ConnectChoiceDialog(Window owner, String currentTarget) {
|
||||
super(owner, "Active Connection", ModalityType.APPLICATION_MODAL);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
setResizable(false);
|
||||
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(16, 16));
|
||||
mainPanel.setBorder(new EmptyBorder(20, 24, 20, 24));
|
||||
|
||||
// Message Panel
|
||||
JPanel messagePanel = new JPanel();
|
||||
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
|
||||
messagePanel.setOpaque(false);
|
||||
|
||||
titleLabel = new JLabel("Connection Already Active");
|
||||
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 2));
|
||||
messagePanel.add(titleLabel);
|
||||
messagePanel.add(Box.createVerticalStrut(10));
|
||||
|
||||
String targetText = (currentTarget != null && !currentTarget.trim().isEmpty())
|
||||
? currentTarget.trim()
|
||||
: "the host";
|
||||
line1 = new JLabel("j3270 is currently connected to " + targetText + ".");
|
||||
line1.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
messagePanel.add(line1);
|
||||
|
||||
messagePanel.add(Box.createVerticalStrut(6));
|
||||
line2 = new JLabel("Would you like to open the connection in a new window or abort the current connection?");
|
||||
line2.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
messagePanel.add(line2);
|
||||
|
||||
mainPanel.add(messagePanel, BorderLayout.CENTER);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
cancelBtn = new JButton("Cancel");
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
cancelBtn.addActionListener(e -> {
|
||||
result = Choice.CANCEL;
|
||||
dispose();
|
||||
});
|
||||
|
||||
abortBtn = new JButton("Abort Current Connection");
|
||||
ThemeManager.styleButton(abortBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
abortBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
abortBtn.addActionListener(e -> {
|
||||
result = Choice.ABORT_CURRENT;
|
||||
dispose();
|
||||
});
|
||||
|
||||
newWindowBtn = new JButton("New Window");
|
||||
ThemeManager.styleButton(newWindowBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
newWindowBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
newWindowBtn.addActionListener(e -> {
|
||||
result = Choice.NEW_WINDOW;
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(abortBtn);
|
||||
buttonPanel.add(newWindowBtn);
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// ESC key cancels
|
||||
getRootPane().registerKeyboardAction(e -> {
|
||||
result = Choice.CANCEL;
|
||||
dispose();
|
||||
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
|
||||
getRootPane().setDefaultButton(newWindowBtn);
|
||||
|
||||
pack();
|
||||
if (owner != null) {
|
||||
setLocationRelativeTo(owner);
|
||||
} else {
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
}
|
||||
|
||||
public Choice getChoice() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public JButton getNewWindowButton() {
|
||||
return newWindowBtn;
|
||||
}
|
||||
|
||||
public JButton getAbortButton() {
|
||||
return abortBtn;
|
||||
}
|
||||
|
||||
public JButton getCancelButton() {
|
||||
return cancelBtn;
|
||||
}
|
||||
|
||||
public JLabel getTitleLabel() {
|
||||
return titleLabel;
|
||||
}
|
||||
|
||||
public JLabel getLine1() {
|
||||
return line1;
|
||||
}
|
||||
|
||||
public JLabel getLine2() {
|
||||
return line2;
|
||||
}
|
||||
|
||||
public static Choice showPrompt(Window owner, String currentTarget) {
|
||||
ConnectChoiceDialog dialog = new ConnectChoiceDialog(owner, currentTarget);
|
||||
dialog.setVisible(true);
|
||||
return dialog.getChoice();
|
||||
}
|
||||
}
|
||||
@@ -28,16 +28,11 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
private JTable table;
|
||||
private JLabel countLabel;
|
||||
|
||||
private static final Color DARK_BG = new Color(35, 35, 35);
|
||||
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
|
||||
private static final Color DARK_FG = new Color(220, 220, 220);
|
||||
private static final Color DARK_BORDER = new Color(65, 65, 65);
|
||||
private static final Color DARK_SELECTION = new Color(75, 110, 175);
|
||||
|
||||
public FieldInspectorDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "3270 Presentation Space Field Inspector", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI();
|
||||
setSize(850, 420);
|
||||
@@ -61,19 +56,16 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(8, 8));
|
||||
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
mainPanel.setBackground(DARK_BG);
|
||||
|
||||
// Top info bar
|
||||
JPanel topPanel = new JPanel(new BorderLayout());
|
||||
topPanel.setOpaque(false);
|
||||
countLabel = new JLabel("0 fields detected on screen");
|
||||
countLabel.setForeground(DARK_FG);
|
||||
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||
countLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
topPanel.add(countLabel, BorderLayout.WEST);
|
||||
|
||||
JButton refreshBtn = new JButton("Refresh");
|
||||
refreshBtn.setBackground(new Color(55, 55, 55));
|
||||
refreshBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(refreshBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
refreshBtn.addActionListener(e -> refreshFields());
|
||||
topPanel.add(refreshBtn, BorderLayout.EAST);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
@@ -91,19 +83,13 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
};
|
||||
|
||||
table = new JTable(tableModel);
|
||||
table.setBackground(DARK_FIELD_BG);
|
||||
table.setForeground(DARK_FG);
|
||||
table.setSelectionBackground(DARK_SELECTION);
|
||||
table.setSelectionForeground(Color.WHITE);
|
||||
table.setGridColor(DARK_BORDER);
|
||||
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
ThemeManager.styleTable(table);
|
||||
table.setFont(ThemeManager.getMonospacedUiFont());
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(20);
|
||||
table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setBackground(new Color(50, 50, 50));
|
||||
header.setForeground(DARK_FG);
|
||||
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
|
||||
header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
|
||||
// Column widths
|
||||
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
|
||||
@@ -125,8 +111,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
scrollPane.setBorder(new LineBorder(DARK_BORDER));
|
||||
scrollPane.getViewport().setBackground(DARK_FIELD_BG);
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom panel
|
||||
@@ -134,8 +119,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
JButton jumpBtn = new JButton("Jump to Selected Field");
|
||||
jumpBtn.setBackground(new Color(50, 100, 160));
|
||||
jumpBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(jumpBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
jumpBtn.addActionListener(e -> {
|
||||
int row = table.getSelectedRow();
|
||||
if (row >= 0) {
|
||||
@@ -144,8 +128,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
});
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
closeBtn.setBackground(new Color(55, 55, 55));
|
||||
closeBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
bottomPanel.add(jumpBtn);
|
||||
@@ -153,6 +136,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
public synchronized void refreshFields() {
|
||||
|
||||
@@ -32,6 +32,7 @@ public class FindDialog extends JDialog {
|
||||
super(parent, "Find on Screen", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI();
|
||||
pack();
|
||||
@@ -42,7 +43,6 @@ public class FindDialog extends JDialog {
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
mainPanel.setBackground(new Color(35, 35, 35));
|
||||
|
||||
// Form
|
||||
JPanel formPanel = new JPanel(new GridBagLayout());
|
||||
@@ -51,14 +51,12 @@ public class FindDialog extends JDialog {
|
||||
gbc.insets = new Insets(4, 4, 4, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Color fg = new Color(220, 220, 220);
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Search text
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
JLabel findLabel = new JLabel("Find what:");
|
||||
findLabel.setForeground(fg);
|
||||
findLabel.setFont(labelFont);
|
||||
formPanel.add(findLabel, gbc);
|
||||
|
||||
@@ -66,23 +64,16 @@ public class FindDialog extends JDialog {
|
||||
gbc.weightx = 1.0;
|
||||
searchField = new JTextField(20);
|
||||
searchField.setText(lastSearchText);
|
||||
searchField.setBackground(new Color(50, 50, 50));
|
||||
searchField.setForeground(fg);
|
||||
searchField.setCaretColor(fg);
|
||||
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
searchField.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(new Color(70, 70, 70)),
|
||||
BorderFactory.createEmptyBorder(3, 6, 3, 6)));
|
||||
searchField.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextField(searchField);
|
||||
formPanel.add(searchField, gbc);
|
||||
|
||||
// Options
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 1;
|
||||
matchCaseCheck = new JCheckBox("Match case", lastMatchCase);
|
||||
matchCaseCheck.setForeground(fg);
|
||||
matchCaseCheck.setOpaque(false);
|
||||
ThemeManager.styleCheckBox(matchCaseCheck);
|
||||
matchCaseCheck.setFont(labelFont);
|
||||
matchCaseCheck.setFocusPainted(false);
|
||||
formPanel.add(matchCaseCheck, gbc);
|
||||
|
||||
// Direction
|
||||
@@ -91,16 +82,11 @@ public class FindDialog extends JDialog {
|
||||
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||
dirPanel.setOpaque(false);
|
||||
JLabel dirLabel = new JLabel("Direction: ");
|
||||
dirLabel.setForeground(fg);
|
||||
dirLabel.setFont(labelFont);
|
||||
forwardRadio = new JRadioButton("Down", lastForward);
|
||||
backwardRadio = new JRadioButton("Up", !lastForward);
|
||||
forwardRadio.setForeground(fg);
|
||||
backwardRadio.setForeground(fg);
|
||||
forwardRadio.setOpaque(false);
|
||||
backwardRadio.setOpaque(false);
|
||||
forwardRadio.setFocusPainted(false);
|
||||
backwardRadio.setFocusPainted(false);
|
||||
ThemeManager.styleRadioButton(forwardRadio);
|
||||
ThemeManager.styleRadioButton(backwardRadio);
|
||||
|
||||
ButtonGroup bg = new ButtonGroup();
|
||||
bg.add(forwardRadio);
|
||||
@@ -116,8 +102,8 @@ public class FindDialog extends JDialog {
|
||||
gbc.gridy = 3;
|
||||
gbc.gridwidth = 2;
|
||||
statusLabel = new JLabel(" ");
|
||||
statusLabel.setForeground(new Color(255, 120, 120));
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
statusLabel.setForeground(ThemeManager.getOiaFgAlert());
|
||||
statusLabel.setFont(labelFont);
|
||||
formPanel.add(statusLabel, gbc);
|
||||
|
||||
mainPanel.add(formPanel, BorderLayout.CENTER);
|
||||
@@ -127,14 +113,12 @@ public class FindDialog extends JDialog {
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton findNextBtn = new JButton("Find Next");
|
||||
findNextBtn.setBackground(new Color(60, 63, 65));
|
||||
findNextBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(findNextBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
findNextBtn.setFont(labelFont);
|
||||
findNextBtn.addActionListener(e -> findNext());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
closeBtn.setBackground(new Color(60, 63, 65));
|
||||
closeBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
@@ -144,6 +128,7 @@ public class FindDialog extends JDialog {
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
getRootPane().setDefaultButton(findNextBtn);
|
||||
|
||||
searchField.addKeyListener(new KeyAdapter() {
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Real-time hotspot and URL auto-detection engine matching IBM Host On-Demand
|
||||
* ShowURLOptions and ScreenText parity.
|
||||
*
|
||||
* Scans screen text using regular expressions for:
|
||||
* 1. URLs (http://, https://, ftp://, file://, mailto:, www.)
|
||||
* 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
|
||||
* 3. Menu choices ("1.", "2.", "===> 3", "=> 4")
|
||||
*
|
||||
* Provides hit-testing for mouse hover/clicks and generates keystroke actions.
|
||||
*/
|
||||
public class HotspotEngine {
|
||||
|
||||
public enum HotspotType {
|
||||
URL,
|
||||
PF_KEY,
|
||||
ENTER,
|
||||
MENU_CHOICE
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an active interactive hotspot region on the terminal screen.
|
||||
*/
|
||||
public static class Hotspot {
|
||||
private final HotspotType type;
|
||||
private final int startRow;
|
||||
private final int startCol;
|
||||
private final int endRow;
|
||||
private final int endCol;
|
||||
private final String displayText;
|
||||
private final String action;
|
||||
private final String tooltip;
|
||||
|
||||
public Hotspot(HotspotType type, int startRow, int startCol, int endRow, int endCol,
|
||||
String displayText, String action, String tooltip) {
|
||||
this.type = type;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.endRow = endRow;
|
||||
this.endCol = endCol;
|
||||
this.displayText = displayText;
|
||||
this.action = action;
|
||||
this.tooltip = tooltip;
|
||||
}
|
||||
|
||||
public HotspotType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getStartRow() {
|
||||
return startRow;
|
||||
}
|
||||
|
||||
public int getStartCol() {
|
||||
return startCol;
|
||||
}
|
||||
|
||||
public int getEndRow() {
|
||||
return endRow;
|
||||
}
|
||||
|
||||
public int getEndCol() {
|
||||
return endCol;
|
||||
}
|
||||
|
||||
public String getDisplayText() {
|
||||
return displayText;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public String getTooltip() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the given row/col coordinate falls within this hotspot.
|
||||
*/
|
||||
public boolean contains(int row, int col) {
|
||||
if (row < startRow || row > endRow) {
|
||||
return false;
|
||||
}
|
||||
if (startRow == endRow) {
|
||||
return col >= startCol && col <= endCol;
|
||||
}
|
||||
if (row == startRow) {
|
||||
return col >= startCol;
|
||||
}
|
||||
if (row == endRow) {
|
||||
return col <= endCol;
|
||||
}
|
||||
return true; // Intermediate rows
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Hotspot{" + type + " [" + startRow + "," + startCol + "]-[" + endRow + "," + endCol + "] '" + displayText + "'}";
|
||||
}
|
||||
}
|
||||
|
||||
// Configurable scan enablement
|
||||
private boolean urlEnabled = true;
|
||||
private boolean pfKeyEnabled = true;
|
||||
private boolean menuChoiceEnabled = true;
|
||||
|
||||
// Regular expression patterns
|
||||
// 1. URLs
|
||||
private static final Pattern URL_PATTERN = Pattern.compile(
|
||||
"(?:https?|ftp|file)://[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|www\\.[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|mailto:[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
// 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
|
||||
private static final Pattern PF_KEY_PATTERN = Pattern.compile(
|
||||
"(?:(?<=\\W)|^)(PF(?:[1-9]|1[0-9]|2[0-4])|F(?:[1-9]|1[0-9]|2[0-4])|PA[1-3]|\\[ENTER\\]|ENTER)(?:(?=\\W)|$)",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5"
|
||||
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
|
||||
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
|
||||
);
|
||||
|
||||
private static final Pattern MENU_PROMPT_PATTERN = Pattern.compile(
|
||||
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
|
||||
);
|
||||
|
||||
// Caching state to avoid rescanning unchanged screens
|
||||
private String cachedScreenText = "";
|
||||
private int cachedRows = 0;
|
||||
private int cachedCols = 0;
|
||||
private List<Hotspot> cachedHotspots = Collections.emptyList();
|
||||
|
||||
public HotspotEngine() {
|
||||
}
|
||||
|
||||
public boolean isUrlEnabled() {
|
||||
return urlEnabled;
|
||||
}
|
||||
|
||||
public void setUrlEnabled(boolean urlEnabled) {
|
||||
this.urlEnabled = urlEnabled;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public boolean isPfKeyEnabled() {
|
||||
return pfKeyEnabled;
|
||||
}
|
||||
|
||||
public void setPfKeyEnabled(boolean pfKeyEnabled) {
|
||||
this.pfKeyEnabled = pfKeyEnabled;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public boolean isMenuChoiceEnabled() {
|
||||
return menuChoiceEnabled;
|
||||
}
|
||||
|
||||
public void setMenuChoiceEnabled(boolean menuChoiceEnabled) {
|
||||
this.menuChoiceEnabled = menuChoiceEnabled;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public void invalidateCache() {
|
||||
cachedScreenText = "";
|
||||
cachedHotspots = Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the given ScreenBuffer for interactive hotspots.
|
||||
*/
|
||||
public synchronized List<Hotspot> scan(ScreenBuffer sb) {
|
||||
if (sb == null) return Collections.emptyList();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
|
||||
// Build plain text representation
|
||||
StringBuilder fullText = new StringBuilder(rows * (cols + 1));
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
ExtendedAttribute ea = sb.getDisplayCell(baddr);
|
||||
if (ea == null || ea.isFieldAttribute() || ea.ucs4 <= 0x20 || ea.ucs4 == 0xFF) {
|
||||
fullText.append(' ');
|
||||
} else {
|
||||
fullText.append((char) ea.ucs4);
|
||||
}
|
||||
}
|
||||
if (r < rows - 1) {
|
||||
fullText.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
return scanText(fullText.toString(), rows, cols);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan formatted screen text (lines separated by '\n') for hotspots.
|
||||
*/
|
||||
public synchronized List<Hotspot> scanText(String screenText, int rows, int cols) {
|
||||
if (screenText == null || rows <= 0 || cols <= 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (screenText.equals(cachedScreenText) && rows == cachedRows && cols == cachedCols) {
|
||||
return cachedHotspots;
|
||||
}
|
||||
|
||||
List<Hotspot> hotspots = new ArrayList<>();
|
||||
String[] lines = screenText.split("\n", -1);
|
||||
int actualRows = Math.min(rows, lines.length);
|
||||
|
||||
// Scan line by line for precise row/column tracking
|
||||
for (int r = 0; r < actualRows; r++) {
|
||||
String line = lines[r];
|
||||
boolean[] occupied = new boolean[line.length() + 1];
|
||||
|
||||
// 1. Scan URLs
|
||||
if (urlEnabled) {
|
||||
Matcher mUrl = URL_PATTERN.matcher(line);
|
||||
while (mUrl.find()) {
|
||||
int startCol = mUrl.start();
|
||||
int endCol = mUrl.end();
|
||||
String match = mUrl.group();
|
||||
|
||||
// Trim trailing punctuation like . , ; : ) ]
|
||||
while (match.length() > 1) {
|
||||
char last = match.charAt(match.length() - 1);
|
||||
if (last == '.' || last == ',' || last == ';' || last == ':' || last == ')' || last == ']' || last == '>') {
|
||||
match = match.substring(0, match.length() - 1);
|
||||
endCol--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match.isEmpty()) continue;
|
||||
|
||||
String actionUrl = match;
|
||||
if (match.toLowerCase().startsWith("www.")) {
|
||||
actionUrl = "http://" + match;
|
||||
}
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
HotspotType.URL,
|
||||
r, startCol, r, endCol - 1,
|
||||
match, actionUrl, "Open URL: " + actionUrl
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Scan PF / Function keys
|
||||
if (pfKeyEnabled) {
|
||||
Matcher mPf = PF_KEY_PATTERN.matcher(line);
|
||||
while (mPf.find()) {
|
||||
int startCol = mPf.start(1);
|
||||
int endCol = mPf.end(1);
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue;
|
||||
|
||||
String keyText = mPf.group(1).toUpperCase();
|
||||
String action;
|
||||
String tooltip;
|
||||
|
||||
if (keyText.equals("ENTER") || keyText.equals("[ENTER]")) {
|
||||
action = "[enter]";
|
||||
tooltip = "Send [ENTER]";
|
||||
} else if (keyText.startsWith("PF")) {
|
||||
String numStr = keyText.substring(2);
|
||||
action = "[pf" + numStr + "]";
|
||||
tooltip = "Send " + action.toUpperCase();
|
||||
} else if (keyText.startsWith("F")) {
|
||||
String numStr = keyText.substring(1);
|
||||
action = "[pf" + numStr + "]";
|
||||
tooltip = "Send " + action.toUpperCase();
|
||||
} else if (keyText.startsWith("PA")) {
|
||||
String numStr = keyText.substring(2);
|
||||
action = "[pa" + numStr + "]";
|
||||
tooltip = "Send " + action.toUpperCase();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
keyText.contains("ENTER") ? HotspotType.ENTER : HotspotType.PF_KEY,
|
||||
r, startCol, r, endCol - 1,
|
||||
mPf.group(1), action, tooltip
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Scan Menu choices ("1.", "2.", "===> 3", "=> 4")
|
||||
if (menuChoiceEnabled) {
|
||||
// Pattern 3a: Number followed by dot (e.g. " 1. ")
|
||||
Matcher mNum = MENU_NUMBER_PATTERN.matcher(line);
|
||||
while (mNum.find()) {
|
||||
int startCol = mNum.start(1);
|
||||
int endCol = mNum.end();
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue;
|
||||
|
||||
String choiceNum = mNum.group(1);
|
||||
String action = choiceNum + "[enter]";
|
||||
String tooltip = "Select option " + choiceNum;
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
HotspotType.MENU_CHOICE,
|
||||
r, startCol, r, endCol - 1,
|
||||
choiceNum + ".", action, tooltip
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
|
||||
// Pattern 3b: Prompt followed by number (e.g. "===> 3")
|
||||
Matcher mPrompt = MENU_PROMPT_PATTERN.matcher(line);
|
||||
while (mPrompt.find()) {
|
||||
int startCol = mPrompt.start();
|
||||
int endCol = mPrompt.end();
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue;
|
||||
|
||||
String choiceNum = mPrompt.group(1);
|
||||
String action = choiceNum + "[enter]";
|
||||
String tooltip = "Execute choice " + choiceNum;
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
HotspotType.MENU_CHOICE,
|
||||
r, startCol, r, endCol - 1,
|
||||
mPrompt.group(), action, tooltip
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedScreenText = screenText;
|
||||
this.cachedRows = rows;
|
||||
this.cachedCols = cols;
|
||||
this.cachedHotspots = Collections.unmodifiableList(hotspots);
|
||||
|
||||
return cachedHotspots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit-test for a hotspot at the specified terminal row and column.
|
||||
*/
|
||||
public Hotspot findHotspot(int row, int col) {
|
||||
for (Hotspot h : cachedHotspots) {
|
||||
if (h.contains(row, col)) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query currently active cached hotspots.
|
||||
*/
|
||||
public List<Hotspot> getHotspots() {
|
||||
return cachedHotspots;
|
||||
}
|
||||
|
||||
private static void markOccupied(boolean[] arr, int start, int end) {
|
||||
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
|
||||
arr[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRangeOccupied(boolean[] arr, int start, int end) {
|
||||
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
|
||||
if (arr[i]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Keyboard mapping profile (.kmp) file importer and keybinding utility.
|
||||
* Parses keyboard mapping profile files, translates key scan codes and mnemonics
|
||||
* to j3270 action handlers.
|
||||
*/
|
||||
public class KeyBindings {
|
||||
|
||||
private static final Logger log = Logger.getLogger(KeyBindings.class.getName());
|
||||
|
||||
// IBM scan code to standard Java KeyStroke key name mapping
|
||||
private static final Map<Integer, String> SCAN_CODE_MAP = new HashMap<>();
|
||||
|
||||
// Mnemonic to j3270 action mapping
|
||||
private static final Map<String, String> MNEMONIC_MAP = new HashMap<>();
|
||||
|
||||
static {
|
||||
// Alphanumeric and symbol keys
|
||||
SCAN_CODE_MAP.put(1, "BACK_QUOTE");
|
||||
SCAN_CODE_MAP.put(2, "1");
|
||||
SCAN_CODE_MAP.put(3, "2");
|
||||
SCAN_CODE_MAP.put(4, "3");
|
||||
SCAN_CODE_MAP.put(5, "4");
|
||||
SCAN_CODE_MAP.put(6, "5");
|
||||
SCAN_CODE_MAP.put(7, "6");
|
||||
SCAN_CODE_MAP.put(8, "7");
|
||||
SCAN_CODE_MAP.put(9, "8");
|
||||
SCAN_CODE_MAP.put(10, "9");
|
||||
SCAN_CODE_MAP.put(11, "0");
|
||||
SCAN_CODE_MAP.put(12, "MINUS");
|
||||
SCAN_CODE_MAP.put(13, "EQUALS");
|
||||
SCAN_CODE_MAP.put(14, "BACK_SPACE");
|
||||
SCAN_CODE_MAP.put(15, "TAB");
|
||||
SCAN_CODE_MAP.put(16, "Q");
|
||||
SCAN_CODE_MAP.put(17, "W");
|
||||
SCAN_CODE_MAP.put(18, "E");
|
||||
SCAN_CODE_MAP.put(19, "R");
|
||||
SCAN_CODE_MAP.put(20, "T");
|
||||
SCAN_CODE_MAP.put(21, "Y");
|
||||
SCAN_CODE_MAP.put(22, "U");
|
||||
SCAN_CODE_MAP.put(23, "I");
|
||||
SCAN_CODE_MAP.put(24, "O");
|
||||
SCAN_CODE_MAP.put(25, "P");
|
||||
SCAN_CODE_MAP.put(26, "OPEN_BRACKET");
|
||||
SCAN_CODE_MAP.put(27, "CLOSE_BRACKET");
|
||||
SCAN_CODE_MAP.put(28, "BACK_SLASH");
|
||||
SCAN_CODE_MAP.put(29, "CAPS_LOCK");
|
||||
SCAN_CODE_MAP.put(30, "A");
|
||||
SCAN_CODE_MAP.put(31, "S");
|
||||
SCAN_CODE_MAP.put(32, "D");
|
||||
SCAN_CODE_MAP.put(33, "F");
|
||||
SCAN_CODE_MAP.put(34, "G");
|
||||
SCAN_CODE_MAP.put(35, "H");
|
||||
SCAN_CODE_MAP.put(36, "J");
|
||||
SCAN_CODE_MAP.put(37, "K");
|
||||
SCAN_CODE_MAP.put(38, "L");
|
||||
SCAN_CODE_MAP.put(39, "SEMICOLON");
|
||||
SCAN_CODE_MAP.put(40, "QUOTE");
|
||||
SCAN_CODE_MAP.put(41, "BACK_QUOTE");
|
||||
SCAN_CODE_MAP.put(42, "ENTER");
|
||||
SCAN_CODE_MAP.put(43, "ENTER");
|
||||
SCAN_CODE_MAP.put(45, "Z");
|
||||
SCAN_CODE_MAP.put(46, "X");
|
||||
SCAN_CODE_MAP.put(47, "C");
|
||||
SCAN_CODE_MAP.put(48, "V");
|
||||
SCAN_CODE_MAP.put(49, "B");
|
||||
SCAN_CODE_MAP.put(50, "N");
|
||||
SCAN_CODE_MAP.put(51, "M");
|
||||
SCAN_CODE_MAP.put(52, "COMMA");
|
||||
SCAN_CODE_MAP.put(53, "PERIOD");
|
||||
SCAN_CODE_MAP.put(54, "SLASH");
|
||||
SCAN_CODE_MAP.put(57, "SPACE");
|
||||
|
||||
// Editing & Navigation keys
|
||||
SCAN_CODE_MAP.put(75, "INSERT");
|
||||
SCAN_CODE_MAP.put(76, "DELETE");
|
||||
SCAN_CODE_MAP.put(79, "LEFT");
|
||||
SCAN_CODE_MAP.put(80, "HOME");
|
||||
SCAN_CODE_MAP.put(81, "END");
|
||||
SCAN_CODE_MAP.put(83, "UP");
|
||||
SCAN_CODE_MAP.put(84, "DOWN");
|
||||
SCAN_CODE_MAP.put(85, "PAGE_UP");
|
||||
SCAN_CODE_MAP.put(86, "PAGE_DOWN");
|
||||
SCAN_CODE_MAP.put(89, "RIGHT");
|
||||
|
||||
// Numeric Keypad
|
||||
SCAN_CODE_MAP.put(90, "NUM_LOCK");
|
||||
SCAN_CODE_MAP.put(91, "NUMPAD7");
|
||||
SCAN_CODE_MAP.put(92, "NUMPAD4");
|
||||
SCAN_CODE_MAP.put(93, "NUMPAD1");
|
||||
SCAN_CODE_MAP.put(95, "DIVIDE");
|
||||
SCAN_CODE_MAP.put(96, "NUMPAD8");
|
||||
SCAN_CODE_MAP.put(97, "NUMPAD5");
|
||||
SCAN_CODE_MAP.put(98, "NUMPAD2");
|
||||
SCAN_CODE_MAP.put(99, "NUMPAD0");
|
||||
SCAN_CODE_MAP.put(100, "MULTIPLY");
|
||||
SCAN_CODE_MAP.put(101, "NUMPAD9");
|
||||
SCAN_CODE_MAP.put(102, "NUMPAD6");
|
||||
SCAN_CODE_MAP.put(103, "NUMPAD3");
|
||||
SCAN_CODE_MAP.put(104, "DECIMAL");
|
||||
SCAN_CODE_MAP.put(105, "SUBTRACT");
|
||||
SCAN_CODE_MAP.put(106, "ADD");
|
||||
SCAN_CODE_MAP.put(108, "ENTER");
|
||||
|
||||
// Function keys & Escape
|
||||
SCAN_CODE_MAP.put(110, "ESCAPE");
|
||||
SCAN_CODE_MAP.put(112, "F1");
|
||||
SCAN_CODE_MAP.put(113, "F2");
|
||||
SCAN_CODE_MAP.put(114, "F3");
|
||||
SCAN_CODE_MAP.put(115, "F4");
|
||||
SCAN_CODE_MAP.put(116, "F5");
|
||||
SCAN_CODE_MAP.put(117, "F6");
|
||||
SCAN_CODE_MAP.put(118, "F7");
|
||||
SCAN_CODE_MAP.put(119, "F8");
|
||||
SCAN_CODE_MAP.put(120, "F9");
|
||||
SCAN_CODE_MAP.put(121, "F10");
|
||||
SCAN_CODE_MAP.put(122, "F11");
|
||||
SCAN_CODE_MAP.put(123, "F12");
|
||||
|
||||
// Mnemonics mapping
|
||||
MNEMONIC_MAP.put("enter", "ENTER");
|
||||
MNEMONIC_MAP.put("enterreset", "ENTER");
|
||||
MNEMONIC_MAP.put("newline", "NEWLINE");
|
||||
MNEMONIC_MAP.put("tab", "TAB");
|
||||
MNEMONIC_MAP.put("backtab", "shift TAB");
|
||||
MNEMONIC_MAP.put("reset", "ESCAPE");
|
||||
MNEMONIC_MAP.put("clear", "CLEAR");
|
||||
MNEMONIC_MAP.put("eraseeof", "END");
|
||||
MNEMONIC_MAP.put("eof", "END");
|
||||
MNEMONIC_MAP.put("erasefld", "ERASE_INPUT");
|
||||
MNEMONIC_MAP.put("erinp", "ERASE_INPUT");
|
||||
MNEMONIC_MAP.put("eraseinput", "ERASE_INPUT");
|
||||
MNEMONIC_MAP.put("dup", "DUP");
|
||||
MNEMONIC_MAP.put("fieldmark", "FIELD_MARK");
|
||||
MNEMONIC_MAP.put("fldmark", "FIELD_MARK");
|
||||
MNEMONIC_MAP.put("fldext", "NEWLINE");
|
||||
MNEMONIC_MAP.put("field-exit", "NEWLINE");
|
||||
MNEMONIC_MAP.put("field+", "TAB");
|
||||
MNEMONIC_MAP.put("field-", "TAB");
|
||||
MNEMONIC_MAP.put("attn", "ATTN");
|
||||
MNEMONIC_MAP.put("sysreq", "SYSREQ");
|
||||
MNEMONIC_MAP.put("cursel", "CURSEL");
|
||||
MNEMONIC_MAP.put("copy", "COPY");
|
||||
MNEMONIC_MAP.put("paste", "PASTE");
|
||||
MNEMONIC_MAP.put("selectall", "SELECTALL");
|
||||
MNEMONIC_MAP.put("insert", "INSERT");
|
||||
MNEMONIC_MAP.put("delete", "DELETE");
|
||||
MNEMONIC_MAP.put("backspace", "BACK_SPACE");
|
||||
MNEMONIC_MAP.put("home", "HOME");
|
||||
MNEMONIC_MAP.put("end", "END");
|
||||
MNEMONIC_MAP.put("up", "UP");
|
||||
MNEMONIC_MAP.put("down", "DOWN");
|
||||
MNEMONIC_MAP.put("left", "LEFT");
|
||||
MNEMONIC_MAP.put("right", "RIGHT");
|
||||
MNEMONIC_MAP.put("pageup", "PAGE_UP");
|
||||
MNEMONIC_MAP.put("pagedn", "PAGE_DOWN");
|
||||
MNEMONIC_MAP.put("pagedown", "PAGE_DOWN");
|
||||
MNEMONIC_MAP.put("docmode", "DOCMODE");
|
||||
MNEMONIC_MAP.put("wordwrap", "WORDWRAP");
|
||||
MNEMONIC_MAP.put("apl", "APL");
|
||||
MNEMONIC_MAP.put("rule", "CROSSHAIR_RULER");
|
||||
MNEMONIC_MAP.put("statusbar", "STATUS_BAR");
|
||||
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
MNEMONIC_MAP.put("pf" + i, "PF" + i);
|
||||
}
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
MNEMONIC_MAP.put("pa" + i, "PA" + i);
|
||||
}
|
||||
// Shifted PF keys 1-12 = PF13-PF24
|
||||
for (int i = 1; i <= 12; i++) {
|
||||
MNEMONIC_MAP.put("spf" + i, "PF" + (i + 12));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .kmp file from a File object.
|
||||
*
|
||||
* @param file the .kmp file to parse
|
||||
* @return Map of j3270 action names to key stroke bindings
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public static Map<String, String> parseKmp(File file) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
return parseKmp(reader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .kmp file from an InputStream.
|
||||
*
|
||||
* @param in the InputStream to parse
|
||||
* @return Map of j3270 action names to key stroke bindings
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public static Map<String, String> parseKmp(InputStream in) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
|
||||
return parseKmp(reader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .kmp file from a Reader.
|
||||
* Supports both IBM Host On-Demand (HoD) .kmp format (hashKey prefixes)
|
||||
* and IBM Personal Communications (PCOMM) INI-style and bind statements.
|
||||
*
|
||||
* @param reader the reader to parse from
|
||||
* @return Map of j3270 action names to key stroke bindings
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public static Map<String, String> parseKmp(Reader reader) throws IOException {
|
||||
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
|
||||
List<String> rawLines = new ArrayList<>();
|
||||
boolean hasHodKeyRemap = false;
|
||||
boolean hasHodHashKey = false;
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
rawLines.add(line);
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.equalsIgnoreCase("[KeyRemap]")) {
|
||||
hasHodKeyRemap = true;
|
||||
}
|
||||
int eq = trimmed.indexOf('=');
|
||||
if (eq > 0) {
|
||||
String left = trimmed.substring(0, eq).trim();
|
||||
if (left.matches("^[A-Za-z][0-9]+$")) {
|
||||
hasHodHashKey = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHodKeyRemap || hasHodHashKey) {
|
||||
Map<String, String> hodMap = haus.nightmare.lib3270j.input.HodKeyMap.parseHodKmp(
|
||||
new StringReader(String.join("\n", rawLines))
|
||||
);
|
||||
if (hodMap != null && !hodMap.isEmpty()) {
|
||||
return hodMap;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
|
||||
|
||||
for (String raw : rawLines) {
|
||||
line = raw.trim();
|
||||
|
||||
// Strip comments
|
||||
int commentIdx = line.indexOf(';');
|
||||
if (commentIdx >= 0) {
|
||||
line = line.substring(0, commentIdx).trim();
|
||||
}
|
||||
commentIdx = line.indexOf('#');
|
||||
if (commentIdx >= 0) {
|
||||
line = line.substring(0, commentIdx).trim();
|
||||
}
|
||||
|
||||
if (line.isEmpty() || line.startsWith("[")) {
|
||||
continue; // Skip section headers and blank lines
|
||||
}
|
||||
|
||||
// Syntax 1: bind [mnemonic] KeyCombination OR bind action KeyCombination
|
||||
if (line.toLowerCase().startsWith("bind ")) {
|
||||
parseBindStatement(line.substring(5).trim(), actionToBindings);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Syntax 2: KEY<n>=action OR Modifier-KEY<n>=action OR KeyStroke=[mnemonic]
|
||||
int eq = line.indexOf('=');
|
||||
if (eq > 0) {
|
||||
String left = line.substring(0, eq).trim();
|
||||
String right = line.substring(eq + 1).trim();
|
||||
parseKeyAssignment(left, right, actionToBindings);
|
||||
}
|
||||
}
|
||||
|
||||
// Format into combined comma-separated bindings map
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : actionToBindings.entrySet()) {
|
||||
String action = entry.getKey();
|
||||
List<String> bindings = entry.getValue();
|
||||
if (!bindings.isEmpty()) {
|
||||
result.put(action, String.join(", ", bindings));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export key bindings to a .kmp file using IBM HoD format.
|
||||
*/
|
||||
public static void exportKmp(File file, Map<String, String> keyBindings) throws IOException {
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||
exportKmp(bw, keyBindings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export key bindings to a Writer using IBM HoD format.
|
||||
*/
|
||||
public static void exportKmp(Writer writer, Map<String, String> keyBindings) throws IOException {
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.exportHodKmp(writer, keyBindings);
|
||||
}
|
||||
|
||||
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
|
||||
String[] parts = stmt.split("\\s+", 2);
|
||||
if (parts.length < 2) return;
|
||||
|
||||
String target = parts[0].trim();
|
||||
String keyStrokeStr = parts[1].trim();
|
||||
|
||||
String action = resolveMnemonic(target);
|
||||
if (action == null) {
|
||||
// Check if parts[1] was the mnemonic instead (e.g. "bind KeyCombination [mnemonic]")
|
||||
String altAction = resolveMnemonic(keyStrokeStr);
|
||||
if (altAction != null) {
|
||||
action = altAction;
|
||||
keyStrokeStr = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
String normKeyStroke = normalizeKeyStroke(keyStrokeStr);
|
||||
if (normKeyStroke != null && !normKeyStroke.isEmpty()) {
|
||||
addBinding(actionToBindings, action, normKeyStroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
|
||||
if (left.matches("^[A-Za-z][0-9]+$")) {
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.KeyEntry entry =
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.parseHashKey(left, right);
|
||||
if (entry != null) {
|
||||
String stroke = entry.toKeyStrokeString();
|
||||
String act = haus.nightmare.lib3270j.input.HodKeyMap.resolveMnemonicToAction(right);
|
||||
if (act != null && stroke != null && !stroke.isEmpty()) {
|
||||
addBinding(actionToBindings, act, stroke);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
|
||||
String action = resolveMnemonic(right);
|
||||
String keyStroke = null;
|
||||
|
||||
if (action != null) {
|
||||
keyStroke = parseScanCodeEntry(left);
|
||||
} else {
|
||||
// Case B: left is mnemonic, right is KeyStroke
|
||||
action = resolveMnemonic(left);
|
||||
if (action != null) {
|
||||
keyStroke = normalizeKeyStroke(right);
|
||||
} else {
|
||||
// Case C: left is KeyStroke, right is mnemonic
|
||||
action = resolveMnemonic(right);
|
||||
if (action != null) {
|
||||
keyStroke = normalizeKeyStroke(left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action != null && keyStroke != null && !keyStroke.isEmpty()) {
|
||||
addBinding(actionToBindings, action, keyStroke);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse scan code entry like "KEY43", "S-KEY112", "C-KEY43", "CS-KEY85", etc.
|
||||
*/
|
||||
public static String parseScanCodeEntry(String entry) {
|
||||
if (entry == null || entry.isEmpty()) return null;
|
||||
|
||||
String upper = entry.toUpperCase().trim();
|
||||
int keyPos = upper.indexOf("KEY");
|
||||
if (keyPos < 0) {
|
||||
return normalizeKeyStroke(entry);
|
||||
}
|
||||
|
||||
String prefix = upper.substring(0, keyPos);
|
||||
if (prefix.endsWith("-")) {
|
||||
prefix = prefix.substring(0, prefix.length() - 1);
|
||||
}
|
||||
|
||||
String numPart = upper.substring(keyPos + 3);
|
||||
int scanCode;
|
||||
try {
|
||||
scanCode = Integer.parseInt(numPart.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String baseKey = SCAN_CODE_MAP.get(scanCode);
|
||||
if (baseKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (prefix.contains("C")) sb.append("ctrl ");
|
||||
if (prefix.contains("A") || prefix.contains("2")) sb.append("alt ");
|
||||
if (prefix.contains("S")) sb.append("shift ");
|
||||
if (prefix.contains("M")) sb.append("meta ");
|
||||
|
||||
sb.append(baseKey);
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize key stroke expression (e.g. "Ctrl+Shift+C" -> "ctrl shift C").
|
||||
*/
|
||||
public static String normalizeKeyStroke(String stroke) {
|
||||
if (stroke == null || stroke.trim().isEmpty()) return null;
|
||||
String s = stroke.trim();
|
||||
|
||||
// Check if it is an IBM scan code
|
||||
if (s.toUpperCase().contains("KEY")) {
|
||||
String fromScan = parseScanCodeEntry(s);
|
||||
if (fromScan != null) return fromScan;
|
||||
}
|
||||
|
||||
// Replace '+' with space
|
||||
s = s.replace("+", " ");
|
||||
String[] parts = s.split("\\s+");
|
||||
|
||||
StringBuilder mods = new StringBuilder();
|
||||
String mainKey = null;
|
||||
|
||||
for (String p : parts) {
|
||||
String lp = p.toLowerCase();
|
||||
if ("ctrl".equals(lp) || "control".equals(lp)) {
|
||||
mods.append("ctrl ");
|
||||
} else if ("shift".equals(lp)) {
|
||||
mods.append("shift ");
|
||||
} else if ("alt".equals(lp)) {
|
||||
mods.append("alt ");
|
||||
} else if ("meta".equals(lp) || "cmd".equals(lp) || "command".equals(lp)) {
|
||||
mods.append("meta ");
|
||||
} else {
|
||||
mainKey = p.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (mainKey == null) return null;
|
||||
return (mods.toString() + mainKey).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a mnemonic token (e.g. "[enter]", "enter", "[pf12]", "eraseeof")
|
||||
* into a j3270 action name.
|
||||
*/
|
||||
public static String resolveMnemonic(String token) {
|
||||
if (token == null) return null;
|
||||
String clean = token.trim();
|
||||
if (clean.startsWith("[") && clean.endsWith("]")) {
|
||||
clean = clean.substring(1, clean.length() - 1);
|
||||
}
|
||||
clean = clean.toLowerCase();
|
||||
|
||||
String direct = MNEMONIC_MAP.get(clean);
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
// Case-insensitive match on existing action names
|
||||
String upper = clean.toUpperCase();
|
||||
if (MNEMONIC_MAP.containsValue(upper)) {
|
||||
return upper;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void addBinding(Map<String, List<String>> map, String action, String keyStroke) {
|
||||
List<String> list = map.computeIfAbsent(action, k -> new ArrayList<>());
|
||||
if (!list.contains(keyStroke)) {
|
||||
list.add(keyStroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.MacroScreen;
|
||||
import haus.nightmare.lib3270j.macro.MacroScreens;
|
||||
import haus.nightmare.lib3270j.macro.MacroXmlParser;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroAction;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Interactive Swing dialog for loading, editing, running, and debugging
|
||||
* IBM Host On-Demand XML Macros (<HASCRIPT> / .mac files).
|
||||
*/
|
||||
public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private JTextArea xmlArea;
|
||||
private JTextArea logArea;
|
||||
private JLabel statusLabel;
|
||||
private JButton playBtn;
|
||||
private JButton pauseBtn;
|
||||
private JButton stopBtn;
|
||||
|
||||
private Macro macro;
|
||||
|
||||
public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "IBM HoD XML Macro Runtime (<HASCRIPT>)", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
this.macro = new Macro(client);
|
||||
this.macro.addMacroListener(this);
|
||||
this.macro.setPromptHandler(this);
|
||||
|
||||
buildUI();
|
||||
setSize(780, 580);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Top info label
|
||||
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
|
||||
topPanel.setOpaque(false);
|
||||
JLabel descLabel = new JLabel("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code><HASCRIPT></code>).<br>"
|
||||
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
|
||||
descLabel.setFont(labelFont);
|
||||
topPanel.add(descLabel, BorderLayout.CENTER);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Center split: XML editor and Execution Log
|
||||
xmlArea = new JTextArea();
|
||||
xmlArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
xmlArea.setLineWrap(false);
|
||||
xmlArea.setText(getDefaultMacroXml());
|
||||
ThemeManager.styleTextArea(xmlArea);
|
||||
|
||||
JScrollPane xmlScroll = new JScrollPane(xmlArea);
|
||||
ThemeManager.styleScrollPane(xmlScroll);
|
||||
xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML (<HASCRIPT>)"));
|
||||
|
||||
logArea = new JTextArea();
|
||||
logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1));
|
||||
logArea.setEditable(false);
|
||||
logArea.setLineWrap(true);
|
||||
ThemeManager.styleTextArea(logArea);
|
||||
|
||||
JScrollPane logScroll = new JScrollPane(logArea);
|
||||
ThemeManager.styleScrollPane(logScroll);
|
||||
logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log"));
|
||||
|
||||
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll);
|
||||
splitPane.setResizeWeight(0.65);
|
||||
splitPane.setOpaque(false);
|
||||
mainPanel.add(splitPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom control panel
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Status: Stopped");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(labelFont);
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton loadBtn = new JButton("Load .mac...");
|
||||
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
loadBtn.setFont(labelFont);
|
||||
loadBtn.addActionListener(e -> loadMacroFile());
|
||||
|
||||
JButton saveBtn = new JButton("Save .mac...");
|
||||
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveBtn.setFont(labelFont);
|
||||
saveBtn.addActionListener(e -> saveMacroFile());
|
||||
|
||||
playBtn = new JButton("Play");
|
||||
ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
playBtn.setFont(labelFont);
|
||||
playBtn.addActionListener(e -> playMacro());
|
||||
|
||||
pauseBtn = new JButton("Pause");
|
||||
ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pauseBtn.setFont(labelFont);
|
||||
pauseBtn.setEnabled(false);
|
||||
pauseBtn.addActionListener(e -> togglePause());
|
||||
|
||||
stopBtn = new JButton("Stop");
|
||||
ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
stopBtn.setFont(labelFont);
|
||||
stopBtn.setEnabled(false);
|
||||
stopBtn.addActionListener(e -> stopMacro());
|
||||
|
||||
JButton clearLogBtn = new JButton("Clear Log");
|
||||
ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
clearLogBtn.setFont(labelFont);
|
||||
clearLogBtn.addActionListener(e -> logArea.setText(""));
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(loadBtn);
|
||||
buttonPanel.add(saveBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(8));
|
||||
buttonPanel.add(playBtn);
|
||||
buttonPanel.add(pauseBtn);
|
||||
buttonPanel.add(stopBtn);
|
||||
buttonPanel.add(clearLogBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(8));
|
||||
buttonPanel.add(closeBtn);
|
||||
|
||||
bottomPanel.add(buttonPanel, BorderLayout.EAST);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private void loadMacroFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Open IBM HoD Macro");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
|
||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
xmlArea.setText(sb.toString());
|
||||
log("Loaded macro file: " + f.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveMacroFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Save IBM HoD Macro");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
|
||||
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
if (!f.getName().contains(".")) {
|
||||
f = new File(f.getParentFile(), f.getName() + ".mac");
|
||||
}
|
||||
try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) {
|
||||
w.write(xmlArea.getText());
|
||||
log("Saved macro file: " + f.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void playMacro() {
|
||||
String xml = xmlArea.getText();
|
||||
if (xml == null || xml.trim().isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
int opt = JOptionPane.showConfirmDialog(this,
|
||||
"Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?",
|
||||
"Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
if (opt != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
MacroScreens screens = MacroXmlParser.parse(xml);
|
||||
macro.setScreens(screens);
|
||||
macro.setClient(client);
|
||||
|
||||
log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName()));
|
||||
macro.play();
|
||||
} catch (Exception ex) {
|
||||
log("Parse / Launch Error: " + ex.getMessage());
|
||||
JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
public void togglePause() {
|
||||
if (macro.getState() == Macro.STATE_PLAYING) {
|
||||
macro.pause();
|
||||
pauseBtn.setText("Resume");
|
||||
log("Macro paused.");
|
||||
} else if (macro.getState() == Macro.STATE_PAUSED) {
|
||||
macro.resume();
|
||||
pauseBtn.setText("Pause");
|
||||
log("Macro resumed.");
|
||||
}
|
||||
}
|
||||
|
||||
public void stopMacro() {
|
||||
macro.stop();
|
||||
log("Macro stopped by user.");
|
||||
}
|
||||
|
||||
private void log(String msg) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
logArea.append(msg + "\n");
|
||||
logArea.setCaretPosition(logArea.getDocument().getLength());
|
||||
});
|
||||
}
|
||||
|
||||
// ========== MacroListener Callbacks ==========
|
||||
|
||||
@Override
|
||||
public void macroStateChanged(Macro macro, int oldState, int newState) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
switch (newState) {
|
||||
case Macro.STATE_PLAYING:
|
||||
statusLabel.setText("Status: Playing");
|
||||
statusLabel.setForeground(ThemeManager.getFgMain());
|
||||
playBtn.setEnabled(false);
|
||||
pauseBtn.setEnabled(true);
|
||||
pauseBtn.setText("Pause");
|
||||
stopBtn.setEnabled(true);
|
||||
break;
|
||||
case Macro.STATE_PAUSED:
|
||||
statusLabel.setText("Status: Paused");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
pauseBtn.setText("Resume");
|
||||
break;
|
||||
case Macro.STATE_ERROR:
|
||||
statusLabel.setText("Status: Error");
|
||||
statusLabel.setForeground(Color.RED);
|
||||
playBtn.setEnabled(true);
|
||||
pauseBtn.setEnabled(false);
|
||||
stopBtn.setEnabled(false);
|
||||
break;
|
||||
case Macro.STATE_STOPPED:
|
||||
default:
|
||||
statusLabel.setText("Status: Stopped");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
playBtn.setEnabled(true);
|
||||
pauseBtn.setEnabled(false);
|
||||
pauseBtn.setText("Pause");
|
||||
stopBtn.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroScreenMatching(MacroScreen screen) {
|
||||
log("Matched screen: [" + screen.getName() + "]");
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroActionExecuting(MacroAction action) {
|
||||
log(" -> Executing: " + action.getClass().getSimpleName());
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroMessage(String title, String message) {
|
||||
log("Message [" + title + "]: " + message);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroError(String error) {
|
||||
log("ERROR: " + error);
|
||||
}
|
||||
|
||||
// ========== MacroPromptHandler Callback ==========
|
||||
|
||||
@Override
|
||||
public String prompt(MacroActionPrompt prompt) {
|
||||
AtomicReference<String> result = new AtomicReference<>(null);
|
||||
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
result.set(showPromptModal(prompt));
|
||||
} else {
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
result.set(showPromptModal(prompt));
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result.get();
|
||||
}
|
||||
|
||||
private String showPromptModal(MacroActionPrompt prompt) {
|
||||
JPanel panel = new JPanel(new BorderLayout(5, 5));
|
||||
JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription());
|
||||
panel.add(msgLabel, BorderLayout.NORTH);
|
||||
|
||||
JTextField textField;
|
||||
if (prompt.isEncrypted()) {
|
||||
textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
|
||||
} else {
|
||||
textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
|
||||
}
|
||||
panel.add(textField, BorderLayout.CENTER);
|
||||
|
||||
int opt = JOptionPane.showConfirmDialog(this, panel,
|
||||
prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(),
|
||||
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||
|
||||
if (opt == JOptionPane.OK_OPTION) {
|
||||
if (textField instanceof JPasswordField) {
|
||||
return new String(((JPasswordField) textField).getPassword());
|
||||
}
|
||||
return textField.getText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getDefaultMacroXml() {
|
||||
return "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
|
||||
+ " <vars>\n"
|
||||
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
|
||||
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
|
||||
+ " </vars>\n\n"
|
||||
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
|
||||
+ " <description>\n"
|
||||
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||
+ " </description>\n"
|
||||
+ " <actions>\n"
|
||||
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
|
||||
+ " </actions>\n"
|
||||
+ " <nextscreens timeout=\"10000\">\n"
|
||||
+ " <nextscreen name=\"ReadyScreen\" />\n"
|
||||
+ " </nextscreens>\n"
|
||||
+ " </screen>\n\n"
|
||||
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
|
||||
+ " <description>\n"
|
||||
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||
+ " </description>\n"
|
||||
+ " <actions>\n"
|
||||
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
|
||||
+ " </actions>\n"
|
||||
+ " </screen>\n"
|
||||
+ "</HASCRIPT>";
|
||||
}
|
||||
}
|
||||
@@ -45,13 +45,10 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
// Spool Display
|
||||
private JTextArea spoolArea;
|
||||
|
||||
private static final Color DARK_BG = new Color(35, 35, 35);
|
||||
private static final Color DARK_FG = new Color(220, 220, 220);
|
||||
private static final Color DARK_FIELD_BG = new Color(45, 45, 45);
|
||||
|
||||
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
|
||||
super(parent, "IBM 3287 Printer Session Manager", false);
|
||||
this.parent = parent;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
config = new PrinterConfig(defaultHost, defaultPort);
|
||||
config.setUseTls(defaultTls);
|
||||
@@ -65,78 +62,74 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
mainPanel.setBackground(DARK_BG);
|
||||
|
||||
// Top: Configuration Panel
|
||||
JPanel topPanel = new JPanel(new GridBagLayout());
|
||||
topPanel.setOpaque(false);
|
||||
topPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
new LineBorder(new Color(65, 65, 65)), "Printer Session Configuration"));
|
||||
((TitledBorder) topPanel.getBorder()).setTitleColor(DARK_FG);
|
||||
topPanel.setBorder(ThemeManager.createTitledBorder("Printer Session Configuration"));
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(3, 4, 3, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Host / Port
|
||||
gbc.gridx = 0; gbc.gridy = 0;
|
||||
JLabel hLbl = new JLabel("Host:");
|
||||
hLbl.setForeground(DARK_FG); hLbl.setFont(labelFont);
|
||||
hLbl.setFont(labelFont);
|
||||
topPanel.add(hLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
hostField = createDarkField(config.getHost() != null ? config.getHost() : "localhost", 14);
|
||||
hostField = createField(config.getHost() != null ? config.getHost() : "localhost", 14);
|
||||
topPanel.add(hostField, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.weightx = 0;
|
||||
JLabel pLbl = new JLabel("Port:");
|
||||
pLbl.setForeground(DARK_FG); pLbl.setFont(labelFont);
|
||||
pLbl.setFont(labelFont);
|
||||
topPanel.add(pLbl, gbc);
|
||||
|
||||
gbc.gridx = 3; gbc.weightx = 0.5;
|
||||
portField = createDarkField(String.valueOf(config.getPort()), 5);
|
||||
portField = createField(String.valueOf(config.getPort()), 5);
|
||||
topPanel.add(portField, gbc);
|
||||
|
||||
// Printer LU / Display LU
|
||||
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0;
|
||||
JLabel pluLbl = new JLabel("Printer LU:");
|
||||
pluLbl.setForeground(DARK_FG); pluLbl.setFont(labelFont);
|
||||
pluLbl.setFont(labelFont);
|
||||
topPanel.add(pluLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
printerLuField = createDarkField("", 10);
|
||||
printerLuField = createField("", 10);
|
||||
topPanel.add(printerLuField, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.weightx = 0;
|
||||
JLabel assocLbl = new JLabel("Assoc LU:");
|
||||
assocLbl.setForeground(DARK_FG); assocLbl.setFont(labelFont);
|
||||
assocLbl.setFont(labelFont);
|
||||
topPanel.add(assocLbl, gbc);
|
||||
|
||||
gbc.gridx = 3; gbc.weightx = 0.5;
|
||||
displayLuField = createDarkField("", 10);
|
||||
displayLuField = createField("", 10);
|
||||
topPanel.add(displayLuField, gbc);
|
||||
|
||||
// CodePage & TLS
|
||||
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0;
|
||||
JLabel cpLbl = new JLabel("CodePage:");
|
||||
cpLbl.setForeground(DARK_FG); cpLbl.setFont(labelFont);
|
||||
cpLbl.setFont(labelFont);
|
||||
topPanel.add(cpLbl, gbc);
|
||||
|
||||
gbc.gridx = 1; gbc.weightx = 1.0;
|
||||
codePageCombo = new JComboBox<>(new String[]{"037", "1047", "500", "273", "277", "278", "280", "284", "285", "297", "870", "1140"});
|
||||
codePageCombo.setBackground(DARK_FIELD_BG);
|
||||
codePageCombo.setForeground(DARK_FG);
|
||||
ThemeManager.styleComboBox(codePageCombo);
|
||||
topPanel.add(codePageCombo, gbc);
|
||||
|
||||
gbc.gridx = 2; gbc.gridwidth = 2;
|
||||
JPanel tlsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
|
||||
tlsPanel.setOpaque(false);
|
||||
tlsCheck = new JCheckBox("TLS/SSL", config.isUseTls());
|
||||
tlsCheck.setOpaque(false); tlsCheck.setForeground(DARK_FG); tlsCheck.setFocusPainted(false);
|
||||
ThemeManager.styleCheckBox(tlsCheck);
|
||||
verifyCertCheck = new JCheckBox("Verify Cert", config.isTlsVerifyCert());
|
||||
verifyCertCheck.setOpaque(false); verifyCertCheck.setForeground(DARK_FG); verifyCertCheck.setFocusPainted(false);
|
||||
ThemeManager.styleCheckBox(verifyCertCheck);
|
||||
tlsPanel.add(tlsCheck);
|
||||
tlsPanel.add(verifyCertCheck);
|
||||
topPanel.add(tlsPanel, gbc);
|
||||
@@ -144,23 +137,22 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
// Destination Type & Target
|
||||
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 1;
|
||||
JLabel destLbl = new JLabel("Destination:");
|
||||
destLbl.setForeground(DARK_FG); destLbl.setFont(labelFont);
|
||||
destLbl.setFont(labelFont);
|
||||
topPanel.add(destLbl, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
destinationCombo = new JComboBox<>(PrinterConfig.DestinationType.values());
|
||||
destinationCombo.setSelectedItem(PrinterConfig.DestinationType.MEMORY);
|
||||
destinationCombo.setBackground(DARK_FIELD_BG);
|
||||
destinationCombo.setForeground(DARK_FG);
|
||||
ThemeManager.styleComboBox(destinationCombo);
|
||||
topPanel.add(destinationCombo, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
JLabel tgtLbl = new JLabel("Target Path:");
|
||||
tgtLbl.setForeground(DARK_FG); tgtLbl.setFont(labelFont);
|
||||
tgtLbl.setFont(labelFont);
|
||||
topPanel.add(tgtLbl, gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
targetField = createDarkField("printer_output.txt", 12);
|
||||
targetField = createField("printer_output.txt", 12);
|
||||
topPanel.add(targetField, gbc);
|
||||
|
||||
// Connect / Disconnect Buttons
|
||||
@@ -169,13 +161,11 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
connBtnPan.setOpaque(false);
|
||||
|
||||
connectBtn = new JButton("Start Printer Session");
|
||||
connectBtn.setBackground(new Color(50, 120, 50));
|
||||
connectBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(connectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
connectBtn.addActionListener(e -> startPrinterSession());
|
||||
|
||||
disconnectBtn = new JButton("Stop Session");
|
||||
disconnectBtn.setBackground(new Color(120, 50, 50));
|
||||
disconnectBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(disconnectBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
disconnectBtn.setEnabled(false);
|
||||
disconnectBtn.addActionListener(e -> stopPrinterSession());
|
||||
|
||||
@@ -188,9 +178,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
// Center: Spool Area & Status
|
||||
JPanel centerPanel = new JPanel(new BorderLayout(6, 6));
|
||||
centerPanel.setOpaque(false);
|
||||
centerPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
new LineBorder(new Color(65, 65, 65)), "Printer Spool & Status"));
|
||||
((TitledBorder) centerPanel.getBorder()).setTitleColor(DARK_FG);
|
||||
centerPanel.setBorder(ThemeManager.createTitledBorder("Printer Spool & Status"));
|
||||
|
||||
// Status Header
|
||||
JPanel statusHeader = new JPanel(new GridLayout(1, 4, 10, 0));
|
||||
@@ -198,19 +186,16 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
statusHeader.setBorder(new EmptyBorder(0, 4, 4, 4));
|
||||
|
||||
statusLabel = new JLabel("Status: Disconnected");
|
||||
statusLabel.setForeground(new Color(180, 180, 180));
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(labelFont);
|
||||
|
||||
sessionTypeLabel = new JLabel("Session: -");
|
||||
sessionTypeLabel.setForeground(DARK_FG);
|
||||
sessionTypeLabel.setFont(labelFont);
|
||||
|
||||
pagesLabel = new JLabel("Pages: 0");
|
||||
pagesLabel.setForeground(DARK_FG);
|
||||
pagesLabel.setFont(labelFont);
|
||||
|
||||
bytesLabel = new JLabel("Bytes: 0");
|
||||
bytesLabel.setForeground(DARK_FG);
|
||||
bytesLabel.setFont(labelFont);
|
||||
|
||||
statusHeader.add(statusLabel);
|
||||
@@ -221,14 +206,12 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
|
||||
// Spool text area
|
||||
spoolArea = new JTextArea();
|
||||
spoolArea.setBackground(new Color(25, 25, 25));
|
||||
spoolArea.setForeground(new Color(100, 255, 100));
|
||||
spoolArea.setCaretColor(Color.WHITE);
|
||||
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
spoolArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
spoolArea.setEditable(false);
|
||||
ThemeManager.styleTextArea(spoolArea);
|
||||
|
||||
JScrollPane spoolScroll = new JScrollPane(spoolArea);
|
||||
spoolScroll.setBorder(new LineBorder(new Color(60, 60, 60)));
|
||||
ThemeManager.styleScrollPane(spoolScroll);
|
||||
centerPanel.add(spoolScroll, BorderLayout.CENTER);
|
||||
|
||||
mainPanel.add(centerPanel, BorderLayout.CENTER);
|
||||
@@ -238,8 +221,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
JButton clearSpoolBtn = new JButton("Clear Spool");
|
||||
clearSpoolBtn.setBackground(new Color(50, 50, 50));
|
||||
clearSpoolBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(clearSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
clearSpoolBtn.addActionListener(e -> {
|
||||
spoolArea.setText("");
|
||||
if (printerSession != null && printerSession.getPD() != null) {
|
||||
@@ -250,18 +232,15 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
});
|
||||
|
||||
JButton saveSpoolBtn = new JButton("Save Spool As...");
|
||||
saveSpoolBtn.setBackground(new Color(50, 50, 50));
|
||||
saveSpoolBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(saveSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveSpoolBtn.addActionListener(e -> saveSpool());
|
||||
|
||||
JButton printSpoolBtn = new JButton("Print Spool...");
|
||||
printSpoolBtn.setBackground(new Color(50, 100, 160));
|
||||
printSpoolBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(printSpoolBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
printSpoolBtn.addActionListener(e -> printSpool());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
closeBtn.setBackground(new Color(60, 60, 60));
|
||||
closeBtn.setForeground(DARK_FG);
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
bottomPanel.add(clearSpoolBtn);
|
||||
@@ -273,17 +252,13 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private JTextField createDarkField(String text, int cols) {
|
||||
private JTextField createField(String text, int cols) {
|
||||
JTextField tf = new JTextField(text, cols);
|
||||
tf.setBackground(DARK_FIELD_BG);
|
||||
tf.setForeground(DARK_FG);
|
||||
tf.setCaretColor(DARK_FG);
|
||||
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
tf.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(new Color(65, 65, 65)),
|
||||
new EmptyBorder(2, 4, 2, 4)));
|
||||
tf.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextField(tf);
|
||||
return tf;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,19 +98,21 @@ public class ScreenExporter {
|
||||
writer.write("<style>\n");
|
||||
writer.write("body { background-color: #0a0a0a; color: #32cd32; font-family: 'Courier New', Courier, monospace; font-size: 14px; margin: 20px; }\n");
|
||||
writer.write(".screen { background-color: #000; padding: 15px; border-radius: 4px; display: inline-block; box-shadow: 0 0 10px rgba(0,0,0,0.8); line-height: 1.2; }\n");
|
||||
writer.write(".c-blue { color: #5078ff; }\n");
|
||||
writer.write(".c-red { color: #ff3232; }\n");
|
||||
writer.write(".c-pink { color: #ff82b4; }\n");
|
||||
writer.write(".c-green { color: #32cd32; }\n");
|
||||
writer.write(".c-turq { color: #40e0d0; }\n");
|
||||
writer.write(".c-yellow { color: #ffff50; }\n");
|
||||
writer.write(".c-blue { color: #7890f0; }\n");
|
||||
writer.write(".c-red { color: #ff0000; }\n");
|
||||
writer.write(".c-pink { color: #ff00ff; }\n");
|
||||
writer.write(".c-green { color: #00ff00; }\n");
|
||||
writer.write(".c-turq { color: #00ffff; }\n");
|
||||
writer.write(".c-yellow { color: #ffff00; }\n");
|
||||
writer.write(".c-white { color: #ffffff; }\n");
|
||||
writer.write(".c-black { color: #000000; }\n");
|
||||
writer.write(".c-orange { color: #ffa500; }\n");
|
||||
writer.write(".c-purple { color: #b482ff; }\n");
|
||||
writer.write(".c-palegreen { color: #90ee90; }\n");
|
||||
writer.write(".c-paleturq { color: #afeeee; }\n");
|
||||
writer.write(".c-grey { color: #aaaaaa; }\n");
|
||||
writer.write(".c-deepblue { color: #000080; }\n");
|
||||
writer.write(".c-orange { color: #ffa200; }\n");
|
||||
writer.write(".c-purple { color: #800080; }\n");
|
||||
writer.write(".c-palegreen { color: #008000; }\n");
|
||||
writer.write(".c-paleturq { color: #008080; }\n");
|
||||
writer.write(".c-mustard { color: #a0a000; }\n");
|
||||
writer.write(".c-grey { color: #c0c0c0; }\n");
|
||||
writer.write(".bold { font-weight: bold; }\n");
|
||||
writer.write(".underline { text-decoration: underline; }\n");
|
||||
writer.write("</style>\n</head>\n<body>\n<div class=\"screen\"><pre>");
|
||||
@@ -209,17 +211,17 @@ public class ScreenExporter {
|
||||
case 6: return "c-yellow";
|
||||
case 7: return "c-white";
|
||||
case 8: return "c-black";
|
||||
case 9: return "c-blue";
|
||||
case 9: return "c-deepblue";
|
||||
case 10: return "c-orange";
|
||||
case 11: return "c-purple";
|
||||
case 12: return "c-palegreen";
|
||||
case 13: return "c-paleturq";
|
||||
case 14: return "c-grey";
|
||||
case 15: return "c-white";
|
||||
case 14: return "c-mustard";
|
||||
case 15: return "c-grey";
|
||||
}
|
||||
}
|
||||
if (faIsProtected(currentFA & 0xFF)) {
|
||||
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-blue";
|
||||
return faIsHigh(currentFA & 0xFF) ? "c-white" : "c-turq";
|
||||
}
|
||||
return faIsHigh(currentFA & 0xFF) ? "c-red" : "c-green";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenHistoryListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenSnapshot;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.event.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Screen History viewer dialog matching IBM Host On-Demand ScreenHistory UI parity.
|
||||
*
|
||||
* Provides:
|
||||
* - Interactive JSlider scrubber to review past 64 screen buffer frames
|
||||
* - Previous / Next navigation buttons
|
||||
* - Exact monospace terminal frame preview with selection capability
|
||||
* - Copy Screen, Copy Selection, and Copy as Table actions
|
||||
* - Live update synchronization as new host screens arrive
|
||||
*/
|
||||
public class ScreenHistoryDialog extends JDialog implements ScreenHistoryListener {
|
||||
|
||||
private final J3270App parentApp;
|
||||
private final Telnet3270Client client;
|
||||
private final ScreenHistoryBuffer historyBuffer;
|
||||
|
||||
private JSlider scrubSlider;
|
||||
private JButton btnPrev;
|
||||
private JButton btnNext;
|
||||
private JLabel infoLabel;
|
||||
private JTextArea previewArea;
|
||||
private JButton btnCopyScreen;
|
||||
private JButton btnCopySelection;
|
||||
private JButton btnCopyTable;
|
||||
private JButton btnClose;
|
||||
|
||||
private int currentIndex = -1; // 0-based index into historyBuffer
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public ScreenHistoryDialog(J3270App parent, Telnet3270Client client) {
|
||||
super(parent, "Screen History", false);
|
||||
this.parentApp = parent;
|
||||
this.client = client;
|
||||
this.historyBuffer = client != null ? client.getScreenHistoryBuffer() : null;
|
||||
|
||||
AppIcon.applyTo(this);
|
||||
initComponents();
|
||||
setupKeyNavigation();
|
||||
|
||||
if (historyBuffer != null) {
|
||||
historyBuffer.addListener(this);
|
||||
refreshHistoryView(historyBuffer.size() - 1);
|
||||
} else {
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
if (historyBuffer != null) {
|
||||
historyBuffer.removeListener(ScreenHistoryDialog.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setSize(780, 560);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new BorderLayout(6, 6));
|
||||
JPanel contentPane = new JPanel(new BorderLayout(8, 8));
|
||||
contentPane.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
setContentPane(contentPane);
|
||||
|
||||
// Top Navigation Bar
|
||||
JPanel topPanel = new JPanel(new BorderLayout(8, 8));
|
||||
topPanel.setBorder(BorderFactory.createTitledBorder("History Scrub Navigation"));
|
||||
|
||||
JPanel sliderPanel = new JPanel(new BorderLayout(6, 6));
|
||||
btnPrev = new JButton("◄ Prev");
|
||||
ThemeManager.styleButton(btnPrev, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnPrev.addActionListener(e -> stepHistory(-1));
|
||||
|
||||
btnNext = new JButton("Next ►");
|
||||
ThemeManager.styleButton(btnNext, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnNext.addActionListener(e -> stepHistory(1));
|
||||
|
||||
scrubSlider = new JSlider(1, 1, 1);
|
||||
scrubSlider.setMajorTickSpacing(10);
|
||||
scrubSlider.setMinorTickSpacing(1);
|
||||
scrubSlider.setPaintTicks(true);
|
||||
scrubSlider.setSnapToTicks(true);
|
||||
scrubSlider.addChangeListener(e -> {
|
||||
if (!scrubSlider.getValueIsAdjusting()) {
|
||||
int selected = scrubSlider.getValue() - 1;
|
||||
displayScreen(selected);
|
||||
}
|
||||
});
|
||||
|
||||
sliderPanel.add(btnPrev, BorderLayout.WEST);
|
||||
sliderPanel.add(scrubSlider, BorderLayout.CENTER);
|
||||
sliderPanel.add(btnNext, BorderLayout.EAST);
|
||||
|
||||
infoLabel = new JLabel("Screen 0 of 0", SwingConstants.CENTER);
|
||||
infoLabel.setFont(infoLabel.getFont().deriveFont(Font.BOLD, 13f));
|
||||
topPanel.add(sliderPanel, BorderLayout.CENTER);
|
||||
topPanel.add(infoLabel, BorderLayout.SOUTH);
|
||||
|
||||
contentPane.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Center Terminal Preview Pane
|
||||
previewArea = new JTextArea();
|
||||
previewArea.setEditable(false);
|
||||
previewArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
|
||||
previewArea.setBackground(Color.BLACK);
|
||||
previewArea.setForeground(new Color(0, 255, 65)); // 3270 Green Phosphor
|
||||
previewArea.setCaretColor(Color.WHITE);
|
||||
previewArea.setLineWrap(false);
|
||||
previewArea.setMargin(new Insets(6, 6, 6, 6));
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(previewArea);
|
||||
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
|
||||
contentPane.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom Action Bar
|
||||
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||
|
||||
btnCopyScreen = new JButton("Copy Screen");
|
||||
ThemeManager.styleButton(btnCopyScreen, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnCopyScreen.setToolTipText("Copy entire historical screen to clipboard");
|
||||
btnCopyScreen.addActionListener(e -> copyCurrentScreen());
|
||||
|
||||
btnCopySelection = new JButton("Copy Selection");
|
||||
ThemeManager.styleButton(btnCopySelection, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnCopySelection.setToolTipText("Copy selected text to clipboard");
|
||||
btnCopySelection.addActionListener(e -> copySelectedText());
|
||||
|
||||
btnCopyTable = new JButton("Copy as Table");
|
||||
ThemeManager.styleButton(btnCopyTable, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnCopyTable.setToolTipText("Copy screen text with trimmed trailing spaces per row");
|
||||
btnCopyTable.addActionListener(e -> copyAsTable());
|
||||
|
||||
btnClose = new JButton("Close");
|
||||
ThemeManager.styleButton(btnClose, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnClose.addActionListener(e -> setVisible(false));
|
||||
|
||||
bottomPanel.add(btnCopyScreen);
|
||||
bottomPanel.add(btnCopySelection);
|
||||
bottomPanel.add(btnCopyTable);
|
||||
bottomPanel.add(Box.createHorizontalStrut(12));
|
||||
bottomPanel.add(btnClose);
|
||||
|
||||
contentPane.add(bottomPanel, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private void setupKeyNavigation() {
|
||||
// Keyboard shortcuts for rapid scrub navigation
|
||||
Action prevAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) { stepHistory(-1); }
|
||||
};
|
||||
Action nextAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) { stepHistory(1); }
|
||||
};
|
||||
Action homeAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) { displayScreen(0); }
|
||||
};
|
||||
Action endAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) {
|
||||
if (historyBuffer != null) displayScreen(historyBuffer.size() - 1);
|
||||
}
|
||||
};
|
||||
Action closeAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) { setVisible(false); }
|
||||
};
|
||||
|
||||
JRootPane root = getRootPane();
|
||||
InputMap im = root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
|
||||
ActionMap am = root.getActionMap();
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK), "prevScreen");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "prevScreen");
|
||||
am.put("prevScreen", prevAction);
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK), "nextScreen");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "nextScreen");
|
||||
am.put("nextScreen", nextAction);
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.ALT_DOWN_MASK), "homeScreen");
|
||||
am.put("homeScreen", homeAction);
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.ALT_DOWN_MASK), "endScreen");
|
||||
am.put("endScreen", endAction);
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeDialog");
|
||||
am.put("closeDialog", closeAction);
|
||||
}
|
||||
|
||||
private void stepHistory(int delta) {
|
||||
if (historyBuffer == null || historyBuffer.isEmpty()) return;
|
||||
int target = currentIndex + delta;
|
||||
target = Math.max(0, Math.min(target, historyBuffer.size() - 1));
|
||||
displayScreen(target);
|
||||
}
|
||||
|
||||
private void displayScreen(int index) {
|
||||
if (historyBuffer == null || historyBuffer.isEmpty()) {
|
||||
updateEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
int total = historyBuffer.size();
|
||||
index = Math.max(0, Math.min(index, total - 1));
|
||||
currentIndex = index;
|
||||
|
||||
ScreenSnapshot snap = historyBuffer.get(index);
|
||||
if (snap != null) {
|
||||
previewArea.setText(snap.getText());
|
||||
previewArea.setCaretPosition(0);
|
||||
|
||||
String timeStr = dateFormat.format(new Date(snap.getTimestamp()));
|
||||
infoLabel.setText("Screen " + (index + 1) + " of " + total + " (" + timeStr + ") — "
|
||||
+ snap.getRows() + "x" + snap.getCols() + ", Cursor: ["
|
||||
+ (snap.getCursorRow() + 1) + "," + (snap.getCursorCol() + 1) + "]");
|
||||
|
||||
scrubSlider.setValue(index + 1);
|
||||
}
|
||||
|
||||
btnPrev.setEnabled(index > 0);
|
||||
btnNext.setEnabled(index < total - 1);
|
||||
btnCopyScreen.setEnabled(true);
|
||||
btnCopySelection.setEnabled(true);
|
||||
btnCopyTable.setEnabled(true);
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
currentIndex = -1;
|
||||
previewArea.setText("No screen history captured yet.\n\nScreens are automatically recorded as the host session receives updates or unlocks the keyboard.");
|
||||
infoLabel.setText("Screen 0 of 0 — Empty History");
|
||||
scrubSlider.setEnabled(false);
|
||||
btnPrev.setEnabled(false);
|
||||
btnNext.setEnabled(false);
|
||||
btnCopyScreen.setEnabled(false);
|
||||
btnCopySelection.setEnabled(false);
|
||||
btnCopyTable.setEnabled(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh slider bounds and select target index.
|
||||
*/
|
||||
public synchronized void refreshHistoryView(int targetIndex) {
|
||||
if (historyBuffer == null || historyBuffer.isEmpty()) {
|
||||
updateEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
int total = historyBuffer.size();
|
||||
scrubSlider.setEnabled(true);
|
||||
scrubSlider.setMinimum(1);
|
||||
scrubSlider.setMaximum(total);
|
||||
|
||||
int select = targetIndex >= 0 ? Math.min(targetIndex, total - 1) : total - 1;
|
||||
displayScreen(select);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHistoryUpdated(ScreenHistoryBuffer buffer, ScreenSnapshot newSnapshot) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
boolean wasAtLatest = (currentIndex >= 0 && currentIndex == buffer.size() - 2);
|
||||
int newTotal = buffer.size();
|
||||
scrubSlider.setMaximum(newTotal);
|
||||
|
||||
if (wasAtLatest || currentIndex < 0) {
|
||||
// Keep tracked with latest frame
|
||||
displayScreen(newTotal - 1);
|
||||
} else {
|
||||
// Keep operator's active scrub position, update label
|
||||
displayScreen(currentIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void copyCurrentScreen() {
|
||||
if (historyBuffer == null || currentIndex < 0) return;
|
||||
ScreenSnapshot snap = historyBuffer.get(currentIndex);
|
||||
if (snap != null) {
|
||||
setClipboardText(snap.getText());
|
||||
}
|
||||
}
|
||||
|
||||
private void copySelectedText() {
|
||||
String selected = previewArea.getSelectedText();
|
||||
if (selected != null && !selected.isEmpty()) {
|
||||
setClipboardText(selected);
|
||||
} else {
|
||||
copyCurrentScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void copyAsTable() {
|
||||
if (historyBuffer == null || currentIndex < 0) return;
|
||||
ScreenSnapshot snap = historyBuffer.get(currentIndex);
|
||||
if (snap != null) {
|
||||
setClipboardText(snap.getFormattedTable());
|
||||
}
|
||||
}
|
||||
|
||||
private void setClipboardText(String text) {
|
||||
try {
|
||||
Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
cb.setContents(new StringSelection(text), null);
|
||||
} catch (Exception e) {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public class ScriptDialog extends JDialog {
|
||||
super(parent, "Run Keystrokes / ECL Script", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI();
|
||||
setSize(600, 480);
|
||||
@@ -34,41 +35,32 @@ public class ScriptDialog extends JDialog {
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
mainPanel.setBackground(new Color(35, 35, 35));
|
||||
|
||||
Color fg = new Color(220, 220, 220);
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Header
|
||||
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
|
||||
topPanel.setOpaque(false);
|
||||
JLabel descLabel = new JLabel("<html>Compose keystrokes and ECL bracketed mnemonics to stream to the mainframe.<br>"
|
||||
+ "Example: <code>TSO[enter]USER[tab]PASSWORD[enter]</code> or <code>[pf3][clear]</code></html>");
|
||||
descLabel.setForeground(new Color(180, 180, 180));
|
||||
descLabel.setFont(labelFont);
|
||||
topPanel.add(descLabel, BorderLayout.CENTER);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Script area
|
||||
scriptArea = new JTextArea();
|
||||
scriptArea.setBackground(new Color(25, 25, 25));
|
||||
scriptArea.setForeground(new Color(50, 205, 50));
|
||||
scriptArea.setCaretColor(Color.WHITE);
|
||||
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
scriptArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
scriptArea.setLineWrap(true);
|
||||
scriptArea.setWrapStyleWord(false);
|
||||
ThemeManager.styleTextArea(scriptArea);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(scriptArea);
|
||||
scrollPane.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(new Color(60, 60, 60)),
|
||||
new EmptyBorder(2, 2, 2, 2)));
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
|
||||
// Mnemonic helper buttons
|
||||
JPanel tokenPanel = new JPanel(new WrapLayout(FlowLayout.LEFT, 4, 4));
|
||||
tokenPanel.setOpaque(false);
|
||||
tokenPanel.setBorder(BorderFactory.createTitledBorder(
|
||||
new LineBorder(new Color(60, 60, 60)), "Insert Keystroke Token"));
|
||||
((TitledBorder) tokenPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
|
||||
tokenPanel.setBorder(ThemeManager.createTitledBorder("Insert Keystroke Token"));
|
||||
|
||||
String[] tokens = {
|
||||
"[enter]", "[tab]", "[backtab]", "[clear]", "[reset]",
|
||||
@@ -83,9 +75,8 @@ public class ScriptDialog extends JDialog {
|
||||
|
||||
for (String token : tokens) {
|
||||
JButton btn = new JButton(token);
|
||||
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
|
||||
btn.setBackground(new Color(50, 50, 50));
|
||||
btn.setForeground(fg);
|
||||
btn.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -2));
|
||||
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btn.setFocusable(false);
|
||||
btn.setMargin(new Insets(2, 4, 2, 4));
|
||||
btn.addActionListener(e -> insertToken(token));
|
||||
@@ -103,34 +94,30 @@ public class ScriptDialog extends JDialog {
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Ready");
|
||||
statusLabel.setForeground(new Color(160, 160, 160));
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(labelFont);
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton loadBtn = new JButton("Load Script...");
|
||||
loadBtn.setBackground(new Color(50, 50, 50));
|
||||
loadBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
loadBtn.setFont(labelFont);
|
||||
loadBtn.addActionListener(e -> loadScript());
|
||||
|
||||
JButton saveBtn = new JButton("Save Script...");
|
||||
saveBtn.setBackground(new Color(50, 50, 50));
|
||||
saveBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveBtn.setFont(labelFont);
|
||||
saveBtn.addActionListener(e -> saveScript());
|
||||
|
||||
JButton runBtn = new JButton("Execute");
|
||||
runBtn.setBackground(new Color(50, 120, 50));
|
||||
runBtn.setForeground(Color.WHITE);
|
||||
ThemeManager.styleButton(runBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
runBtn.setFont(labelFont);
|
||||
runBtn.addActionListener(e -> executeScript());
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
closeBtn.setBackground(new Color(60, 60, 60));
|
||||
closeBtn.setForeground(fg);
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
@@ -144,6 +131,7 @@ public class ScriptDialog extends JDialog {
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private void insertToken(String token) {
|
||||
|
||||
@@ -18,37 +18,67 @@ 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;
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
|
||||
// OIA colors
|
||||
private static final Color OIA_BG = new Color(20, 20, 20);
|
||||
private static final Color OIA_FG = new Color(50, 205, 50);
|
||||
private static final Color OIA_DIM = new Color(80, 80, 80);
|
||||
private static final Color OIA_ALERT = new Color(255, 80, 80);
|
||||
private static final Color OIA_WARN = new Color(255, 200, 80);
|
||||
private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener;
|
||||
|
||||
public StatusBar() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
setBackground(OIA_BG);
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40)));
|
||||
setPreferredSize(new Dimension(800, 22));
|
||||
setBackground(ThemeManager.getStatusBarBg());
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
|
||||
|
||||
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
Font oiaFont = ThemeManager.getMonospacedUiFont();
|
||||
updateBarHeight(oiaFont);
|
||||
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
|
||||
tlsStatus = createLabel("", oiaFont, OIA_FG);
|
||||
luName = createLabel("", oiaFont, OIA_FG);
|
||||
lockStatus = createLabel("", oiaFont, OIA_ALERT);
|
||||
fieldTypeStatus = createLabel("", oiaFont, OIA_DIM);
|
||||
codePageInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
modelInfo = createLabel("", oiaFont, OIA_DIM);
|
||||
cursorPosition = createLabel("001/001 [0000]", oiaFont, OIA_FG);
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
|
||||
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
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());
|
||||
|
||||
add(Box.createHorizontalStrut(6));
|
||||
add(connectionStatus);
|
||||
@@ -58,9 +88,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);
|
||||
@@ -76,23 +114,95 @@ 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) {
|
||||
if (this.client != null && this.client.getOIA() != null && this.oiaNotifyListener != null) {
|
||||
this.client.getOIA().unregisterOIAEvent(this.oiaNotifyListener);
|
||||
}
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
if (this.client != null && this.client.getOIA() != null) {
|
||||
this.oiaNotifyListener = event -> updateStatus();
|
||||
this.client.getOIA().registerOIAEvent(this.oiaNotifyListener);
|
||||
}
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
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)));
|
||||
Font oiaFont = ThemeManager.getMonospacedUiFont();
|
||||
updateBarHeight(oiaFont);
|
||||
applyFontRecursively(this, oiaFont);
|
||||
revalidate();
|
||||
repaint();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
private void updateBarHeight(Font font) {
|
||||
int fontHeight = 16;
|
||||
try {
|
||||
FontMetrics fm = getFontMetrics(font);
|
||||
if (fm != null) fontHeight = fm.getHeight();
|
||||
} catch (Exception ignored) {
|
||||
fontHeight = font.getSize() + 4;
|
||||
}
|
||||
int barHeight = Math.max(22, fontHeight + 6);
|
||||
setPreferredSize(new Dimension(800, barHeight));
|
||||
}
|
||||
|
||||
private void applyFontRecursively(Component comp, Font font) {
|
||||
if (comp instanceof JLabel) {
|
||||
comp.setFont(font);
|
||||
} else if (comp instanceof Container) {
|
||||
for (Component child : ((Container) comp).getComponents()) {
|
||||
applyFontRecursively(child, font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(this::updateStatus);
|
||||
return;
|
||||
}
|
||||
UITheme theme = ThemeManager.getTheme();
|
||||
if (client == null) {
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -101,38 +211,42 @@ public class StatusBar extends JPanel {
|
||||
switch (state) {
|
||||
case NOT_CONNECTED:
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
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...");
|
||||
connectionStatus.setForeground(OIA_ALERT);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
|
||||
break;
|
||||
case CONNECTED_3270:
|
||||
connectionStatus.setText("TN3270");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
break;
|
||||
case CONNECTED_TN3270E:
|
||||
connectionStatus.setText("TN3270E");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
break;
|
||||
case CONNECTED_SSCP:
|
||||
connectionStatus.setText("SSCP-LU");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
break;
|
||||
case CONNECTED_NVT:
|
||||
case CONNECTED_NVT_CHAR:
|
||||
case CONNECTED_E_NVT:
|
||||
connectionStatus.setText("NVT");
|
||||
connectionStatus.setForeground(OIA_FG);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
break;
|
||||
case CONNECTED_UNBOUND:
|
||||
connectionStatus.setText("Unbound");
|
||||
connectionStatus.setForeground(OIA_WARN);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaAttention(theme));
|
||||
break;
|
||||
default:
|
||||
connectionStatus.setText(state.name());
|
||||
connectionStatus.setForeground(OIA_DIM);
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -144,11 +258,11 @@ public class StatusBar extends JPanel {
|
||||
String protocol = session != null ? session.getProtocol() : "TLS";
|
||||
if (verified) {
|
||||
tlsStatus.setText("🔒 TLS");
|
||||
tlsStatus.setForeground(OIA_FG);
|
||||
tlsStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
|
||||
} else {
|
||||
tlsStatus.setText("🔓 TLS (Unverified)");
|
||||
tlsStatus.setForeground(new Color(255, 180, 80));
|
||||
tlsStatus.setForeground(ThemeManager.getOiaAttention(theme));
|
||||
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
|
||||
}
|
||||
} else {
|
||||
@@ -164,49 +278,96 @@ public class StatusBar extends JPanel {
|
||||
lu = "LU:" + client.getConfig().getLuName();
|
||||
}
|
||||
luName.setText(lu);
|
||||
luName.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
|
||||
// Lock / Inhibit status
|
||||
int inhibit = client.getOIA().getInputInhibited();
|
||||
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
|
||||
Color lockFg = ThemeManager.getOiaInputInhibited(theme);
|
||||
switch (inhibit) {
|
||||
case ECLConstants.INHIBIT_SYSTEM_LOCK:
|
||||
lockStatus.setText("X SYSTEM");
|
||||
lockFg = ThemeManager.getOiaInputInhibited(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_COMM_CHECK:
|
||||
lockStatus.setText("X COMM");
|
||||
lockFg = ThemeManager.getOiaCommCheck(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_NUMERIC_ONLY:
|
||||
lockStatus.setText("X NUM");
|
||||
lockFg = ThemeManager.getOiaAttention(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_PROTECTED_FIELD:
|
||||
lockStatus.setText("X PROT");
|
||||
lockFg = ThemeManager.getOiaInputInhibited(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_OVERFLOW:
|
||||
lockStatus.setText("X >");
|
||||
lockFg = ThemeManager.getOiaAttention(theme);
|
||||
break;
|
||||
case ECLConstants.INHIBIT_OPERATOR_DUE:
|
||||
lockStatus.setText("X OP");
|
||||
lockFg = ThemeManager.getOiaAttention(theme);
|
||||
break;
|
||||
default:
|
||||
lockStatus.setText("X LOCKED");
|
||||
lockFg = ThemeManager.getOiaInputInhibited(theme);
|
||||
break;
|
||||
}
|
||||
lockStatus.setForeground(OIA_ALERT);
|
||||
} else if (client.getInputProcessor().isInsertMode()) {
|
||||
lockStatus.setText("INSERT");
|
||||
lockStatus.setForeground(OIA_FG);
|
||||
lockStatus.setForeground(lockFg);
|
||||
} else {
|
||||
lockStatus.setText("");
|
||||
}
|
||||
|
||||
// Field status: Numeric vs Alphanumeric
|
||||
// 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 vs DBCS
|
||||
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
|
||||
if (client.getOIA().isNumeric()) {
|
||||
if (client.getOIA().isDBCS()) {
|
||||
fieldTypeStatus.setText("DBCS");
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
} else if (client.getOIA().isNumeric()) {
|
||||
fieldTypeStatus.setText("NUM");
|
||||
fieldTypeStatus.setForeground(OIA_WARN);
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
} else {
|
||||
fieldTypeStatus.setText("ALPHA");
|
||||
fieldTypeStatus.setForeground(OIA_DIM);
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
}
|
||||
} else {
|
||||
fieldTypeStatus.setText("");
|
||||
@@ -215,18 +376,31 @@ public class StatusBar extends JPanel {
|
||||
// Active Code Page
|
||||
String cp = client.getCodePage();
|
||||
codePageInfo.setText(cp != null ? "CP" + cp : "");
|
||||
codePageInfo.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
codePageInfo.setToolTipText("Active EBCDIC Code Page: CP" + cp);
|
||||
|
||||
// Model & Dimensions info
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
modelInfo.setText(client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]");
|
||||
String modelName;
|
||||
if (client.getConfig().isDynamicModel() || (client.getConfig().getModel() != null && client.getConfig().getModel().isDynamic())) {
|
||||
modelName = "IBM-DYNAMIC [" + rows + "x" + cols + "]";
|
||||
} else {
|
||||
modelName = client.getConfig().getModel().name().replace('_', '-') + " [" + rows + "x" + cols + "]";
|
||||
}
|
||||
modelInfo.setText(modelName);
|
||||
modelInfo.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
|
||||
// Cursor position and buffer address
|
||||
int curAddr = sb.getCursorAddress();
|
||||
int row = sb.getCursorRow() + 1;
|
||||
int col = sb.getCursorCol() + 1;
|
||||
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
|
||||
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
}
|
||||
|
||||
public String getLockStatusText() {
|
||||
return lockStatus != null ? lockStatus.getText() : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
/**
|
||||
* Supported UI themes for the j3270 Java desktop interface.
|
||||
*/
|
||||
public enum UITheme {
|
||||
DARK("Dark Mode"),
|
||||
LIGHT("Light Mode");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
UITheme(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static UITheme fromString(String name) {
|
||||
if (name == null) return DARK;
|
||||
String s = name.trim().toUpperCase();
|
||||
if ("LIGHT".equals(s) || "LIGHT MODE".equals(s) || "LIGHT_MODE".equals(s)) {
|
||||
return LIGHT;
|
||||
}
|
||||
return DARK;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
|
||||
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
super(parent, "Untrusted SSL/TLS Certificate", true);
|
||||
AppIcon.applyTo(this);
|
||||
buildUI(host, port, chain, exception);
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
@@ -26,25 +27,26 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
private void buildUI(String host, int port, X509Certificate[] chain, CertificateException exception) {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(12, 12));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
mainPanel.setBackground(new Color(30, 30, 30));
|
||||
|
||||
// Header
|
||||
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
|
||||
headerPanel.setBackground(new Color(30, 30, 30));
|
||||
headerPanel.setOpaque(false);
|
||||
JLabel iconLabel = new JLabel("⚠️");
|
||||
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
|
||||
iconLabel.putClientProperty("uiFontSizeDelta", 15);
|
||||
iconLabel.setFont(ThemeManager.getUiFont(Font.PLAIN, 15));
|
||||
headerPanel.add(iconLabel);
|
||||
|
||||
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
|
||||
titleBox.setBackground(new Color(30, 30, 30));
|
||||
titleBox.setOpaque(false);
|
||||
JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
|
||||
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
|
||||
titleLabel.setForeground(new Color(255, 180, 80));
|
||||
titleLabel.putClientProperty("uiFontSizeDelta", 3);
|
||||
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
|
||||
titleLabel.setForeground(ThemeManager.getOiaFgWarn());
|
||||
titleBox.add(titleLabel);
|
||||
|
||||
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
|
||||
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
subtitleLabel.setForeground(new Color(180, 180, 180));
|
||||
subtitleLabel.setFont(ThemeManager.getUiFont());
|
||||
subtitleLabel.setForeground(ThemeManager.getFgMuted());
|
||||
titleBox.add(subtitleLabel);
|
||||
headerPanel.add(titleBox);
|
||||
mainPanel.add(headerPanel, BorderLayout.NORTH);
|
||||
@@ -70,32 +72,29 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
|
||||
JTextArea detailsArea = new JTextArea(sb.toString());
|
||||
detailsArea.setEditable(false);
|
||||
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
detailsArea.setBackground(new Color(20, 20, 20));
|
||||
detailsArea.setForeground(new Color(210, 210, 210));
|
||||
detailsArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextArea(detailsArea);
|
||||
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(detailsArea);
|
||||
scrollPane.setPreferredSize(new Dimension(520, 260));
|
||||
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
|
||||
buttonPanel.setBackground(new Color(30, 30, 30));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton cancelBtn = new JButton("Cancel Connection");
|
||||
cancelBtn.setBackground(new Color(60, 60, 60));
|
||||
cancelBtn.setForeground(new Color(220, 220, 220));
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.addActionListener(e -> {
|
||||
accepted = false;
|
||||
dispose();
|
||||
});
|
||||
|
||||
JButton trustBtn = new JButton("Connect Anyway");
|
||||
trustBtn.setBackground(new Color(180, 100, 40));
|
||||
trustBtn.setForeground(Color.WHITE);
|
||||
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
trustBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
trustBtn.addActionListener(e -> {
|
||||
accepted = true;
|
||||
dispose();
|
||||
@@ -106,6 +105,7 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
getRootPane().setDefaultButton(trustBtn);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="124 738 320 320"
|
||||
width="320"
|
||||
height="320"
|
||||
xml:space="preserve"
|
||||
id="svg_j3270_icon"
|
||||
version="1.1">
|
||||
<metadata id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
|
||||
<rect x="124" y="738" width="320" height="320" fill="#000000" />
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g id="g14">
|
||||
<!-- Green Terminal Screen Frame -->
|
||||
<path
|
||||
id="path28"
|
||||
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
|
||||
<!-- Blue Wave / Exponential Curve -->
|
||||
<path
|
||||
id="path30"
|
||||
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
|
||||
<!-- Red Circular Arc -->
|
||||
<path
|
||||
id="path32"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
|
||||
<!-- Yellow Smiley Head -->
|
||||
<path
|
||||
id="path34"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
|
||||
<!-- 'l' -->
|
||||
<path
|
||||
id="path36"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<path
|
||||
id="path38"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<!-- 'i' -->
|
||||
<path
|
||||
id="path40"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<path
|
||||
id="path42"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<!-- 'b' -->
|
||||
<path
|
||||
id="path44"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<path
|
||||
id="path46"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<!-- '3' -->
|
||||
<path
|
||||
id="path48"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<path
|
||||
id="path50"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<!-- '2' -->
|
||||
<path
|
||||
id="path52"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<path
|
||||
id="path54"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<!-- '7' -->
|
||||
<path
|
||||
id="path56"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<path
|
||||
id="path58"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<!-- '0' -->
|
||||
<path
|
||||
id="path60"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<path
|
||||
id="path62"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<!-- 'j' -->
|
||||
<path
|
||||
id="path64"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<path
|
||||
id="path66"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<!-- Smile -->
|
||||
<path
|
||||
id="path68"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
|
||||
<!-- Left Eye (cyan) -->
|
||||
<path
|
||||
id="path70"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
|
||||
<!-- Right Eye (cyan) -->
|
||||
<path
|
||||
id="path72"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
|
||||
<!-- Nose -->
|
||||
<path
|
||||
id="path74"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2892.03,1528.23 -29.21,-20.68" />
|
||||
<path
|
||||
id="path76"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2862.82,1507.55 h 32.86" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="144 814 280 169"
|
||||
width="280"
|
||||
height="169"
|
||||
xml:space="preserve"
|
||||
id="svg_lib3270j_cropped"
|
||||
version="1.1">
|
||||
<metadata id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
|
||||
<rect x="144" y="814" width="280" height="169" fill="#000000" />
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g id="g14">
|
||||
<!-- Green Terminal Screen Frame -->
|
||||
<path
|
||||
id="path28"
|
||||
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
|
||||
<!-- Blue Wave / Exponential Curve -->
|
||||
<path
|
||||
id="path30"
|
||||
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
|
||||
<!-- Red Circular Arc -->
|
||||
<path
|
||||
id="path32"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
|
||||
<!-- Yellow Smiley Head -->
|
||||
<path
|
||||
id="path34"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
|
||||
<!-- 'l' -->
|
||||
<path
|
||||
id="path36"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<path
|
||||
id="path38"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<!-- 'i' -->
|
||||
<path
|
||||
id="path40"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<path
|
||||
id="path42"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<!-- 'b' -->
|
||||
<path
|
||||
id="path44"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<path
|
||||
id="path46"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<!-- '3' -->
|
||||
<path
|
||||
id="path48"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<path
|
||||
id="path50"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<!-- '2' -->
|
||||
<path
|
||||
id="path52"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<path
|
||||
id="path54"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<!-- '7' -->
|
||||
<path
|
||||
id="path56"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<path
|
||||
id="path58"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<!-- '0' -->
|
||||
<path
|
||||
id="path60"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<path
|
||||
id="path62"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<!-- 'j' -->
|
||||
<path
|
||||
id="path64"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<path
|
||||
id="path66"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<!-- Smile -->
|
||||
<path
|
||||
id="path68"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
|
||||
<!-- Left Eye (cyan) -->
|
||||
<path
|
||||
id="path70"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
|
||||
<!-- Right Eye (cyan) -->
|
||||
<path
|
||||
id="path72"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
|
||||
<!-- Nose -->
|
||||
<path
|
||||
id="path74"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2892.03,1528.23 -29.21,-20.68" />
|
||||
<path
|
||||
id="path76"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2862.82,1507.55 h 32.86" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 311 B |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 452 B |
|
After Width: | Height: | Size: 714 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,57 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.AppIcon;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class AppIconTest {
|
||||
|
||||
@Test
|
||||
public void testIconsAreLoaded() {
|
||||
List<Image> icons = AppIcon.getAppIcons();
|
||||
assertNotNull(icons, "Icons list must not be null");
|
||||
assertFalse(icons.isEmpty(), "At least one icon image must be loaded");
|
||||
|
||||
// Verify standard sizes are present
|
||||
int[] expectedSizes = {16, 24, 32, 48, 64, 128, 256, 512};
|
||||
for (int expectedSize : expectedSizes) {
|
||||
boolean found = icons.stream().anyMatch(img -> img.getWidth(null) == expectedSize && img.getHeight(null) == expectedSize);
|
||||
assertTrue(found, "Expected icon of size " + expectedSize + "x" + expectedSize + " was not found");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultIcon() {
|
||||
Image defaultIcon = AppIcon.getDefaultIcon();
|
||||
assertNotNull(defaultIcon, "Default icon must not be null");
|
||||
assertEquals(256, defaultIcon.getWidth(null), "Default icon width should be 256");
|
||||
assertEquals(256, defaultIcon.getHeight(null), "Default icon height should be 256");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplyToInHeadless() {
|
||||
// Must execute without exception even in headless environment
|
||||
assertDoesNotThrow(() -> AppIcon.applyTo(null));
|
||||
|
||||
if (!GraphicsEnvironment.isHeadless()) {
|
||||
JFrame frame = new JFrame();
|
||||
assertDoesNotThrow(() -> AppIcon.applyTo(frame));
|
||||
frame.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSvgResourcesExist() {
|
||||
URL svgUrl = getClass().getResource("/icons/lib3270j.svg");
|
||||
assertNotNull(svgUrl, "lib3270j.svg must exist in /icons/");
|
||||
|
||||
URL iconSvgUrl = getClass().getResource("/icons/j3270_icon.svg");
|
||||
assertNotNull(iconSvgUrl, "j3270_icon.svg must exist in /icons/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.ConnectChoiceDialog;
|
||||
import haus.nightmare.j3270.ui.ThemeManager;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionConflictTest {
|
||||
|
||||
private final List<J3270App> createdApps = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
for (J3270App app : createdApps) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private J3270App createApp() {
|
||||
try {
|
||||
J3270App app = new J3270App();
|
||||
createdApps.add(app);
|
||||
return app;
|
||||
} catch (HeadlessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectChoiceDialog components, styling, and action results")
|
||||
public void testConnectChoiceDialogComponents() {
|
||||
try {
|
||||
ConnectChoiceDialog dialog = new ConnectChoiceDialog(null, "mvs.corp.local:23");
|
||||
assertEquals("Active Connection", dialog.getTitle());
|
||||
assertNotNull(dialog.getNewWindowButton());
|
||||
assertNotNull(dialog.getAbortButton());
|
||||
assertNotNull(dialog.getCancelButton());
|
||||
assertEquals("New Window", dialog.getNewWindowButton().getText());
|
||||
assertEquals("Abort Current Connection", dialog.getAbortButton().getText());
|
||||
assertEquals("Cancel", dialog.getCancelButton().getText());
|
||||
|
||||
assertTrue(dialog.getLine1().getText().contains("mvs.corp.local:23"));
|
||||
assertTrue(dialog.getLine2().getText().contains("new window"));
|
||||
assertTrue(dialog.getLine2().getText().contains("abort"));
|
||||
|
||||
// Test button click choices
|
||||
dialog.getNewWindowButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.NEW_WINDOW, dialog.getChoice());
|
||||
|
||||
ConnectChoiceDialog dialog2 = new ConnectChoiceDialog(null, "cms.test:992");
|
||||
dialog2.getAbortButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.ABORT_CURRENT, dialog2.getChoice());
|
||||
|
||||
ConnectChoiceDialog dialog3 = new ConnectChoiceDialog(null, "cics.test:23");
|
||||
dialog3.getCancelButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.CANCEL, dialog3.getChoice());
|
||||
} catch (HeadlessException ignored) {
|
||||
// Expected in headless CI environments without X11
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with NEW_WINDOW choice opens in a new window and preserves current connection")
|
||||
public void testConnectChoiceNewWindow() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
config1.setSavedProfileName("Host 1");
|
||||
app.connect(config1, false);
|
||||
|
||||
assertTrue(app.getConnectedTarget().contains("host1.test") || app.getConnectedTarget().contains("Host 1"));
|
||||
|
||||
// Configure handler to return NEW_WINDOW
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
assertTrue(target.contains("host1.test") || target.contains("Host 1"));
|
||||
return ConnectChoiceDialog.Choice.NEW_WINDOW;
|
||||
});
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
config2.setSavedProfileName("Host 2");
|
||||
app.connect(config2, true);
|
||||
|
||||
// Handler was invoked
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called when already connected");
|
||||
|
||||
// A new window was opened
|
||||
assertEquals(initialWindows + 1, J3270App.getOpenWindows().size(),
|
||||
"A new window should have been added to openWindows");
|
||||
|
||||
// Original app remains on config1
|
||||
assertEquals("host1.test", app.getClient().getConfig().getHost());
|
||||
|
||||
// Find the new window
|
||||
J3270App newWindow = null;
|
||||
for (J3270App w : J3270App.getOpenWindows()) {
|
||||
if (w != app) {
|
||||
newWindow = w;
|
||||
createdApps.add(w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(newWindow, "New window must exist");
|
||||
assertNotNull(newWindow.getClient(), "New window should have a client");
|
||||
assertEquals("host2.test", newWindow.getClient().getConfig().getHost(),
|
||||
"New window must be connected to host2.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with ABORT_CURRENT choice aborts existing connection and connects in same window")
|
||||
public void testConnectChoiceAbortCurrent() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
|
||||
});
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called");
|
||||
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
|
||||
"No new window should be created when choosing ABORT_CURRENT");
|
||||
assertEquals("host2.test", app.getClient().getConfig().getHost(),
|
||||
"Same window should now be connected to host2.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with CANCEL choice keeps existing connection and creates no new window")
|
||||
public void testConnectChoiceCancel() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
return ConnectChoiceDialog.Choice.CANCEL;
|
||||
});
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called");
|
||||
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
|
||||
"No new window should be created when choosing CANCEL");
|
||||
assertEquals("host1.test", app.getClient().getConfig().getHost(),
|
||||
"Existing window must remain connected to host1.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("File menu contains Close Window item with VK_W accelerator")
|
||||
public void testCloseWindowMenuItem() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb);
|
||||
JMenu fileMenu = null;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
if ("File".equals(mb.getMenu(i).getText())) {
|
||||
fileMenu = mb.getMenu(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(fileMenu, "File menu must exist");
|
||||
|
||||
JMenuItem closeWindowItem = null;
|
||||
for (int i = 0; i < fileMenu.getItemCount(); i++) {
|
||||
JMenuItem item = fileMenu.getItem(i);
|
||||
if (item != null && "Close Window".equals(item.getText())) {
|
||||
closeWindowItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(closeWindowItem, "File menu must contain Close Window item");
|
||||
KeyStroke ks = closeWindowItem.getAccelerator();
|
||||
assertNotNull(ks, "Close Window item must have an accelerator");
|
||||
assertEquals(KeyEvent.VK_W, ks.getKeyCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Closing one window when multiple windows are open leaves remaining window open")
|
||||
public void testMultiWindowCloseIndependent() {
|
||||
J3270App app1 = createApp();
|
||||
if (app1 == null) return;
|
||||
|
||||
J3270App app2 = createApp();
|
||||
if (app2 == null) return;
|
||||
|
||||
assertTrue(J3270App.getOpenWindows().contains(app1));
|
||||
assertTrue(J3270App.getOpenWindows().contains(app2));
|
||||
assertEquals(2, J3270App.getOpenWindows().size());
|
||||
|
||||
// Close app1
|
||||
app1.closeWindow();
|
||||
|
||||
assertFalse(J3270App.getOpenWindows().contains(app1), "app1 should be removed from open windows");
|
||||
assertTrue(J3270App.getOpenWindows().contains(app2), "app2 should remain open");
|
||||
assertEquals(1, J3270App.getOpenWindows().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Abort current connection cleanly tears down old client and connects new client in same window")
|
||||
public void testAbortCurrentConnectionGracefullyDisconnects() throws Exception {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
Telnet3270Client oldClient = app.getClient();
|
||||
assertNotNull(oldClient);
|
||||
|
||||
app.setConnectChoiceHandler((owner, target) -> ConnectChoiceDialog.Choice.ABORT_CURRENT);
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
// Client on app is immediately replaced with new client
|
||||
Telnet3270Client newClient = app.getClient();
|
||||
assertNotNull(newClient);
|
||||
assertNotSame(oldClient, newClient);
|
||||
assertEquals("host2.test", newClient.getConfig().getHost());
|
||||
|
||||
// Old client should be disconnected
|
||||
Thread.sleep(100);
|
||||
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, oldClient.getConnectionState());
|
||||
assertFalse(oldClient.isConnected());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Explicit app.disconnect() tears down client and clears UI state")
|
||||
public void testExplicitDisconnectResetsUI() throws Exception {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config, false);
|
||||
|
||||
Telnet3270Client client = app.getClient();
|
||||
assertNotNull(client);
|
||||
|
||||
app.disconnect();
|
||||
|
||||
assertNull(app.getClient());
|
||||
assertFalse(app.isConnected());
|
||||
Thread.sleep(100);
|
||||
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
assertFalse(client.isConnected());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
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");
|
||||
File logFile1 = new File("j3270.log.1");
|
||||
assertTrue(logFile.exists() || logFile1.exists(), "j3270.log should be created when debug is enabled");
|
||||
|
||||
// Clean up
|
||||
J3270App.configureLogging(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
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.Frame;
|
||||
import java.awt.HeadlessException;
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionHostTypeConfigTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private String originalDefaultHostType;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
FileTransferDialog.resetSessionState();
|
||||
originalSavedHosts = HostStorage.getSavedHosts();
|
||||
originalDefaultHostType = Settings.getDefaultHostType();
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
FileTransferDialog.resetSessionState();
|
||||
HostStorage.clearAll();
|
||||
if (originalSavedHosts != null) {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
}
|
||||
Settings.setDefaultHostType(originalDefaultHostType);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost converts to ConnectionConfig preserving hostType and saved connection metadata")
|
||||
public void testSavedHostToConnectionConfigPreservesHostType() {
|
||||
SavedHost cmsHost = new SavedHost(
|
||||
"cms-uuid-1", "VM/CMS System", "vm370.corp.local", 23, 4, 62, 160, "",
|
||||
false, "CMS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgCms = cmsHost.toConnectionConfig();
|
||||
assertTrue(cfgCms.isSavedConnection(), "Config from SavedHost must be marked as saved connection");
|
||||
assertEquals("cms-uuid-1", cfgCms.getSavedProfileId());
|
||||
assertEquals("VM/CMS System", cfgCms.getSavedProfileName());
|
||||
assertEquals("CMS", cfgCms.getHostType());
|
||||
assertEquals(FTConfig.HostType.CMS, cfgCms.getFtHostType());
|
||||
|
||||
SavedHost cicsHost = new SavedHost(
|
||||
"cics-uuid-2", "CICS Gateway", "cics.corp.local", 23, 2, 24, 80, "LU1",
|
||||
false, "CICS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgCics = cicsHost.toConnectionConfig();
|
||||
assertTrue(cfgCics.isSavedConnection());
|
||||
assertEquals("CICS", cfgCics.getHostType());
|
||||
assertEquals(FTConfig.HostType.CICS, cfgCics.getFtHostType());
|
||||
|
||||
SavedHost tsoHost = new SavedHost(
|
||||
"tso-uuid-3", "MVS TSO", "tso.corp.local", 23, 4, 43, 80, "",
|
||||
false, "TSO", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgTso = tsoHost.toConnectionConfig();
|
||||
assertTrue(cfgTso.isSavedConnection());
|
||||
assertEquals("TSO", cfgTso.getHostType());
|
||||
assertEquals(FTConfig.HostType.TSO, cfgTso.getFtHostType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog preselects CMS when using a saved connection configured as CMS")
|
||||
public void testFileTransferDialogPrefillsFromSavedConnectionCms() {
|
||||
ConnectionConfig config = new ConnectionConfig("vm.example.com", 23);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId("vm-1");
|
||||
config.setSavedProfileName("VM370");
|
||||
config.setHostType("CMS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must preselect CMS when saved connection is configured as CMS");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog preselects CICS when using a saved connection configured as CICS")
|
||||
public void testFileTransferDialogPrefillsFromSavedConnectionCics() {
|
||||
ConnectionConfig config = new ConnectionConfig("cics.example.com", 23);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId("cics-1");
|
||||
config.setSavedProfileName("CICS Region");
|
||||
config.setHostType("CICS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CICS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must preselect CICS when saved connection is configured as CICS");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog overrides stale prior transfer state with configured host type from saved connection")
|
||||
public void testFileTransferDialogOverridesOldTransferStateWhenSavedConnectionUsed() {
|
||||
// Simulate a prior transfer performed on a TSO system
|
||||
FileTransferDialog.TransferSessionState oldState = new FileTransferDialog.TransferSessionState();
|
||||
oldState.hostType = FTConfig.HostType.TSO;
|
||||
oldState.localFile = "test.txt";
|
||||
oldState.hostFile = "USER.DATA";
|
||||
FileTransferDialog.setLastTransferState(oldState);
|
||||
|
||||
// Now connect to a CMS saved connection
|
||||
ConnectionConfig cmsConfig = new ConnectionConfig("vm.corp.internal", 23);
|
||||
cmsConfig.setSavedConnection(true);
|
||||
cmsConfig.setSavedProfileName("VM Prod");
|
||||
cmsConfig.setHostType("CMS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(cmsConfig);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must use the configured host type (CMS) of the saved connection rather than prior session's TSO");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectDialog allows host type configuration and saves it to profile")
|
||||
public void testConnectDialogHostTypeConfigurationAndProfile() {
|
||||
try {
|
||||
ConnectDialog dialog = new ConnectDialog((Frame) null);
|
||||
assertNotNull(dialog.getHostTypeCombo(), "ConnectDialog must provide a Host Type combo box");
|
||||
assertEquals("TSO", dialog.getSelectedHostType());
|
||||
|
||||
// Switch to CMS
|
||||
dialog.setInitialHostType("CMS");
|
||||
assertEquals("CMS", dialog.getSelectedHostType());
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getSelectedFtHostType());
|
||||
|
||||
// Switch to CICS
|
||||
dialog.setInitialHostType("CICS");
|
||||
assertEquals("CICS", dialog.getSelectedHostType());
|
||||
assertEquals(FTConfig.HostType.CICS, dialog.getSelectedFtHostType());
|
||||
|
||||
// Loading a SavedHost profile updates host type in ConnectDialog
|
||||
SavedHost cmsProfile = new SavedHost(
|
||||
"cms-prof-1", "CMS Dev", "cms.dev.local", 23, 4, 62, 160, "",
|
||||
false, "CMS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
dialog.loadProfile(cmsProfile);
|
||||
assertEquals("CMS", dialog.getSelectedHostType());
|
||||
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Settings persists and exports defaultHostType")
|
||||
public void testSettingsDefaultHostTypePersistenceAndIni() throws Exception {
|
||||
Settings.setDefaultHostType("CMS");
|
||||
assertEquals("CMS", Settings.getDefaultHostType());
|
||||
|
||||
File tmpIni = File.createTempFile("j3270_test_cfg", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try (PrintWriter writer = new PrintWriter(tmpIni)) {
|
||||
writer.println("[connection]");
|
||||
writer.println("hostType = CICS");
|
||||
}
|
||||
|
||||
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
|
||||
assertEquals("CICS", Settings.getDefaultHostType());
|
||||
|
||||
// Test export
|
||||
File exportFile = File.createTempFile("j3270_export", ".ini");
|
||||
exportFile.deleteOnExit();
|
||||
Settings.exportToIniFile(exportFile.getAbsolutePath());
|
||||
|
||||
String content = new String(java.nio.file.Files.readAllBytes(exportFile.toPath()));
|
||||
assertTrue(content.contains("hostType = CICS"), "Exported INI must contain hostType = CICS");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("J3270App setConnectionHostType updates connection config and syncs with profile in HostStorage")
|
||||
public void testSetConnectionHostTypeUpdatesActiveConnectionAndStorage() {
|
||||
SavedHost saved = new SavedHost(
|
||||
"saved-prof-1", "Prod CMS", "vm.prod", 23, 4, 62, 160, "",
|
||||
false, "TSO", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
HostStorage.saveHost(saved);
|
||||
|
||||
ConnectionConfig config = saved.toConnectionConfig();
|
||||
assertEquals("TSO", config.getHostType());
|
||||
|
||||
try {
|
||||
J3270App app = new J3270App();
|
||||
try {
|
||||
// Verify Connection menu exists in JMenuBar
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb);
|
||||
boolean hasConnectionMenu = false;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null && "Connection".equals(m.getText())) {
|
||||
hasConnectionMenu = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(hasConnectionMenu, "JMenuBar must include a Connection menu");
|
||||
|
||||
// Simulate connecting with this config
|
||||
app.connect(config);
|
||||
|
||||
// Change host type via setConnectionHostType
|
||||
app.setConnectionHostType(FTConfig.HostType.CMS);
|
||||
assertEquals(FTConfig.HostType.CMS, app.getClient().getConfig().getFtHostType());
|
||||
|
||||
// Profile in storage should also be updated
|
||||
SavedHost reloaded = HostStorage.findById("saved-prof-1");
|
||||
assertNotNull(reloaded);
|
||||
assertEquals("CMS", reloaded.getHostType());
|
||||
} finally {
|
||||
app.dispose();
|
||||
}
|
||||
} catch (HeadlessException ignored) {
|
||||
// In headless environments where JFrame cannot be initialized, test model updates directly
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getConfig().setFtHostType(FTConfig.HostType.CMS);
|
||||
saved.setHostType(FTConfig.HostType.CMS.name());
|
||||
HostStorage.saveHost(saved);
|
||||
|
||||
SavedHost reloaded = HostStorage.findById("saved-prof-1");
|
||||
assertNotNull(reloaded);
|
||||
assertEquals("CMS", reloaded.getHostType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPdsSessionStatePersistence() {
|
||||
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
|
||||
state.hostType = FTConfig.HostType.TSO;
|
||||
state.isSend = true;
|
||||
state.hostFile = "'USER.PDS.DATA'";
|
||||
state.units = FTConfig.AllocationUnit.CYLINDERS;
|
||||
state.space = "10,5";
|
||||
state.dirBlocks = "20";
|
||||
|
||||
FileTransferDialog.setLastTransferState(state);
|
||||
|
||||
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
|
||||
assertNotNull(retrieved);
|
||||
assertEquals(FTConfig.AllocationUnit.CYLINDERS, retrieved.units);
|
||||
assertEquals("10,5", retrieved.space);
|
||||
assertEquals("20", retrieved.dirBlocks);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, null);
|
||||
assertEquals(FTConfig.AllocationUnit.CYLINDERS, dialog.getUnitsCombo().getSelectedItem());
|
||||
assertEquals("10,5", dialog.getSpaceField().getText());
|
||||
assertEquals("20", dialog.getDirField().getText());
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostDirectoryDialogStructureAndFilter() {
|
||||
// Test default query resolution
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, null));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, ""));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, " "));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "* * *"));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "*"));
|
||||
assertEquals("TEST EXEC", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "TEST EXEC"));
|
||||
|
||||
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, null));
|
||||
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, ""));
|
||||
assertEquals("'MY.DATASET'", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, "'MY.DATASET'"));
|
||||
|
||||
// Test CMS filter formatting
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(null));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(""));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("* * *"));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("*"));
|
||||
assertEquals("TEST * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST"));
|
||||
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC"));
|
||||
assertEquals("TEST EXEC B", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC B"));
|
||||
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST.EXEC"));
|
||||
|
||||
try {
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "");
|
||||
assertNotNull(dialog);
|
||||
assertFalse(dialog.isConfirmed());
|
||||
assertNull(dialog.getSelectedHostFile());
|
||||
assertEquals("* * A", dialog.getQueryField().getText());
|
||||
dialog.dispose();
|
||||
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog2 =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "* * *");
|
||||
assertEquals("* * A", dialog2.getQueryField().getText());
|
||||
dialog2.dispose();
|
||||
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog3 =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "TEST EXEC");
|
||||
assertEquals("TEST EXEC", dialog3.getQueryField().getText());
|
||||
dialog3.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesFilterWildcards() {
|
||||
CMSDirectoryEntry cmsA = new CMSDirectoryEntry("PROFILE", "EXEC", "A1");
|
||||
CMSDirectoryEntry cmsB = new CMSDirectoryEntry("PROFILE", "EXEC", "B1");
|
||||
CMSDirectoryEntry cmsSys = new CMSDirectoryEntry("SYSTEM", "CONFIG", "S2");
|
||||
|
||||
// * * A matches A disk files (A1, A0, etc.), excludes B and S
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* * A"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsB, "* * A"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* * A"));
|
||||
|
||||
// Blank, *, * * * matches all
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, ""));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsB, "* * *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsSys, "* * *"));
|
||||
|
||||
// Two tokens (fn ft) and dot tokens
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* EXEC"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*.EXEC"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROFILE *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROF*.*"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* EXEC"));
|
||||
|
||||
// TSO Wildcards
|
||||
TSODirectoryEntry tso = new TSODirectoryEntry("TSOUSER.TEST.DATA");
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, ""));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "* * *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*.DATA"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TEST"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TSOUSER.*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "'TSOUSER.TEST.*'"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(tso, "*.CNTL"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.ui.SettingsDialog;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionIniStorageTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private String origHost;
|
||||
private int origPort;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalSavedHosts = HostStorage.getSavedHosts();
|
||||
origHost = Settings.getAutoConnectHost();
|
||||
origPort = Settings.getAutoConnectPort();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
if (originalSavedHosts != null) {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
} else {
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
Settings.setAutoConnectHost(origHost);
|
||||
Settings.setAutoConnectPort(origPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage exports to and loads from connections.ini with full fidelity")
|
||||
public void testExportAndLoadConnectionsIniRoundtrip() throws IOException {
|
||||
List<SavedHost> hosts = new ArrayList<>();
|
||||
SavedHost h1 = new SavedHost("id-mainframe-1", "Production TSO", "mainframe.corp.com", 23, 4, 62, 160,
|
||||
"LU01", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
SavedHost h2 = new SavedHost("id-vm-2", "VM CMS Test", "vmcms.lab.net", 992, 2, 24, 80,
|
||||
"LUCMS", false, "CMS", true, false, true, "GDDM", "1047", "SOCKS5", "10.0.0.1", 1080, "proxyuser", "proxypass");
|
||||
hosts.add(h1);
|
||||
hosts.add(h2);
|
||||
|
||||
File tmpIni = File.createTempFile("connections_test", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
HostStorage.exportToIniFile(tmpIni, hosts);
|
||||
|
||||
assertTrue(tmpIni.exists());
|
||||
String content = new String(Files.readAllBytes(tmpIni.toPath()));
|
||||
assertTrue(content.contains("[Production TSO]"), "Must contain section header [Production TSO]");
|
||||
assertTrue(content.contains("[VM CMS Test]"), "Must contain section header [VM CMS Test]");
|
||||
assertTrue(content.contains("host = mainframe.corp.com"));
|
||||
assertTrue(content.contains("proxyHost = 10.0.0.1"));
|
||||
assertTrue(content.contains("proxyType = SOCKS5"));
|
||||
|
||||
List<SavedHost> loaded = HostStorage.loadFromIniFile(tmpIni);
|
||||
assertEquals(2, loaded.size(), "Should load 2 connections from INI");
|
||||
|
||||
SavedHost loaded1 = loaded.get(0);
|
||||
assertEquals("id-mainframe-1", loaded1.getId());
|
||||
assertEquals("Production TSO", loaded1.getName());
|
||||
assertEquals("mainframe.corp.com", loaded1.getHost());
|
||||
assertEquals(23, loaded1.getPort());
|
||||
assertEquals(4, loaded1.getModel());
|
||||
assertEquals(62, loaded1.getDynamicRows());
|
||||
assertEquals(160, loaded1.getDynamicCols());
|
||||
assertEquals("LU01", loaded1.getLuName());
|
||||
assertTrue(loaded1.isAutoConnect());
|
||||
assertEquals("TSO", loaded1.getHostType());
|
||||
assertFalse(loaded1.isUseTls());
|
||||
assertTrue(loaded1.isTlsVerifyCert());
|
||||
|
||||
SavedHost loaded2 = loaded.get(1);
|
||||
assertEquals("id-vm-2", loaded2.getId());
|
||||
assertEquals("VM CMS Test", loaded2.getName());
|
||||
assertEquals("vmcms.lab.net", loaded2.getHost());
|
||||
assertEquals(992, loaded2.getPort());
|
||||
assertEquals(2, loaded2.getModel());
|
||||
assertEquals(24, loaded2.getDynamicRows());
|
||||
assertEquals(80, loaded2.getDynamicCols());
|
||||
assertEquals("LUCMS", loaded2.getLuName());
|
||||
assertFalse(loaded2.isAutoConnect());
|
||||
assertEquals("CMS", loaded2.getHostType());
|
||||
assertTrue(loaded2.isUseTls());
|
||||
assertFalse(loaded2.isTlsVerifyCert());
|
||||
assertEquals("SOCKS5", loaded2.getProxyType());
|
||||
assertEquals("10.0.0.1", loaded2.getProxyHost());
|
||||
assertEquals(1080, loaded2.getProxyPort());
|
||||
assertEquals("proxyuser", loaded2.getProxyUsername());
|
||||
assertEquals("proxypass", loaded2.getProxyPassword());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage importFromIniFile merges and updates existing hosts by name or id")
|
||||
public void testImportFromIniFileMergesAndUpdates() throws IOException {
|
||||
SavedHost initial = new SavedHost("id-100", "Mainframe Prod", "old.host.org", 23, 4, 62, 160,
|
||||
"", false, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
List<SavedHost> initList = new ArrayList<>();
|
||||
initList.add(initial);
|
||||
HostStorage.saveAll(initList);
|
||||
|
||||
File tmpIni = File.createTempFile("connections_merge", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
// Update Mainframe Prod with new host address
|
||||
w.write("[Mainframe Prod]\n");
|
||||
w.write("id = id-100\n");
|
||||
w.write("host = new.host.org\n");
|
||||
w.write("port = 2323\n");
|
||||
w.write("\n");
|
||||
// Add a second connection
|
||||
w.write("[Development CICS]\n");
|
||||
w.write("host = cics.dev.org\n");
|
||||
w.write("port = 992\n");
|
||||
w.write("hostType = CICS\n");
|
||||
w.write("useTls = true\n");
|
||||
}
|
||||
|
||||
List<SavedHost> result = HostStorage.importFromIniFile(tmpIni);
|
||||
assertEquals(2, result.size(), "Import should result in 2 saved hosts (1 updated, 1 added)");
|
||||
|
||||
SavedHost updated = HostStorage.findById("id-100");
|
||||
assertNotNull(updated);
|
||||
assertEquals("new.host.org", updated.getHost());
|
||||
assertEquals(2323, updated.getPort());
|
||||
|
||||
SavedHost added = HostStorage.findByName("Development CICS");
|
||||
assertNotNull(added);
|
||||
assertEquals("cics.dev.org", added.getHost());
|
||||
assertEquals(992, added.getPort());
|
||||
assertEquals("CICS", added.getHostType());
|
||||
assertTrue(added.isUseTls());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage parses INI section prefixes and aliases properly")
|
||||
public void testImportHandlesMissingFieldsAndAliases() throws IOException {
|
||||
File tmpIni = File.createTempFile("connections_alias", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
w.write("[connection:Aliased Host]\n");
|
||||
w.write("hostname = test.alias.net\n");
|
||||
w.write("port = 1023\n");
|
||||
w.write("terminalModel = 2\n");
|
||||
w.write("type = CMS\n");
|
||||
w.write("tls = true\n");
|
||||
w.write("verifyCert = false\n");
|
||||
w.write("charset = 1047\n");
|
||||
w.write("rows = 24\n");
|
||||
w.write("cols = 80\n");
|
||||
w.write("lu = LUVM\n");
|
||||
}
|
||||
|
||||
List<SavedHost> parsed = HostStorage.loadFromIniFile(tmpIni);
|
||||
assertEquals(1, parsed.size());
|
||||
|
||||
SavedHost h = parsed.get(0);
|
||||
assertEquals("Aliased Host", h.getName());
|
||||
assertEquals("test.alias.net", h.getHost());
|
||||
assertEquals(1023, h.getPort());
|
||||
assertEquals(2, h.getModel());
|
||||
assertEquals("CMS", h.getHostType());
|
||||
assertTrue(h.isUseTls());
|
||||
assertFalse(h.isTlsVerifyCert());
|
||||
assertEquals("1047", h.getCodePage());
|
||||
assertEquals(24, h.getDynamicRows());
|
||||
assertEquals(80, h.getDynamicCols());
|
||||
assertEquals("LUVM", h.getLuName());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog contains Import Connections and Export Connections buttons and is taller")
|
||||
public void testSettingsDialogConnectionButtonsAndHeight() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
JButton btnImportConn = dialog.getBtnImportConnections();
|
||||
JButton btnExportConn = dialog.getBtnExportConnections();
|
||||
|
||||
assertNotNull(btnImportConn, "Import Connections button must exist in SettingsDialog");
|
||||
assertNotNull(btnExportConn, "Export Connections button must exist in SettingsDialog");
|
||||
|
||||
assertEquals("Import Connections...", btnImportConn.getText());
|
||||
assertEquals("Export Connections...", btnExportConn.getText());
|
||||
|
||||
// Check window height: must be taller than previous 540 to accommodate extra buttons
|
||||
assertTrue(dialog.getHeight() > 540,
|
||||
"Settings window height (" + dialog.getHeight() + ") must be taller than 540 to accommodate the extra buttons");
|
||||
|
||||
// Verify config buttons share container row1, and connection buttons share container row2
|
||||
JButton btnImport = dialog.getBtnImport();
|
||||
JButton btnExport = dialog.getBtnExport();
|
||||
assertNotNull(btnImport);
|
||||
assertNotNull(btnExport);
|
||||
|
||||
Container configRow = btnImport.getParent();
|
||||
assertNotNull(configRow);
|
||||
assertSame(configRow, btnExport.getParent(), "btnImport and btnExport must share the same parent row");
|
||||
|
||||
Container connRow = btnImportConn.getParent();
|
||||
assertNotNull(connRow);
|
||||
assertSame(connRow, btnExportConn.getParent(), "btnImportConn and btnExportConn must share the same parent row");
|
||||
|
||||
assertNotSame(configRow, connRow, "Connection buttons must be below and NOT in line with config buttons");
|
||||
|
||||
// Verify connRow is positioned below configRow in buttonPanel
|
||||
Container buttonPanel = configRow.getParent();
|
||||
assertNotNull(buttonPanel);
|
||||
assertSame(buttonPanel, connRow.getParent(), "Both rows must be in buttonPanel");
|
||||
|
||||
int configRowIdx = -1;
|
||||
int connRowIdx = -1;
|
||||
Component[] rows = buttonPanel.getComponents();
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
if (rows[i] == configRow) configRowIdx = i;
|
||||
if (rows[i] == connRow) connRowIdx = i;
|
||||
}
|
||||
assertTrue(configRowIdx >= 0, "configRow must be in buttonPanel");
|
||||
assertTrue(connRowIdx >= 0, "connRow must be in buttonPanel");
|
||||
assertTrue(connRowIdx > configRowIdx, "connRow must be below configRow in buttonPanel (" + connRowIdx + " > " + configRowIdx + ")");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog importConnectionsFile updates savedHostBox and Settings")
|
||||
public void testSettingsDialogImportConnectionsFileUpdatesDropdownAndSettings() throws IOException {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
File tmpIni = File.createTempFile("settings_dialog_import_test", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
w.write("[Auto Mainframe]\n");
|
||||
w.write("host = auto.mainframe.org\n");
|
||||
w.write("port = 992\n");
|
||||
w.write("autoConnect = true\n");
|
||||
w.write("useTls = true\n");
|
||||
}
|
||||
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
dialog.importConnectionsFile(tmpIni);
|
||||
|
||||
// Check savedHostBox updated
|
||||
JComboBox<SavedHost> box = dialog.getSavedHostBox();
|
||||
assertNotNull(box);
|
||||
assertTrue(box.getItemCount() >= 1);
|
||||
|
||||
SavedHost sel = (SavedHost) box.getSelectedItem();
|
||||
assertNotNull(sel);
|
||||
assertEquals("auto.mainframe.org", sel.getHost());
|
||||
assertTrue(sel.isAutoConnect());
|
||||
|
||||
// Check Settings updated for autoConnect host
|
||||
assertEquals("auto.mainframe.org", Settings.getAutoConnectHost());
|
||||
assertEquals(992, Settings.getAutoConnectPort());
|
||||
assertTrue(Settings.getAutoConnectTls());
|
||||
|
||||
// Test exportConnectionsFile
|
||||
File exportTarget = File.createTempFile("settings_dialog_export_test", ".ini");
|
||||
exportTarget.deleteOnExit();
|
||||
dialog.exportConnectionsFile(exportTarget);
|
||||
assertTrue(exportTarget.exists());
|
||||
String exported = new String(Files.readAllBytes(exportTarget.toPath()));
|
||||
assertTrue(exported.contains("auto.mainframe.org"));
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog importConnectionsFile throws FileNotFoundException for nonexistent file")
|
||||
public void testImportNonExistentConnectionsFileThrows() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
File nonExistent = new File("/path/to/nonexistent/connections_" + System.currentTimeMillis() + ".ini");
|
||||
assertThrows(FileNotFoundException.class, () -> {
|
||||
dialog.importConnectionsFile(nonExistent);
|
||||
});
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Frame;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class HostStorageAndProxyTest {
|
||||
|
||||
private List<SavedHost> originalHosts;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalHosts = HostStorage.getSavedHosts();
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
HostStorage.clearAll();
|
||||
HostStorage.saveAll(originalHosts);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost correctly models per-connection proxy and converts to ConnectionConfig")
|
||||
public void testSavedHostProxyToConnectionConfig() {
|
||||
// 1. Host with SOCKS5 proxy and auth
|
||||
SavedHost host1 = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Prod Mainframe",
|
||||
"mvs.corp.local",
|
||||
2323,
|
||||
4,
|
||||
43,
|
||||
80,
|
||||
"TSO01",
|
||||
false,
|
||||
"TSO",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"037",
|
||||
"SOCKS5",
|
||||
"proxy.internal.corp",
|
||||
1080,
|
||||
"admin",
|
||||
"secret123"
|
||||
);
|
||||
|
||||
ConnectionConfig cfg1 = host1.toConnectionConfig();
|
||||
assertEquals("mvs.corp.local", cfg1.getHost());
|
||||
assertEquals(2323, cfg1.getPort());
|
||||
assertTrue(cfg1.isUseTls());
|
||||
assertEquals("TSO01", cfg1.getLuName());
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS5, cfg1.getProxyType());
|
||||
assertEquals("proxy.internal.corp", cfg1.getProxyHost());
|
||||
assertEquals(1080, cfg1.getProxyPort());
|
||||
assertEquals("admin", cfg1.getProxyUsername());
|
||||
assertEquals("secret123", cfg1.getProxyPassword());
|
||||
|
||||
// 2. Host with direct connection (NONE proxy)
|
||||
SavedHost host2 = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Local VM370",
|
||||
"127.0.0.1",
|
||||
23,
|
||||
2,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
false,
|
||||
"CMS",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
"BOTH",
|
||||
"037",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
|
||||
ConnectionConfig cfg2 = host2.toConnectionConfig();
|
||||
assertEquals("127.0.0.1", cfg2.getHost());
|
||||
assertEquals(23, cfg2.getPort());
|
||||
assertFalse(cfg2.isUseTls());
|
||||
assertEquals(ConnectionConfig.ProxyType.NONE, cfg2.getProxyType());
|
||||
assertNull(cfg2.getProxyHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage persists and restores multiple host profiles with isolated per-connection proxies")
|
||||
public void testHostStoragePersistenceAndIsolation() {
|
||||
SavedHost h1 = new SavedHost(
|
||||
"id-1",
|
||||
"System A (SOCKS5)",
|
||||
"sysa.corp",
|
||||
23,
|
||||
4,
|
||||
62,
|
||||
160,
|
||||
"",
|
||||
false,
|
||||
"TSO",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"037",
|
||||
"SOCKS5",
|
||||
"socks.gateway.net",
|
||||
1080,
|
||||
"alice",
|
||||
"passA"
|
||||
);
|
||||
|
||||
SavedHost h2 = new SavedHost(
|
||||
"id-2",
|
||||
"System B (HTTP CONNECT)",
|
||||
"sysb.dmz",
|
||||
992,
|
||||
5,
|
||||
27,
|
||||
132,
|
||||
"LU99",
|
||||
false,
|
||||
"TSO",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"1047",
|
||||
"HTTP",
|
||||
"squid.proxy.org",
|
||||
3128,
|
||||
"bob",
|
||||
"passB"
|
||||
);
|
||||
|
||||
SavedHost h3 = new SavedHost(
|
||||
"id-3",
|
||||
"System C (Direct)",
|
||||
"sysc.lan",
|
||||
23,
|
||||
2,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
true, // autoConnect
|
||||
"CMS",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"500",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
|
||||
HostStorage.saveHost(h1);
|
||||
HostStorage.saveHost(h2);
|
||||
HostStorage.saveHost(h3);
|
||||
|
||||
List<SavedHost> loaded = HostStorage.getSavedHosts();
|
||||
assertEquals(3, loaded.size());
|
||||
|
||||
SavedHost loadedA = HostStorage.findById("id-1");
|
||||
assertNotNull(loadedA);
|
||||
assertEquals("System A (SOCKS5)", loadedA.getName());
|
||||
assertEquals("SOCKS5", loadedA.getProxyType());
|
||||
assertEquals("socks.gateway.net", loadedA.getProxyHost());
|
||||
assertEquals(1080, loadedA.getProxyPort());
|
||||
assertEquals("alice", loadedA.getProxyUsername());
|
||||
assertEquals("passA", loadedA.getProxyPassword());
|
||||
|
||||
SavedHost loadedB = HostStorage.findById("id-2");
|
||||
assertNotNull(loadedB);
|
||||
assertEquals("HTTP", loadedB.getProxyType());
|
||||
assertEquals("squid.proxy.org", loadedB.getProxyHost());
|
||||
assertEquals(3128, loadedB.getProxyPort());
|
||||
assertEquals("bob", loadedB.getProxyUsername());
|
||||
assertEquals("passB", loadedB.getProxyPassword());
|
||||
|
||||
SavedHost loadedC = HostStorage.findById("id-3");
|
||||
assertNotNull(loadedC);
|
||||
assertEquals("NONE", loadedC.getProxyType());
|
||||
assertEquals("", loadedC.getProxyHost());
|
||||
assertTrue(loadedC.isAutoConnect());
|
||||
|
||||
// Verify getAutoConnectHost returns System C with its own connection settings
|
||||
SavedHost auto = HostStorage.getAutoConnectHost();
|
||||
assertNotNull(auto);
|
||||
assertEquals("id-3", auto.getId());
|
||||
assertEquals(ConnectionConfig.ProxyType.NONE, auto.toConnectionConfig().getProxyType());
|
||||
|
||||
// Verify deleteHost
|
||||
HostStorage.deleteHost("id-2");
|
||||
assertEquals(2, HostStorage.getSavedHosts().size());
|
||||
assertNull(HostStorage.findById("id-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage JSON matches a3270 format and parses a3270 JSON strings")
|
||||
public void testA3270JsonFormatCompatibility() {
|
||||
// String representative of a3270 saved_hosts preference JSON
|
||||
String a3270Json = "[\n" +
|
||||
" {\n" +
|
||||
" \"id\": \"android-uuid-1\",\n" +
|
||||
" \"name\": \"Dallas Mainframe\",\n" +
|
||||
" \"host\": \"dallas.ibm.com\",\n" +
|
||||
" \"port\": 2323,\n" +
|
||||
" \"model\": 4,\n" +
|
||||
" \"dynamicRows\": 62,\n" +
|
||||
" \"dynamicCols\": 160,\n" +
|
||||
" \"luName\": \"DAL01\",\n" +
|
||||
" \"autoConnect\": false,\n" +
|
||||
" \"hostType\": \"TSO\",\n" +
|
||||
" \"useTls\": true,\n" +
|
||||
" \"tlsVerifyCert\": false,\n" +
|
||||
" \"tn3270e\": true,\n" +
|
||||
" \"graphicsMode\": \"BOTH\",\n" +
|
||||
" \"codePage\": \"037\",\n" +
|
||||
" \"proxyType\": \"SOCKS5\",\n" +
|
||||
" \"proxyHost\": \"10.20.30.40\",\n" +
|
||||
" \"proxyPort\": 9050,\n" +
|
||||
" \"proxyUsername\": \"dallasuser\",\n" +
|
||||
" \"proxyPassword\": \"dallaspass\"\n" +
|
||||
" }\n" +
|
||||
"]";
|
||||
|
||||
List<SavedHost> parsed = HostStorage.parseHostsJson(a3270Json);
|
||||
assertEquals(1, parsed.size());
|
||||
SavedHost h = parsed.get(0);
|
||||
assertEquals("android-uuid-1", h.getId());
|
||||
assertEquals("Dallas Mainframe", h.getName());
|
||||
assertEquals("dallas.ibm.com", h.getHost());
|
||||
assertEquals(2323, h.getPort());
|
||||
assertEquals(4, h.getModel());
|
||||
assertTrue(h.isUseTls());
|
||||
assertFalse(h.isTlsVerifyCert());
|
||||
assertEquals("SOCKS5", h.getProxyType());
|
||||
assertEquals("10.20.30.40", h.getProxyHost());
|
||||
assertEquals(9050, h.getProxyPort());
|
||||
assertEquals("dallasuser", h.getProxyUsername());
|
||||
assertEquals("dallaspass", h.getProxyPassword());
|
||||
|
||||
// Roundtrip serialization
|
||||
String serialized = HostStorage.serializeHostsJson(parsed);
|
||||
List<SavedHost> roundtripped = HostStorage.parseHostsJson(serialized);
|
||||
assertEquals(1, roundtripped.size());
|
||||
SavedHost r = roundtripped.get(0);
|
||||
assertEquals(h.getId(), r.getId());
|
||||
assertEquals(h.getProxyType(), r.getProxyType());
|
||||
assertEquals(h.getProxyHost(), r.getProxyHost());
|
||||
assertEquals(h.getProxyPort(), r.getProxyPort());
|
||||
assertEquals(h.getProxyUsername(), r.getProxyUsername());
|
||||
assertEquals(h.getProxyPassword(), r.getProxyPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectDialog loads profile proxy and applies per-connection proxy to ConnectionConfig")
|
||||
public void testConnectDialogProxyHandling() {
|
||||
if (!GraphicsEnvironment.isHeadless()) {
|
||||
ConnectDialog dialog = new ConnectDialog((Frame) null);
|
||||
dialog.setInitialHost("test.mainframe.com");
|
||||
dialog.setInitialPort(23);
|
||||
dialog.setInitialProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", 1080, "testuser", "testpass");
|
||||
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS5, dialog.getSelectedProxyType());
|
||||
assertEquals("127.0.0.1", dialog.getProxyHost());
|
||||
assertEquals(1080, dialog.getProxyPort());
|
||||
assertEquals("testuser", dialog.getProxyUsername());
|
||||
assertEquals("testpass", dialog.getProxyPassword());
|
||||
|
||||
// Loading a profile updates dialog proxy fields
|
||||
SavedHost profile = new SavedHost(
|
||||
"p1", "Profile 1", "prod.ibm.net", 992, 4, 43, 80, "", false,
|
||||
"TSO", true, true, true, "BOTH", "037",
|
||||
"HTTP", "proxy.corp.net", 8080, "corpuser", "corppass"
|
||||
);
|
||||
dialog.loadProfile(profile);
|
||||
|
||||
assertEquals(ConnectionConfig.ProxyType.HTTP, dialog.getSelectedProxyType());
|
||||
assertEquals("proxy.corp.net", dialog.getProxyHost());
|
||||
assertEquals(8080, dialog.getProxyPort());
|
||||
assertEquals("corpuser", dialog.getProxyUsername());
|
||||
assertEquals("corppass", dialog.getProxyPassword());
|
||||
dialog.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
|
||||
public void testSavedHostGraphicsModeSerializationRoundtrip() {
|
||||
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
|
||||
SavedHost host = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Gfx " + gm.name(),
|
||||
"test.host.com",
|
||||
23,
|
||||
4,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
false,
|
||||
"TSO",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
gm.name(),
|
||||
"037",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
String json = HostStorage.serializeHostsJson(List.of(host));
|
||||
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
|
||||
assertEquals(1, parsed.size());
|
||||
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
|
||||
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
|
||||
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
|
||||
"ConnectionConfig must reflect GraphicsMode." + gm.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class AutoConnectSettingsAndTerminalCenteringTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private Settings.StartupBehavior originalStartupBehavior;
|
||||
private String originalAutoConnectHost;
|
||||
private int originalAutoConnectPort;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalSavedHosts = new ArrayList<>(HostStorage.getSavedHosts());
|
||||
originalStartupBehavior = Settings.getStartupBehavior();
|
||||
originalAutoConnectHost = Settings.getAutoConnectHost();
|
||||
originalAutoConnectPort = Settings.getAutoConnectPort();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
Settings.setStartupBehavior(originalStartupBehavior);
|
||||
Settings.setAutoConnectHost(originalAutoConnectHost);
|
||||
Settings.setAutoConnectPort(originalAutoConnectPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TerminalPanel centering: left bound is flush when constrained, centered with left <= right when too big")
|
||||
public void testTerminalPanelCenteringLogic() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
panel.setClient(client);
|
||||
|
||||
int cols = sb.getDisplayCols(); // 80
|
||||
int rows = sb.getDisplayRows(); // 24
|
||||
int gridW = cols * panel.cellWidth; // 800
|
||||
int gridH = rows * panel.cellHeight; // 480
|
||||
|
||||
// 1. Panel is smaller than grid: left bound must be 0 (flush left cells), vertical must be 0
|
||||
panel.setSize(gridW - 100, gridH - 50);
|
||||
assertEquals(0, panel.getRenderOffsetX(), "Left bound should be 0 when window width is constrained");
|
||||
assertEquals(0, panel.getRenderOffsetY(), "Top bound should be 0 when window height is constrained");
|
||||
|
||||
// 2. Panel exactly matches grid: offset must be 0
|
||||
panel.setSize(gridW, gridH);
|
||||
assertEquals(0, panel.getRenderOffsetX(), "Offset X must be 0 when width exactly matches grid");
|
||||
assertEquals(0, panel.getRenderOffsetY(), "Offset Y must be 0 when height exactly matches grid");
|
||||
|
||||
// 3. Panel is larger with odd extra (extra = 7)
|
||||
panel.setSize(gridW + 7, gridH + 11);
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int leftSpace = ox;
|
||||
int rightSpace = (gridW + 7) - (ox + gridW);
|
||||
assertTrue(leftSpace <= rightSpace, "Left blank space (" + leftSpace + ") must be <= right space (" + rightSpace + ")");
|
||||
assertEquals(3, leftSpace);
|
||||
assertEquals(4, rightSpace);
|
||||
|
||||
int oy = panel.getRenderOffsetY();
|
||||
int topSpace = oy;
|
||||
int bottomSpace = (gridH + 11) - (oy + gridH);
|
||||
assertTrue(topSpace <= bottomSpace, "Top space must be <= bottom space");
|
||||
assertEquals(5, topSpace);
|
||||
assertEquals(6, bottomSpace);
|
||||
|
||||
// 4. Panel is larger with even extra (extra = 6)
|
||||
panel.setSize(gridW + 6, gridH + 8);
|
||||
assertEquals(3, panel.getRenderOffsetX());
|
||||
assertEquals(4, panel.getRenderOffsetY());
|
||||
|
||||
panel.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("guardedPack does not resize window when window is too small")
|
||||
public void testGuardedPackDoesNotResizeWhenTooSmall() {
|
||||
try {
|
||||
JFrame frame = new JFrame();
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
panel.setClient(client);
|
||||
|
||||
frame.getContentPane().add(panel);
|
||||
frame.setSize(400, 300);
|
||||
frame.addNotify(); // makes frame displayable
|
||||
|
||||
// Set size explicitly smaller than preferred size (800+ x 480+)
|
||||
frame.setSize(400, 300);
|
||||
Dimension sizeBefore = frame.getSize();
|
||||
|
||||
// Run guardedPack
|
||||
panel.guardedPack();
|
||||
|
||||
// Window should not have expanded
|
||||
Dimension sizeAfter = frame.getSize();
|
||||
assertEquals(sizeBefore.width, sizeAfter.width, "Window width should NOT be enlarged when too small");
|
||||
assertEquals(sizeBefore.height, sizeAfter.height, "Window height should NOT be enlarged when too small");
|
||||
|
||||
frame.dispose();
|
||||
panel.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
// Environment without X display
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog Startup & Auto-Connect presents saved connections dropdown and updates auto-connect host")
|
||||
public void testSettingsDialogSavedConnectionsDropdown() throws Exception {
|
||||
SavedHost host1 = new SavedHost("id-test-1", "Host Alpha", "alpha.mainframe.test", 23, 2, 24, 80, "LU1", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
SavedHost host2 = new SavedHost("id-test-2", "Host Beta", "beta.mainframe.test", 992, 4, 43, 80, "LU2", false, "VM", true, false, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
|
||||
List<SavedHost> testHosts = new ArrayList<>();
|
||||
testHosts.add(host1);
|
||||
testHosts.add(host2);
|
||||
HostStorage.saveAll(testHosts);
|
||||
Settings.setStartupBehavior(Settings.StartupBehavior.AUTO_CONNECT);
|
||||
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
JComboBox<SavedHost> combo = dialog.getSavedHostBox();
|
||||
assertNotNull(combo, "savedHostBox should not be null");
|
||||
assertEquals(2, combo.getItemCount(), "Dropdown should contain all saved hosts");
|
||||
|
||||
// Host Alpha was autoConnect=true, so it should be initially selected
|
||||
SavedHost initialSel = (SavedHost) combo.getSelectedItem();
|
||||
assertNotNull(initialSel);
|
||||
assertEquals("id-test-1", initialSel.getId());
|
||||
|
||||
// Switch to Host Beta
|
||||
combo.setSelectedIndex(1);
|
||||
|
||||
// Trigger applySettings via reflection
|
||||
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
|
||||
applyMethod.setAccessible(true);
|
||||
applyMethod.invoke(dialog);
|
||||
|
||||
// Verify Host Beta is now the auto-connect host
|
||||
SavedHost auto = HostStorage.getAutoConnectHost();
|
||||
assertNotNull(auto, "Auto-connect host should exist");
|
||||
assertEquals("id-test-2", auto.getId());
|
||||
assertEquals("Host Beta", auto.getName());
|
||||
assertEquals("beta.mainframe.test", auto.getHost());
|
||||
assertEquals(992, auto.getPort());
|
||||
|
||||
SavedHost loadedAlpha = HostStorage.findById("id-test-1");
|
||||
assertNotNull(loadedAlpha);
|
||||
assertFalse(loadedAlpha.isAutoConnect(), "Host Alpha should no longer have autoConnect = true");
|
||||
|
||||
// Verify Settings autoConnectHost/Port synchronized
|
||||
assertEquals("beta.mainframe.test", Settings.getAutoConnectHost());
|
||||
assertEquals(992, Settings.getAutoConnectPort());
|
||||
|
||||
// Switch startup behavior to SHOW_CONNECT and apply
|
||||
Field sbField = SettingsDialog.class.getDeclaredField("startupBehaviorBox");
|
||||
sbField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
JComboBox<Settings.StartupBehavior> sbBox = (JComboBox<Settings.StartupBehavior>) sbField.get(dialog);
|
||||
sbBox.setSelectedItem(Settings.StartupBehavior.SHOW_CONNECT);
|
||||
|
||||
applyMethod.invoke(dialog);
|
||||
assertNull(HostStorage.getAutoConnectHost(), "When startup behavior is SHOW_CONNECT, auto-connect host should be cleared");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} finally {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Window size is adjusted once on connect for selected model and left untouched on onScreenSizeChanged")
|
||||
public void testWindowSizedOnceForModelAndNotOnScreenSizeChanged() {
|
||||
try {
|
||||
JFrame frame = new JFrame();
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
frame.getContentPane().add(panel);
|
||||
frame.addNotify();
|
||||
|
||||
// Pack for Model 4 (43 rows, 80 cols)
|
||||
panel.packForModel(43, 80);
|
||||
Dimension model4Size = frame.getSize();
|
||||
assertTrue(model4Size.height >= 43 * 20, "Height should accommodate 43 rows");
|
||||
assertTrue(model4Size.width >= 80 * 10, "Width should accommodate 80 cols");
|
||||
|
||||
// Pack for Model 5 (27 rows, 132 cols)
|
||||
panel.packForModel(27, 132);
|
||||
Dimension model5Size = frame.getSize();
|
||||
assertTrue(model5Size.width >= 132 * 10, "Width should accommodate 132 cols");
|
||||
|
||||
// Set custom size and verify guardedPack leaves the window alone
|
||||
frame.setSize(600, 400);
|
||||
Dimension fixedSize = frame.getSize();
|
||||
|
||||
panel.guardedPack();
|
||||
assertEquals(fixedSize.width, frame.getWidth());
|
||||
assertEquals(fixedSize.height, frame.getHeight());
|
||||
|
||||
frame.dispose();
|
||||
panel.dispose();
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class BehaviorSettingsTest {
|
||||
|
||||
private boolean origAutoReconnect;
|
||||
private int origReconnectMaxRetries;
|
||||
private boolean origInputMask;
|
||||
private String origInputMaskChar;
|
||||
private boolean origBlockPaste;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
|
||||
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
|
||||
origInputMask = Settings.getInputMask();
|
||||
origInputMaskChar = Settings.getInputMaskChar();
|
||||
origBlockPaste = Settings.getBlockPaste();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
|
||||
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
|
||||
Settings.setInputMask(origInputMask);
|
||||
Settings.setInputMaskChar(origInputMaskChar);
|
||||
Settings.setBlockPaste(origBlockPaste);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Auto-reconnect settings get/set and alias consistency")
|
||||
public void testAutoReconnectSettingsPersistence() {
|
||||
Settings.setAutoConnectAutoReconnect(true);
|
||||
assertTrue(Settings.getAutoConnectAutoReconnect());
|
||||
assertTrue(Settings.getAutoReconnect());
|
||||
|
||||
Settings.setAutoReconnect(false);
|
||||
assertFalse(Settings.getAutoConnectAutoReconnect());
|
||||
assertFalse(Settings.getAutoReconnect());
|
||||
|
||||
Settings.setAutoConnectReconnectMaxRetries(8);
|
||||
assertEquals(8, Settings.getAutoConnectReconnectMaxRetries());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Input mask settings get/set and defaults")
|
||||
public void testInputMaskSettingsPersistence() {
|
||||
Settings.setInputMask(false);
|
||||
assertFalse(Settings.getInputMask());
|
||||
assertFalse(Settings.getInputMaskEnabled());
|
||||
|
||||
Settings.setInputMaskEnabled(true);
|
||||
assertTrue(Settings.getInputMask());
|
||||
assertTrue(Settings.getInputMaskEnabled());
|
||||
|
||||
Settings.setInputMaskChar("#");
|
||||
assertEquals("#", Settings.getInputMaskChar());
|
||||
|
||||
Settings.setInputMaskChar("*");
|
||||
assertEquals("*", Settings.getInputMaskChar());
|
||||
|
||||
// Empty string should fall back to '*'
|
||||
Settings.setInputMaskChar("");
|
||||
assertEquals("*", Settings.getInputMaskChar());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Block paste setting defaults to true and binds to SettingsDialog")
|
||||
public void testBlockPasteSettingAndDialogBinding() throws Exception {
|
||||
// Default must be true
|
||||
assertTrue(Settings.getBlockPaste(), "Settings.getBlockPaste() should default to true");
|
||||
|
||||
Settings.setBlockPaste(false);
|
||||
assertFalse(Settings.getBlockPaste());
|
||||
Settings.setBlockPaste(true);
|
||||
assertTrue(Settings.getBlockPaste());
|
||||
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
Field blockPasteField = SettingsDialog.class.getDeclaredField("blockPasteCheck");
|
||||
blockPasteField.setAccessible(true);
|
||||
JCheckBox blockPasteCheck = (JCheckBox) blockPasteField.get(dialog);
|
||||
assertNotNull(blockPasteCheck, "blockPasteCheck must exist in SettingsDialog");
|
||||
assertEquals("Block paste", blockPasteCheck.getText());
|
||||
assertEquals(Settings.getBlockPaste(), blockPasteCheck.isSelected());
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("INI export and load preserves autoReconnect and inputMask")
|
||||
public void testIniExportAndLoad() throws Exception {
|
||||
Settings.setAutoConnectAutoReconnect(true);
|
||||
Settings.setAutoConnectReconnectMaxRetries(12);
|
||||
Settings.setInputMask(false);
|
||||
Settings.setInputMaskChar("@");
|
||||
|
||||
File tempFile = File.createTempFile("j3270_behavior_test", ".ini");
|
||||
tempFile.deleteOnExit();
|
||||
|
||||
Settings.exportToIniFile(tempFile.getAbsolutePath());
|
||||
|
||||
// Reset to different values
|
||||
Settings.setAutoConnectAutoReconnect(false);
|
||||
Settings.setAutoConnectReconnectMaxRetries(3);
|
||||
Settings.setInputMask(true);
|
||||
Settings.setInputMaskChar("*");
|
||||
|
||||
// Load back from INI
|
||||
Settings.loadFromIniFile(tempFile.getAbsolutePath());
|
||||
|
||||
assertTrue(Settings.getAutoConnectAutoReconnect());
|
||||
assertEquals(12, Settings.getAutoConnectReconnectMaxRetries());
|
||||
assertFalse(Settings.getInputMask());
|
||||
assertEquals("@", Settings.getInputMaskChar());
|
||||
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TerminalPanel reloads input mask settings correctly")
|
||||
public void testTerminalPanelInputMaskReload() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
Settings.setInputMask(false);
|
||||
Settings.setInputMaskChar("$");
|
||||
panel.reloadSettings();
|
||||
|
||||
assertFalse(panel.isInputMaskEnabled());
|
||||
assertEquals("$", panel.getInputMaskChar());
|
||||
|
||||
panel.setInputMaskEnabled(true);
|
||||
assertTrue(panel.isInputMaskEnabled());
|
||||
|
||||
panel.setInputMaskChar("#");
|
||||
assertEquals("#", panel.getInputMaskChar());
|
||||
} catch (HeadlessException ignored) {
|
||||
// Safe fallback for headless runner
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog contains Auto-Reconnect checkbox and Input Mask controls in Behavior panel")
|
||||
public void testSettingsDialogBehaviorControls() throws Exception {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
// In automated/headless environments, JFrame cannot be initialized
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
// Access private fields in SettingsDialog to verify component bindings
|
||||
Field autoRecField = SettingsDialog.class.getDeclaredField("autoReconnectCheck");
|
||||
autoRecField.setAccessible(true);
|
||||
JCheckBox autoReconnectCheck = (JCheckBox) autoRecField.get(dialog);
|
||||
assertNotNull(autoReconnectCheck, "autoReconnectCheck must exist in SettingsDialog");
|
||||
assertEquals("Auto-Reconnect on Disconnect", autoReconnectCheck.getText());
|
||||
assertEquals(Settings.getAutoConnectAutoReconnect(), autoReconnectCheck.isSelected());
|
||||
|
||||
Field inputMaskCheckField = SettingsDialog.class.getDeclaredField("inputMaskCheck");
|
||||
inputMaskCheckField.setAccessible(true);
|
||||
JCheckBox inputMaskCheck = (JCheckBox) inputMaskCheckField.get(dialog);
|
||||
assertNotNull(inputMaskCheck, "inputMaskCheck must exist in SettingsDialog");
|
||||
assertEquals(Settings.getInputMask(), inputMaskCheck.isSelected());
|
||||
|
||||
Field inputMaskCharField = SettingsDialog.class.getDeclaredField("inputMaskCharField");
|
||||
inputMaskCharField.setAccessible(true);
|
||||
JTextField maskCharField = (JTextField) inputMaskCharField.get(dialog);
|
||||
assertNotNull(maskCharField, "inputMaskCharField must exist in SettingsDialog");
|
||||
assertEquals(Settings.getInputMaskChar(), maskCharField.getText());
|
||||
assertEquals(inputMaskCheck.isSelected(), maskCharField.isEnabled());
|
||||
|
||||
// Test interaction: unchecking inputMask disables character field
|
||||
inputMaskCheck.setSelected(false);
|
||||
for (java.awt.event.ActionListener al : inputMaskCheck.getActionListeners()) {
|
||||
al.actionPerformed(new java.awt.event.ActionEvent(inputMaskCheck, java.awt.event.ActionEvent.ACTION_PERFORMED, ""));
|
||||
}
|
||||
assertFalse(maskCharField.isEnabled());
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog tabs are organized into Appearance, Behavior, Connection, and Entry Assist")
|
||||
public void testSettingsDialogReorganizedTabsAndGrouping() throws Exception {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
// Find top-level JTabbedPane
|
||||
JTabbedPane mainTabs = null;
|
||||
for (Component c : dialog.getContentPane().getComponents()) {
|
||||
if (c instanceof JTabbedPane) {
|
||||
mainTabs = (JTabbedPane) c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(mainTabs, "Main JTabbedPane must exist");
|
||||
assertEquals(4, mainTabs.getTabCount(), "Must have 4 main tabs");
|
||||
assertEquals("Appearance", mainTabs.getTitleAt(0));
|
||||
assertEquals("Behavior", mainTabs.getTitleAt(1));
|
||||
assertEquals("Connection", mainTabs.getTitleAt(2));
|
||||
assertEquals("Entry Assist", mainTabs.getTitleAt(3));
|
||||
|
||||
// Verify Appearance subtabs: Display & Font, Colors
|
||||
Component appComp = mainTabs.getComponentAt(0);
|
||||
assertTrue(appComp instanceof JPanel);
|
||||
JTabbedPane appTabs = findTabbedPane((JPanel) appComp);
|
||||
assertNotNull(appTabs, "Appearance panel must have subtabs");
|
||||
assertEquals(2, appTabs.getTabCount());
|
||||
assertEquals("Display & Font", appTabs.getTitleAt(0));
|
||||
assertEquals("Colors", appTabs.getTitleAt(1));
|
||||
|
||||
// Verify Behavior subtabs: General, Keymap
|
||||
Component behComp = mainTabs.getComponentAt(1);
|
||||
assertTrue(behComp instanceof JPanel);
|
||||
JTabbedPane behTabs = findTabbedPane((JPanel) behComp);
|
||||
assertNotNull(behTabs, "Behavior panel must have subtabs");
|
||||
assertEquals(2, behTabs.getTabCount());
|
||||
assertEquals("General", behTabs.getTitleAt(0));
|
||||
assertEquals("Keymap", behTabs.getTitleAt(1));
|
||||
|
||||
// Verify controls exist in SettingsDialog
|
||||
Field cursorStyleField = SettingsDialog.class.getDeclaredField("cursorStyleBox");
|
||||
cursorStyleField.setAccessible(true);
|
||||
assertNotNull(cursorStyleField.get(dialog), "cursorStyleBox must exist");
|
||||
|
||||
Field crosshairField = SettingsDialog.class.getDeclaredField("crosshairRulerCheck");
|
||||
crosshairField.setAccessible(true);
|
||||
assertNotNull(crosshairField.get(dialog), "crosshairRulerCheck must exist");
|
||||
|
||||
Field fourColorField = SettingsDialog.class.getDeclaredField("fourColorOverrideCheck");
|
||||
fourColorField.setAccessible(true);
|
||||
assertNotNull(fourColorField.get(dialog), "fourColorOverrideCheck must exist");
|
||||
|
||||
Field numericLockField = SettingsDialog.class.getDeclaredField("numericFieldLockCheck");
|
||||
numericLockField.setAccessible(true);
|
||||
assertNotNull(numericLockField.get(dialog), "numericFieldLockCheck must exist");
|
||||
|
||||
Field autoSkipField = SettingsDialog.class.getDeclaredField("autoSkipCheck");
|
||||
autoSkipField.setAccessible(true);
|
||||
assertNotNull(autoSkipField.get(dialog), "autoSkipCheck must exist");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static JTabbedPane findTabbedPane(Container container) {
|
||||
for (Component c : container.getComponents()) {
|
||||
if (c instanceof JTabbedPane) {
|
||||
return (JTabbedPane) c;
|
||||
}
|
||||
if (c instanceof Container) {
|
||||
JTabbedPane nested = findTabbedPane((Container) c);
|
||||
if (nested != null) return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.InputMethodEvent;
|
||||
import java.text.AttributedString;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class DBCSInputAndClipboardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Clipboard selection skips DB_RIGHT cells to avoid duplicating wide characters")
|
||||
public void testDBCSClipboardCopyNoDoubling() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'A' at cell 2
|
||||
ExtendedAttribute cell0 = sb.getCell(0);
|
||||
cell0.db = ExtendedAttribute.DB_LEFT;
|
||||
cell0.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell1 = sb.getCell(1);
|
||||
cell1.db = ExtendedAttribute.DB_RIGHT;
|
||||
cell1.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell2 = sb.getCell(2);
|
||||
cell2.db = ExtendedAttribute.DB_NONE;
|
||||
cell2.ucs4 = 'A';
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
// Stream selection covering cells 0..2 (row 0, col 0..2)
|
||||
panel.select(0, 0, 0, 2);
|
||||
String selected = panel.getSelectedText();
|
||||
|
||||
assertEquals("\u3000A", selected, "Stream selection must not duplicate the DBCS character");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Block selection skips DB_RIGHT cells to avoid duplicating wide characters")
|
||||
public void testDBCSBlockSelectionNoDoubling() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
panel.setBlockSelectMode(true);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'B' at cell 2
|
||||
ExtendedAttribute cell0 = sb.getCell(0);
|
||||
cell0.db = ExtendedAttribute.DB_LEFT;
|
||||
cell0.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell1 = sb.getCell(1);
|
||||
cell1.db = ExtendedAttribute.DB_RIGHT;
|
||||
cell1.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell2 = sb.getCell(2);
|
||||
cell2.db = ExtendedAttribute.DB_NONE;
|
||||
cell2.ucs4 = 'B';
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
panel.select(0, 0, 0, 2);
|
||||
String selected = panel.getSelectedText();
|
||||
|
||||
assertEquals("\u3000B", selected, "Block selection must not duplicate the DBCS character");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InputMethodRequests returns cursor bounds and processes committed text")
|
||||
public void testDBCSInputMethodRequestsAndEvents() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
assertNotNull(panel.getInputMethodRequests(), "getInputMethodRequests must return this");
|
||||
|
||||
Rectangle loc = panel.getTextLocation(null);
|
||||
assertNotNull(loc, "Text location must not be null");
|
||||
assertTrue(loc.width > 0, "Cursor width must be positive");
|
||||
assertTrue(loc.height > 0, "Cursor height must be positive");
|
||||
|
||||
// Simulate committed IME text '\u3000'
|
||||
AttributedString as = new AttributedString("\u3000");
|
||||
InputMethodEvent imeEvent = new InputMethodEvent(panel, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
|
||||
as.getIterator(), 1, null, null);
|
||||
panel.inputMethodTextChanged(imeEvent);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
|
||||
assertEquals('\u3000', sb.getCell(0).ucs4);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
|
||||
assertEquals('\u3000', sb.getCell(1).ucs4);
|
||||
assertEquals(2, sb.getCursorAddress(), "Cursor should advance by 2 after typing 2-cell DBCS");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InputProcessor backspace over DBCS character deletes both cells")
|
||||
public void testInputProcessorDBCSTypingAndBackspace() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
InputProcessor input = client.getInputProcessor();
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
sb.setCursorAddress(0);
|
||||
|
||||
// Type DBCS character
|
||||
input.typeCharacter('\u3000');
|
||||
assertEquals(2, sb.getCursorAddress());
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
|
||||
|
||||
// Backspace over DBCS character
|
||||
input.backspace();
|
||||
assertEquals(0, sb.getCursorAddress(), "Cursor should back up 2 cells");
|
||||
assertEquals(0, sb.getCell(0).ucs4, "Cell 0 should be deleted (null)");
|
||||
assertEquals(0, sb.getCell(1).ucs4, "Cell 1 should be deleted (null)");
|
||||
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(0).db);
|
||||
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(1).db);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.ComponentEvent;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class FontSettingsPersistenceTest {
|
||||
|
||||
private int originalFontSize;
|
||||
private String originalFontFamily;
|
||||
private int originalUiFontSize;
|
||||
private String originalUiFontFamily;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
originalFontSize = Settings.getFontSize();
|
||||
originalFontFamily = Settings.getFontFamily();
|
||||
originalUiFontSize = Settings.getUiFontSize();
|
||||
originalUiFontFamily = Settings.getUiFontFamily();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setFontSize(originalFontSize);
|
||||
Settings.setFontFamily(originalFontFamily);
|
||||
Settings.setUiFontSize(originalUiFontSize);
|
||||
Settings.setUiFontFamily(originalUiFontFamily);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsPersistence() {
|
||||
Settings.setFontSize(22);
|
||||
Settings.setFontFamily("Monospaced");
|
||||
assertEquals(22, Settings.getFontSize());
|
||||
assertEquals("Monospaced", Settings.getFontFamily());
|
||||
|
||||
Settings.setFontSize(18);
|
||||
assertEquals(18, Settings.getFontSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUiFontSettingsIndependence() {
|
||||
// Configure terminal fonts
|
||||
Settings.setFontFamily("Courier");
|
||||
Settings.setFontSize(20);
|
||||
|
||||
// Configure UI fonts
|
||||
Settings.setUiFontFamily("Dialog");
|
||||
Settings.setUiFontSize(15);
|
||||
|
||||
// Verify independent getters
|
||||
assertEquals("Courier", Settings.getFontFamily());
|
||||
assertEquals(20, Settings.getFontSize());
|
||||
assertEquals("Dialog", Settings.getUiFontFamily());
|
||||
assertEquals(15, Settings.getUiFontSize());
|
||||
|
||||
// Change UI font and ensure terminal font is untouched
|
||||
Settings.setUiFontSize(18);
|
||||
Settings.setUiFontFamily("SansSerif");
|
||||
assertEquals("Courier", Settings.getFontFamily(), "Terminal font family must not change when UI font changes");
|
||||
assertEquals(20, Settings.getFontSize(), "Terminal font size must not change when UI font changes");
|
||||
assertEquals("SansSerif", Settings.getUiFontFamily());
|
||||
assertEquals(18, Settings.getUiFontSize());
|
||||
|
||||
// Change terminal font and ensure UI font is untouched
|
||||
Settings.setFontSize(24);
|
||||
Settings.setFontFamily("IBM 3270");
|
||||
assertEquals("SansSerif", Settings.getUiFontFamily(), "UI font family must not change when terminal font changes");
|
||||
assertEquals(18, Settings.getUiFontSize(), "UI font size must not change when terminal font changes");
|
||||
assertEquals("IBM 3270", Settings.getFontFamily());
|
||||
assertEquals(24, Settings.getFontSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUiFontIniImportExport() throws Exception {
|
||||
java.io.File tempIni = java.io.File.createTempFile("j3270_ui_font_test", ".ini");
|
||||
tempIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (java.io.PrintWriter pw = new java.io.PrintWriter(tempIni)) {
|
||||
pw.println("[appearance]");
|
||||
pw.println("uiFontFamily = Dialog");
|
||||
pw.println("uiFontSize = 16");
|
||||
pw.println("fontFamily = Monospaced");
|
||||
pw.println("fontSize = 22");
|
||||
}
|
||||
|
||||
Settings.loadFromIniFile(tempIni.getAbsolutePath());
|
||||
|
||||
assertEquals("Dialog", Settings.getUiFontFamily());
|
||||
assertEquals(16, Settings.getUiFontSize());
|
||||
assertEquals("Monospaced", Settings.getFontFamily());
|
||||
assertEquals(22, Settings.getFontSize());
|
||||
|
||||
// Export to another file and check contents
|
||||
java.io.File exportIni = java.io.File.createTempFile("j3270_ui_font_export", ".ini");
|
||||
exportIni.deleteOnExit();
|
||||
|
||||
Settings.exportToIniFile(exportIni.getAbsolutePath());
|
||||
String exported = new String(java.nio.file.Files.readAllBytes(exportIni.toPath()));
|
||||
|
||||
assertTrue(exported.contains("uiFontFamily = Dialog"), "Exported INI must contain uiFontFamily");
|
||||
assertTrue(exported.contains("uiFontSize = 16"), "Exported INI must contain uiFontSize");
|
||||
assertTrue(exported.contains("fontFamily = Monospaced"), "Exported INI must contain terminal fontFamily");
|
||||
assertTrue(exported.contains("fontSize = 22"), "Exported INI must contain terminal fontSize");
|
||||
} finally {
|
||||
tempIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTerminalPanelResizeDoesNotResetFontSize() {
|
||||
try {
|
||||
Settings.setFontSize(24);
|
||||
Settings.setFontFamily("Monospaced");
|
||||
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
assertEquals(24, panel.getFontSize());
|
||||
assertEquals(24, Settings.getFontSize());
|
||||
|
||||
// Simulate window / component resize events
|
||||
panel.setSize(new Dimension(800, 600));
|
||||
ComponentEvent resizeEvent1 = new ComponentEvent(panel, ComponentEvent.COMPONENT_RESIZED);
|
||||
for (java.awt.event.ComponentListener cl : panel.getComponentListeners()) {
|
||||
cl.componentResized(resizeEvent1);
|
||||
}
|
||||
|
||||
// Verify font size and family did NOT reset
|
||||
assertEquals(24, panel.getFontSize());
|
||||
assertEquals(24, Settings.getFontSize());
|
||||
assertEquals("Monospaced", Settings.getFontFamily());
|
||||
|
||||
// Simulate another resize to smaller dimension
|
||||
panel.setSize(new Dimension(400, 300));
|
||||
ComponentEvent resizeEvent2 = new ComponentEvent(panel, ComponentEvent.COMPONENT_RESIZED);
|
||||
for (java.awt.event.ComponentListener cl : panel.getComponentListeners()) {
|
||||
cl.componentResized(resizeEvent2);
|
||||
}
|
||||
|
||||
assertEquals(24, panel.getFontSize());
|
||||
assertEquals(24, Settings.getFontSize());
|
||||
assertEquals("Monospaced", Settings.getFontFamily());
|
||||
|
||||
panel.dispose();
|
||||
} catch (HeadlessException e) {
|
||||
// In headless environment without display, Settings persistence is verified above
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsDialogApplyDoesNotIncrementFontSize() throws Exception {
|
||||
try {
|
||||
Settings.setUiFontSize(16);
|
||||
Settings.setFontSize(22);
|
||||
Settings.setUiFontFamily("Dialog");
|
||||
Settings.setFontFamily("Monospaced");
|
||||
ThemeManager.applyUIManagerDefaults(ThemeManager.getTheme());
|
||||
|
||||
SettingsDialog dialog = new SettingsDialog(null);
|
||||
|
||||
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
|
||||
applyMethod.setAccessible(true);
|
||||
|
||||
// Find all JLabels in SettingsDialog
|
||||
java.util.List<javax.swing.JLabel> labels = new java.util.ArrayList<>();
|
||||
findLabelsRecursively(dialog.getContentPane(), labels);
|
||||
assertFalse(labels.isEmpty(), "SettingsDialog must contain labels");
|
||||
|
||||
for (javax.swing.JLabel label : labels) {
|
||||
assertEquals(16, label.getFont().getSize(),
|
||||
"Label '" + label.getText() + "' must initially have size 16");
|
||||
}
|
||||
|
||||
// Click Apply multiple times
|
||||
for (int click = 1; click <= 5; click++) {
|
||||
applyMethod.invoke(dialog);
|
||||
|
||||
assertEquals(16, Settings.getUiFontSize(), "Settings uiFontSize must stay 16");
|
||||
assertEquals(22, Settings.getFontSize(), "Settings fontSize must stay 22");
|
||||
|
||||
for (javax.swing.JLabel label : labels) {
|
||||
assertEquals(16, label.getFont().getSize(),
|
||||
"Label '" + label.getText() + "' must remain at size 16 after click #" + click);
|
||||
}
|
||||
}
|
||||
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environment if dialog creation fails
|
||||
}
|
||||
}
|
||||
|
||||
private void findLabelsRecursively(java.awt.Container container, java.util.List<javax.swing.JLabel> labels) {
|
||||
for (java.awt.Component c : container.getComponents()) {
|
||||
if (c instanceof javax.swing.JLabel) {
|
||||
labels.add((javax.swing.JLabel) c);
|
||||
} else if (c instanceof java.awt.Container) {
|
||||
findLabelsRecursively((java.awt.Container) c, labels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for HotspotEngine (Phase 25.2).
|
||||
* Verifies regex detection of URLs, PF keys, Enter, and menu choices,
|
||||
* hit-testing, action strings, and word boundary isolation.
|
||||
*/
|
||||
public class HotspotEngineTest {
|
||||
|
||||
private HotspotEngine engine;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
engine = new HotspotEngine();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects HTTP, HTTPS, FTP, and WWW URLs with action strings")
|
||||
public void testUrlDetection() {
|
||||
String screen = "Visit https://ibm.com/support or http://example.org:8080/path?q=1 for help.\n" +
|
||||
"Download from ftp://ftp.archive.org/pub/files or browse www.google.com.";
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
|
||||
|
||||
assertEquals(4, hotspots.size());
|
||||
|
||||
HotspotEngine.Hotspot h0 = hotspots.get(0);
|
||||
assertEquals(HotspotEngine.HotspotType.URL, h0.getType());
|
||||
assertEquals("https://ibm.com/support", h0.getDisplayText());
|
||||
assertEquals("https://ibm.com/support", h0.getAction());
|
||||
assertEquals(0, h0.getStartRow());
|
||||
assertEquals(6, h0.getStartCol());
|
||||
assertTrue(h0.contains(0, 10));
|
||||
assertFalse(h0.contains(0, 5));
|
||||
|
||||
HotspotEngine.Hotspot h1 = hotspots.get(1);
|
||||
assertEquals("http://example.org:8080/path?q=1", h1.getDisplayText());
|
||||
assertEquals("http://example.org:8080/path?q=1", h1.getAction());
|
||||
|
||||
HotspotEngine.Hotspot h2 = hotspots.get(2);
|
||||
assertEquals("ftp://ftp.archive.org/pub/files", h2.getDisplayText());
|
||||
assertEquals("ftp://ftp.archive.org/pub/files", h2.getAction());
|
||||
|
||||
HotspotEngine.Hotspot h3 = hotspots.get(3);
|
||||
assertEquals("www.google.com", h3.getDisplayText());
|
||||
assertEquals("http://www.google.com", h3.getAction(), "www. URLs should have http:// prepended for action");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects PF1..PF24, F1..F24, PA1..PA3, and [ENTER] function keys")
|
||||
public void testFunctionKeyDetection() {
|
||||
String screen = "PF1=Help PF3=Exit PF7=Up PF8=Down PF12=Cancel\n" +
|
||||
"F1=Help F12=Cancel PA1=Abort [ENTER]=Continue";
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
|
||||
|
||||
assertEquals(9, hotspots.size());
|
||||
|
||||
// PF1
|
||||
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(0).getType());
|
||||
assertEquals("PF1", hotspots.get(0).getDisplayText());
|
||||
assertEquals("[pf1]", hotspots.get(0).getAction());
|
||||
|
||||
// PF3
|
||||
assertEquals("PF3", hotspots.get(1).getDisplayText());
|
||||
assertEquals("[pf3]", hotspots.get(1).getAction());
|
||||
|
||||
// PF7
|
||||
assertEquals("PF7", hotspots.get(2).getDisplayText());
|
||||
assertEquals("[pf7]", hotspots.get(2).getAction());
|
||||
|
||||
// PF8
|
||||
assertEquals("PF8", hotspots.get(3).getDisplayText());
|
||||
assertEquals("[pf8]", hotspots.get(3).getAction());
|
||||
|
||||
// PF12
|
||||
assertEquals("PF12", hotspots.get(4).getDisplayText());
|
||||
assertEquals("[pf12]", hotspots.get(4).getAction());
|
||||
|
||||
// F1 -> [pf1]
|
||||
assertEquals("F1", hotspots.get(5).getDisplayText());
|
||||
assertEquals("[pf1]", hotspots.get(5).getAction());
|
||||
|
||||
// F12 -> [pf12]
|
||||
assertEquals("F12", hotspots.get(6).getDisplayText());
|
||||
assertEquals("[pf12]", hotspots.get(6).getAction());
|
||||
|
||||
// PA1 -> [pa1]
|
||||
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(7).getType());
|
||||
assertEquals("PA1", hotspots.get(7).getDisplayText());
|
||||
assertEquals("[pa1]", hotspots.get(7).getAction());
|
||||
|
||||
// [ENTER] -> [enter]
|
||||
assertEquals(HotspotEngine.HotspotType.ENTER, hotspots.get(8).getType());
|
||||
assertEquals("[ENTER]", hotspots.get(8).getDisplayText());
|
||||
assertEquals("[enter]", hotspots.get(8).getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Isolates function keys with strict word boundaries to avoid false positives")
|
||||
public void testWordBoundaryIsolation() {
|
||||
String screen = "STAFF1 MEMBER SPF12 FILE TRANSFER PROMPT";
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 1, 80);
|
||||
|
||||
// Neither STAFF1 nor SPF12 nor TRANSFER should be detected as PF1, F1, or F12
|
||||
assertEquals(0, hotspots.size(), "Embedded letters in words should not trigger PF key hotspots");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects numbered menu choices and arrow prompts")
|
||||
public void testMenuChoiceDetection() {
|
||||
String screen = " 1. User Settings\n" +
|
||||
" 2. Session Profiles\n" +
|
||||
"===> 3";
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 3, 80);
|
||||
|
||||
assertEquals(3, hotspots.size());
|
||||
|
||||
HotspotEngine.Hotspot m1 = hotspots.get(0);
|
||||
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, m1.getType());
|
||||
assertEquals("1.", m1.getDisplayText());
|
||||
assertEquals("1[enter]", m1.getAction());
|
||||
assertEquals(0, m1.getStartRow());
|
||||
|
||||
HotspotEngine.Hotspot m2 = hotspots.get(1);
|
||||
assertEquals("2.", m2.getDisplayText());
|
||||
assertEquals("2[enter]", m2.getAction());
|
||||
assertEquals(1, m2.getStartRow());
|
||||
|
||||
HotspotEngine.Hotspot m3 = hotspots.get(2);
|
||||
assertEquals("===> 3", m3.getDisplayText());
|
||||
assertEquals("3[enter]", m3.getAction());
|
||||
assertEquals(2, m3.getStartRow());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Hit-testing finds hotspot under specific coordinate")
|
||||
public void testFindHotspot() {
|
||||
String screen = "PF3=Exit https://github.com ===> 1";
|
||||
engine.scanText(screen, 1, 80);
|
||||
|
||||
// Under PF3 (cols 0..2)
|
||||
HotspotEngine.Hotspot hPf = engine.findHotspot(0, 1);
|
||||
assertNotNull(hPf);
|
||||
assertEquals("PF3", hPf.getDisplayText());
|
||||
|
||||
// Space between hotspots
|
||||
assertNull(engine.findHotspot(0, 5));
|
||||
|
||||
// Under URL (cols 11..28)
|
||||
HotspotEngine.Hotspot hUrl = engine.findHotspot(0, 15);
|
||||
assertNotNull(hUrl);
|
||||
assertEquals("https://github.com", hUrl.getDisplayText());
|
||||
|
||||
// Under prompt (cols 32..37)
|
||||
HotspotEngine.Hotspot hPrompt = engine.findHotspot(0, 34);
|
||||
assertNotNull(hPrompt);
|
||||
assertEquals("===> 1", hPrompt.getDisplayText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Toggling engine options enables or disables specific hotspot categories")
|
||||
public void testTogglingCategories() {
|
||||
String screen = "PF3=Exit https://example.com 1. Options";
|
||||
|
||||
engine.scanText(screen, 1, 80);
|
||||
assertEquals(3, engine.getHotspots().size());
|
||||
|
||||
// Disable URLs
|
||||
engine.setUrlEnabled(false);
|
||||
List<HotspotEngine.Hotspot> noUrls = engine.scanText(screen, 1, 80);
|
||||
assertEquals(2, noUrls.size());
|
||||
assertFalse(noUrls.stream().anyMatch(h -> h.getType() == HotspotEngine.HotspotType.URL));
|
||||
|
||||
// Disable PF keys
|
||||
engine.setPfKeyEnabled(false);
|
||||
List<HotspotEngine.Hotspot> menuOnly = engine.scanText(screen, 1, 80);
|
||||
assertEquals(1, menuOnly.size());
|
||||
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, menuOnly.get(0).getType());
|
||||
|
||||
// Disable menu choices
|
||||
engine.setMenuChoiceEnabled(false);
|
||||
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
|
||||
assertEquals(0, empty.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ImportConfigTest {
|
||||
|
||||
private String origFontFamily;
|
||||
private int origFontSize;
|
||||
private String origHost;
|
||||
private int origPort;
|
||||
private boolean origAutoReconnect;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
origFontFamily = Settings.getFontFamily();
|
||||
origFontSize = Settings.getFontSize();
|
||||
origHost = Settings.getAutoConnectHost();
|
||||
origPort = Settings.getAutoConnectPort();
|
||||
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setFontFamily(origFontFamily);
|
||||
Settings.setFontSize(origFontSize);
|
||||
Settings.setAutoConnectHost(origHost);
|
||||
Settings.setAutoConnectPort(origPort);
|
||||
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog contains Import Config button positioned to the left of Export Config")
|
||||
public void testImportButtonPlacementAndProperties() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
JButton btnImport = dialog.getBtnImport();
|
||||
JButton btnExport = dialog.getBtnExport();
|
||||
|
||||
assertNotNull(btnImport, "Import Config button must exist");
|
||||
assertNotNull(btnExport, "Export Config button must exist");
|
||||
|
||||
assertEquals("Import Config...", btnImport.getText());
|
||||
assertEquals("Export Config...", btnExport.getText());
|
||||
|
||||
// Verify btnImport is in the same container and positioned before (to the left of) btnExport
|
||||
Container parent = btnImport.getParent();
|
||||
assertNotNull(parent, "Import button must have a parent container");
|
||||
assertSame(parent, btnExport.getParent(), "Import and Export buttons must share the same parent panel");
|
||||
|
||||
Component[] comps = parent.getComponents();
|
||||
int importIdx = -1;
|
||||
int exportIdx = -1;
|
||||
for (int i = 0; i < comps.length; i++) {
|
||||
if (comps[i] == btnImport) importIdx = i;
|
||||
if (comps[i] == btnExport) exportIdx = i;
|
||||
}
|
||||
|
||||
assertTrue(importIdx >= 0, "btnImport must be in parent container");
|
||||
assertTrue(exportIdx >= 0, "btnExport must be in parent container");
|
||||
assertTrue(importIdx < exportIdx, "btnImport must be placed to the left of btnExport (index " + importIdx + " < " + exportIdx + ")");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("importConfigFile updates Settings and refreshes SettingsDialog controls")
|
||||
public void testImportConfigFileUpdatesSettingsAndDialog() throws IOException {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
File tempIni = File.createTempFile("j3270_test_import", ".ini");
|
||||
try {
|
||||
try (FileWriter writer = new FileWriter(tempIni)) {
|
||||
writer.write("[appearance]\n");
|
||||
writer.write("fontSize = 24\n");
|
||||
writer.write("fontFamily = Monospaced\n");
|
||||
writer.write("\n[behavior]\n");
|
||||
writer.write("startupBehavior = AUTO_CONNECT\n");
|
||||
writer.write("autoConnectHost = testhost.example.org\n");
|
||||
writer.write("autoConnectPort = 2323\n");
|
||||
writer.write("autoConnectAutoReconnect = true\n");
|
||||
}
|
||||
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
dialog.importConfigFile(tempIni);
|
||||
|
||||
// Verify Settings updated
|
||||
assertEquals(24, Settings.getFontSize());
|
||||
assertEquals("testhost.example.org", Settings.getAutoConnectHost());
|
||||
assertEquals(2323, Settings.getAutoConnectPort());
|
||||
assertTrue(Settings.getAutoConnectAutoReconnect());
|
||||
|
||||
// Verify dialog state reflects the imported values
|
||||
assertEquals(Settings.StartupBehavior.AUTO_CONNECT, Settings.getStartupBehavior());
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
} finally {
|
||||
if (tempIni.exists()) {
|
||||
tempIni.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("importConfigFile throws FileNotFoundException for nonexistent file")
|
||||
public void testImportNonExistentFileThrows() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
File nonExistent = new File("/path/to/nonexistent/file_" + System.currentTimeMillis() + ".ini");
|
||||
assertThrows(FileNotFoundException.class, () -> {
|
||||
dialog.importConfigFile(nonExistent);
|
||||
});
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class KeyBindingsTest {
|
||||
|
||||
@Test
|
||||
public void testDefaultBindingsContainRequiredKeys() {
|
||||
String insertBindings = Settings.getDefaultBinding("INSERT");
|
||||
assertNotNull(insertBindings);
|
||||
assertTrue(insertBindings.contains("INSERT"), "Default INSERT binding should contain INSERT key");
|
||||
if (Settings.isMac()) {
|
||||
assertTrue(insertBindings.contains("HELP"), "macOS INSERT binding should contain HELP key");
|
||||
assertTrue(insertBindings.contains("alt I"), "macOS INSERT binding should contain alt I fallback");
|
||||
} else {
|
||||
assertTrue(insertBindings.contains("ctrl I"), "Non-macOS INSERT binding should contain ctrl I fallback");
|
||||
}
|
||||
|
||||
String copyBindings = Settings.getDefaultBinding("COPY");
|
||||
assertNotNull(copyBindings);
|
||||
assertTrue(copyBindings.contains("ctrl INSERT"), "Default COPY binding should support 3270 Ctrl+Insert");
|
||||
assertTrue(copyBindings.contains("ctrl C"), "Default COPY binding should support Ctrl+C");
|
||||
assertTrue(copyBindings.contains("ctrl shift C"), "Default COPY binding should support Ctrl+Shift+C");
|
||||
if (Settings.isMac()) {
|
||||
assertTrue(copyBindings.contains("meta C"), "macOS COPY binding should support Cmd+C");
|
||||
}
|
||||
|
||||
String pasteBindings = Settings.getDefaultBinding("PASTE");
|
||||
assertNotNull(pasteBindings);
|
||||
assertTrue(pasteBindings.contains("shift INSERT"), "Default PASTE binding should support 3270 Shift+Insert");
|
||||
assertTrue(pasteBindings.contains("ctrl V"), "Default PASTE binding should support Ctrl+V");
|
||||
assertTrue(pasteBindings.contains("ctrl shift V"), "Default PASTE binding should support Ctrl+Shift+V");
|
||||
if (Settings.isMac()) {
|
||||
assertTrue(pasteBindings.contains("meta V"), "macOS PASTE binding should support Cmd+V");
|
||||
}
|
||||
|
||||
String selectAllBindings = Settings.getDefaultBinding("SELECTALL");
|
||||
assertNotNull(selectAllBindings);
|
||||
assertTrue(selectAllBindings.contains("ctrl A"), "Default SELECTALL binding should support Ctrl+A");
|
||||
if (Settings.isMac()) {
|
||||
assertTrue(selectAllBindings.contains("meta A"), "macOS SELECTALL binding should support Cmd+A");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTerminalPanelInputMapAndAction() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
InputMap im = panel.getInputMap(JComponent.WHEN_FOCUSED);
|
||||
ActionMap am = panel.getActionMap();
|
||||
|
||||
// Check INSERT key mappings
|
||||
KeyStroke ksInsert = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0);
|
||||
assertEquals("j3270-INSERT", im.get(ksInsert), "VK_INSERT must map to j3270-INSERT");
|
||||
|
||||
if (Settings.isMac()) {
|
||||
KeyStroke ksHelp = KeyStroke.getKeyStroke(KeyEvent.VK_HELP, 0);
|
||||
assertEquals("j3270-INSERT", im.get(ksHelp), "VK_HELP must map to j3270-INSERT on Mac");
|
||||
KeyStroke ksAltI = KeyStroke.getKeyStroke("alt I");
|
||||
assertEquals("j3270-INSERT", im.get(ksAltI), "alt I must map to j3270-INSERT on Mac");
|
||||
}
|
||||
|
||||
// Check COPY key mappings
|
||||
KeyStroke ksCtrlIns = KeyStroke.getKeyStroke("ctrl INSERT");
|
||||
assertEquals("j3270-COPY", im.get(ksCtrlIns), "ctrl INSERT must map to j3270-COPY");
|
||||
KeyStroke ksCtrlC = KeyStroke.getKeyStroke("ctrl C");
|
||||
assertEquals("j3270-COPY", im.get(ksCtrlC), "ctrl C must map to j3270-COPY");
|
||||
if (Settings.isMac()) {
|
||||
KeyStroke ksMetaC = KeyStroke.getKeyStroke("meta C");
|
||||
assertEquals("j3270-COPY", im.get(ksMetaC), "meta C must map to j3270-COPY on Mac");
|
||||
}
|
||||
|
||||
// Check PASTE key mappings
|
||||
KeyStroke ksShiftIns = KeyStroke.getKeyStroke("shift INSERT");
|
||||
assertEquals("j3270-PASTE", im.get(ksShiftIns), "shift INSERT must map to j3270-PASTE");
|
||||
KeyStroke ksCtrlV = KeyStroke.getKeyStroke("ctrl V");
|
||||
assertEquals("j3270-PASTE", im.get(ksCtrlV), "ctrl V must map to j3270-PASTE");
|
||||
if (Settings.isMac()) {
|
||||
KeyStroke ksMetaV = KeyStroke.getKeyStroke("meta V");
|
||||
assertEquals("j3270-PASTE", im.get(ksMetaV), "meta V must map to j3270-PASTE on Mac");
|
||||
}
|
||||
|
||||
// Check SELECTALL key mappings
|
||||
KeyStroke ksCtrlA = KeyStroke.getKeyStroke("ctrl A");
|
||||
assertEquals("j3270-SELECTALL", im.get(ksCtrlA), "ctrl A must map to j3270-SELECTALL");
|
||||
if (Settings.isMac()) {
|
||||
KeyStroke ksMetaA = KeyStroke.getKeyStroke("meta A");
|
||||
assertEquals("j3270-SELECTALL", im.get(ksMetaA), "meta A must map to j3270-SELECTALL on Mac");
|
||||
}
|
||||
|
||||
// Test insert mode toggle action
|
||||
assertFalse(panel.isInsertMode(), "Initial insertMode should be false");
|
||||
Action insertAction = am.get("j3270-INSERT");
|
||||
assertNotNull(insertAction, "j3270-INSERT action must exist");
|
||||
|
||||
AtomicBoolean modeChangedFired = new AtomicBoolean(false);
|
||||
panel.addModeChangeListener(() -> modeChangedFired.set(true));
|
||||
|
||||
insertAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
|
||||
assertTrue(panel.isInsertMode(), "insertMode should be true after invoking action");
|
||||
assertTrue(modeChangedFired.get(), "ModeChangeListener should have fired");
|
||||
|
||||
modeChangedFired.set(false);
|
||||
insertAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
|
||||
assertFalse(panel.isInsertMode(), "insertMode should be false after invoking action again");
|
||||
assertTrue(modeChangedFired.get(), "ModeChangeListener should have fired again");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertModeSyncWithClient() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.toggleInsertMode();
|
||||
assertTrue(panel.isInsertMode());
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
panel.setClient(client);
|
||||
|
||||
// When client is attached, client's inputProcessor should receive current insertMode
|
||||
assertNotNull(client.getInputProcessor());
|
||||
assertTrue(client.getInputProcessor().isInsertMode(), "Client inputProcessor should inherit insertMode");
|
||||
assertTrue(panel.isInsertMode());
|
||||
|
||||
// Toggling on panel updates client
|
||||
panel.toggleInsertMode();
|
||||
assertFalse(client.getInputProcessor().isInsertMode());
|
||||
assertFalse(panel.isInsertMode());
|
||||
|
||||
panel.toggleInsertMode();
|
||||
assertTrue(client.getInputProcessor().isInsertMode());
|
||||
assertTrue(panel.isInsertMode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJ3270AppModesMenuSync() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb);
|
||||
|
||||
JMenu modesMenu = null;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null && "Modes".equals(m.getText())) {
|
||||
modesMenu = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(modesMenu, "Modes menu should exist");
|
||||
|
||||
JCheckBoxMenuItem insertItem = null;
|
||||
for (int i = 0; i < modesMenu.getItemCount(); i++) {
|
||||
JMenuItem item = modesMenu.getItem(i);
|
||||
if (item instanceof JCheckBoxMenuItem && "Insert Mode".equals(item.getText())) {
|
||||
insertItem = (JCheckBoxMenuItem) item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(insertItem, "Insert Mode menu item should exist");
|
||||
assertFalse(insertItem.isSelected(), "Initial Insert Mode item should not be selected");
|
||||
|
||||
// Find terminal panel and toggle
|
||||
TerminalPanel panel = null;
|
||||
for (java.awt.Component c : app.getContentPane().getComponents()) {
|
||||
if (c instanceof TerminalPanel) {
|
||||
panel = (TerminalPanel) c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(panel, "TerminalPanel should be found");
|
||||
|
||||
// Toggling via panel updates menu item
|
||||
panel.toggleInsertMode();
|
||||
assertTrue(panel.isInsertMode());
|
||||
assertTrue(insertItem.isSelected(), "Menu item should reflect true insert mode");
|
||||
|
||||
panel.toggleInsertMode();
|
||||
assertFalse(panel.isInsertMode());
|
||||
assertFalse(insertItem.isSelected(), "Menu item should reflect false insert mode");
|
||||
|
||||
// Clicking menu item toggles insert mode on panel
|
||||
insertItem.doClick();
|
||||
assertTrue(panel.isInsertMode(), "Panel should be in insert mode after clicking menu item");
|
||||
assertTrue(insertItem.isSelected(), "Menu item should be selected after click");
|
||||
|
||||
insertItem.doClick();
|
||||
assertFalse(panel.isInsertMode(), "Panel should exit insert mode after clicking menu item");
|
||||
assertFalse(insertItem.isSelected(), "Menu item should be deselected after click");
|
||||
} finally {
|
||||
app.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyAndPasteActions() {
|
||||
try {
|
||||
java.awt.Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
panel.setClient(client);
|
||||
|
||||
var sb = client.getScreenBuffer();
|
||||
sb.setChar(0, 0, 'J');
|
||||
sb.setChar(0, 1, '3');
|
||||
sb.setChar(0, 2, '2');
|
||||
sb.setChar(0, 3, '7');
|
||||
sb.setChar(0, 4, '0');
|
||||
|
||||
panel.setSelectionRange(0, 4);
|
||||
assertTrue(panel.hasSelection());
|
||||
assertEquals("J3270", panel.getSelectedText());
|
||||
|
||||
Action copyAction = panel.getActionMap().get("j3270-COPY");
|
||||
assertNotNull(copyAction);
|
||||
copyAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
|
||||
|
||||
try {
|
||||
String clip = (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
.getData(java.awt.datatransfer.DataFlavor.stringFlavor);
|
||||
assertEquals("J3270", clip);
|
||||
} catch (Exception e) {
|
||||
fail("Clipboard read failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Test multiline paste handling
|
||||
try {
|
||||
java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
|
||||
new java.awt.datatransfer.StringSelection("LINE1\nLINE2"), null);
|
||||
Action pasteAction = panel.getActionMap().get("j3270-PASTE");
|
||||
assertNotNull(pasteAction);
|
||||
pasteAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, ""));
|
||||
// Shouldn't throw any exceptions
|
||||
} catch (Exception e) {
|
||||
fail("Paste action failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToggleStatusBar() {
|
||||
assertEquals("alt B", Settings.getDefaultBinding("STATUS_BAR"));
|
||||
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
InputMap im = panel.getInputMap(JComponent.WHEN_FOCUSED);
|
||||
ActionMap am = panel.getActionMap();
|
||||
|
||||
KeyStroke ksAltB = KeyStroke.getKeyStroke("alt B");
|
||||
assertEquals("j3270-STATUS_BAR", im.get(ksAltB), "alt B must map to j3270-STATUS_BAR");
|
||||
assertNotNull(am.get("j3270-STATUS_BAR"), "j3270-STATUS_BAR action must exist");
|
||||
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb);
|
||||
|
||||
JMenu viewMenu = null;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null && "View".equals(m.getText())) {
|
||||
viewMenu = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(viewMenu);
|
||||
|
||||
JCheckBoxMenuItem statusBarItem = null;
|
||||
for (int i = 0; i < viewMenu.getItemCount(); i++) {
|
||||
JMenuItem item = viewMenu.getItem(i);
|
||||
if (item instanceof JCheckBoxMenuItem && "Status Bar".equals(item.getText())) {
|
||||
statusBarItem = (JCheckBoxMenuItem) item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(statusBarItem, "Status Bar menu item must exist in View menu");
|
||||
assertTrue(statusBarItem.isSelected(), "Status bar should be selected by default");
|
||||
|
||||
StatusBar bar = null;
|
||||
for (java.awt.Component c : app.getContentPane().getComponents()) {
|
||||
if (c instanceof StatusBar) {
|
||||
bar = (StatusBar) c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(bar, "StatusBar component should exist in content pane");
|
||||
assertTrue(bar.isVisible(), "StatusBar should be visible by default");
|
||||
|
||||
// Test toggling via toggleStatusBar()
|
||||
app.toggleStatusBar();
|
||||
assertFalse(bar.isVisible(), "StatusBar should be hidden after toggle");
|
||||
assertFalse(statusBarItem.isSelected(), "Menu item should be unchecked");
|
||||
assertFalse(Settings.getStatusBarVisible());
|
||||
|
||||
app.toggleStatusBar();
|
||||
assertTrue(bar.isVisible(), "StatusBar should be shown after second toggle");
|
||||
assertTrue(statusBarItem.isSelected(), "Menu item should be checked");
|
||||
assertTrue(Settings.getStatusBarVisible());
|
||||
|
||||
// Test toggling via menu item click
|
||||
statusBarItem.doClick();
|
||||
assertFalse(bar.isVisible(), "StatusBar should be hidden after menu click");
|
||||
assertFalse(statusBarItem.isSelected(), "Menu item should be unchecked");
|
||||
|
||||
statusBarItem.doClick();
|
||||
assertTrue(bar.isVisible(), "StatusBar should be shown after menu click");
|
||||
assertTrue(statusBarItem.isSelected(), "Menu item should be checked");
|
||||
} finally {
|
||||
app.dispose();
|
||||
Settings.setStatusBarVisible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 13.3: Keyboard Remap (.kmp) File Importer.
|
||||
* Tests parsing of scan codes, modifier combinations, bind statements,
|
||||
* mnemonic actions, and comments.
|
||||
*/
|
||||
public class KmpKeymapImportTest {
|
||||
|
||||
@Test
|
||||
public void testParseScanCodeAssignments() throws Exception {
|
||||
String kmpContent =
|
||||
"# Sample keyboard profile mapping\n" +
|
||||
"; IBM scan code mappings\n" +
|
||||
"KEY43=[enter]\n" +
|
||||
"S-KEY112=[pf13]\n" +
|
||||
"C-KEY43=[newline]\n" +
|
||||
"A-KEY110=[reset]\n" +
|
||||
"KEY15=[tab]\n" +
|
||||
"S-KEY15=[backtab]\n";
|
||||
|
||||
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
|
||||
assertNotNull(imported);
|
||||
|
||||
// KEY43 is ENTER -> ENTER
|
||||
assertEquals("ENTER", imported.get("ENTER"));
|
||||
// S-KEY112 is Shift+F1 -> PF13
|
||||
assertEquals("shift F1", imported.get("PF13"));
|
||||
// C-KEY43 is Ctrl+ENTER -> NEWLINE
|
||||
assertEquals("ctrl ENTER", imported.get("NEWLINE"));
|
||||
// A-KEY110 is Alt+ESCAPE -> ESCAPE
|
||||
assertEquals("alt ESCAPE", imported.get("ESCAPE"));
|
||||
// KEY15 is TAB -> TAB
|
||||
assertEquals("TAB", imported.get("TAB"));
|
||||
// S-KEY15 is Shift+TAB -> shift TAB
|
||||
assertEquals("shift TAB", imported.get("shift TAB"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBindStatements() throws Exception {
|
||||
String kmpContent =
|
||||
"bind [enter] Ctrl+Enter\n" +
|
||||
"bind [pf1] F1\n" +
|
||||
"bind [pf24] Shift+F12\n" +
|
||||
"bind [eraseeof] End\n" +
|
||||
"bind [clear] Pause\n";
|
||||
|
||||
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
|
||||
assertNotNull(imported);
|
||||
|
||||
assertEquals("ctrl ENTER", imported.get("ENTER"));
|
||||
assertEquals("F1", imported.get("PF1"));
|
||||
assertEquals("shift F12", imported.get("PF24"));
|
||||
assertEquals("END", imported.get("END"));
|
||||
assertEquals("PAUSE", imported.get("CLEAR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScanCodeWithMultipleModifiers() {
|
||||
// C-S-KEY85 -> Ctrl+Shift+PAGE_UP
|
||||
String res = KeyBindings.parseScanCodeEntry("CS-KEY85");
|
||||
assertEquals("ctrl shift PAGE_UP", res);
|
||||
|
||||
// 2-KEY80 -> Alt+HOME (2- prefix is Alt in scan code syntax)
|
||||
String altRes = KeyBindings.parseScanCodeEntry("2-KEY80");
|
||||
assertEquals("alt HOME", altRes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMnemonicResolution() {
|
||||
assertEquals("ENTER", KeyBindings.resolveMnemonic("[enter]"));
|
||||
assertEquals("ENTER", KeyBindings.resolveMnemonic("enterreset"));
|
||||
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[newline]"));
|
||||
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[field-exit]"));
|
||||
assertEquals("PF1", KeyBindings.resolveMnemonic("[pf1]"));
|
||||
assertEquals("PF12", KeyBindings.resolveMnemonic("[pf12]"));
|
||||
assertEquals("PF13", KeyBindings.resolveMnemonic("[spf1]"));
|
||||
assertEquals("ESCAPE", KeyBindings.resolveMnemonic("[reset]"));
|
||||
assertEquals("ERASE_INPUT", KeyBindings.resolveMnemonic("[erasefld]"));
|
||||
assertEquals("DOCMODE", KeyBindings.resolveMnemonic("[docmode]"));
|
||||
assertEquals("WORDWRAP", KeyBindings.resolveMnemonic("[wordwrap]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommentsAndBlankLinesIgnored() throws Exception {
|
||||
String kmpContent =
|
||||
"[ProfileHeader]\n" +
|
||||
" \n" +
|
||||
"# Full line comment\n" +
|
||||
"; Another comment\n" +
|
||||
"KEY42=[enter] ; inline comment\n" +
|
||||
" \n";
|
||||
|
||||
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
|
||||
assertEquals(1, imported.size());
|
||||
assertEquals("ENTER", imported.get("ENTER"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class MenuBarShortcutsTest {
|
||||
|
||||
private Map<String, JMenuItem> collectMenuItems(JMenu menu) {
|
||||
Map<String, JMenuItem> map = new HashMap<>();
|
||||
for (int i = 0; i < menu.getItemCount(); i++) {
|
||||
JMenuItem item = menu.getItem(i);
|
||||
if (item != null) {
|
||||
map.put(item.getText(), item);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionAndViewShortcutsUseAlt() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
// In automated/headless environments, JFrame cannot be initialized
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb, "JMenuBar should be present");
|
||||
|
||||
JMenu viewMenu = null;
|
||||
JMenu actionsMenu = null;
|
||||
JMenu fileMenu = null;
|
||||
JMenu editMenu = null;
|
||||
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null) {
|
||||
if ("View".equals(m.getText())) viewMenu = m;
|
||||
else if ("Actions".equals(m.getText())) actionsMenu = m;
|
||||
else if ("File".equals(m.getText())) fileMenu = m;
|
||||
else if ("Edit".equals(m.getText())) editMenu = m;
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(viewMenu, "View menu should exist");
|
||||
assertNotNull(actionsMenu, "Actions menu should exist");
|
||||
assertNotNull(fileMenu, "File menu should exist");
|
||||
assertNotNull(editMenu, "Edit menu should exist");
|
||||
|
||||
// Verify View Menu items
|
||||
Map<String, JMenuItem> viewItems = collectMenuItems(viewMenu);
|
||||
assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
|
||||
assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
|
||||
assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0);
|
||||
assertAcceleratorUsesAlt(viewItems.get("Status Bar"), KeyEvent.VK_B);
|
||||
|
||||
// Crosshair Ruler must use Alt+Shift+R
|
||||
JMenuItem rulerItem = viewItems.get("Crosshair Ruler");
|
||||
assertNotNull(rulerItem, "Crosshair Ruler menu item must exist");
|
||||
KeyStroke rulerKs = rulerItem.getAccelerator();
|
||||
assertNotNull(rulerKs, "Crosshair Ruler should have an accelerator");
|
||||
assertEquals(KeyEvent.VK_R, rulerKs.getKeyCode(), "Key code mismatch for Crosshair Ruler");
|
||||
int rulerMods = rulerKs.getModifiers();
|
||||
assertTrue((rulerMods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
|
||||
"Crosshair Ruler accelerator must have ALT modifier");
|
||||
assertTrue((rulerMods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK)) != 0,
|
||||
"Crosshair Ruler accelerator must have SHIFT modifier");
|
||||
assertEquals(0, rulerMods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
|
||||
"Crosshair Ruler accelerator must NOT have CTRL or META modifiers");
|
||||
|
||||
// Verify Actions Menu items use ALT_DOWN_MASK
|
||||
Map<String, JMenuItem> actionItems = collectMenuItems(actionsMenu);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Send Enter"), KeyEvent.VK_ENTER);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Clear"), KeyEvent.VK_K);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Reset"), KeyEvent.VK_R);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Erase Input"), KeyEvent.VK_E);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Attention"), KeyEvent.VK_A);
|
||||
assertAcceleratorUsesAlt(actionItems.get("System Request"), KeyEvent.VK_S);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q);
|
||||
assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L);
|
||||
assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T);
|
||||
|
||||
// Ensure Crosshair Ruler and Reset do not collide
|
||||
assertNotEquals(rulerKs, actionItems.get("Reset").getAccelerator(),
|
||||
"Crosshair Ruler and Reset accelerators must not collide");
|
||||
} finally {
|
||||
// Dispose frame
|
||||
app.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void assertAcceleratorUsesAlt(JMenuItem item, int expectedKeyCode) {
|
||||
assertNotNull(item, "Menu item must exist");
|
||||
KeyStroke ks = item.getAccelerator();
|
||||
assertNotNull(ks, "Menu item " + item.getText() + " should have an accelerator");
|
||||
assertEquals(expectedKeyCode, ks.getKeyCode(), "Key code mismatch for " + item.getText());
|
||||
int mods = ks.getModifiers();
|
||||
assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
|
||||
"Menu item " + item.getText() + " accelerator must have ALT modifier");
|
||||
assertEquals(0, mods & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.SHIFT_MASK),
|
||||
"Menu item " + item.getText() + " accelerator must NOT have SHIFT modifier");
|
||||
assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
|
||||
"Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.HeadlessException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 1 UI Overlays and Color Alignment.
|
||||
* Covers items 1.1, 1.2, and 1.5 of the PhasedUpdates specification.
|
||||
*/
|
||||
public class Phase1UiOverlayTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Item 1.1 & 1.2: TerminalPanel host and mono colors match IBM Host On-Demand specs")
|
||||
public void testTerminalPanelColorAlignment() {
|
||||
// Index 10: Orange (255, 162, 0)
|
||||
assertEquals(new Color(255, 162, 0), TerminalPanel.DEFAULT_HOST_COLORS[10]);
|
||||
|
||||
// Index 14: Mustard (160, 160, 0)
|
||||
assertEquals(new Color(160, 160, 0), TerminalPanel.DEFAULT_HOST_COLORS[14]);
|
||||
|
||||
// Index 1: Blue (120, 144, 240)
|
||||
assertEquals(new Color(120, 144, 240), TerminalPanel.DEFAULT_HOST_COLORS[1]);
|
||||
|
||||
// Base 4-Color mono defaults
|
||||
assertEquals(new Color(0, 255, 0), TerminalPanel.DEFAULT_MONO_NORMAL);
|
||||
assertEquals(new Color(255, 0, 0), TerminalPanel.DEFAULT_MONO_INTENSIFY);
|
||||
assertEquals(new Color(0, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED);
|
||||
assertEquals(new Color(255, 255, 255), TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Item 1.5: ThemeManager HoD OIA category colors match specification")
|
||||
public void testThemeManagerOiaColors() {
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
// oSI: Status / System Available -> CUSTOMBLUE (120, 144, 240)
|
||||
assertEquals(new Color(120, 144, 240), ThemeManager.getOiaStatusSysAvail());
|
||||
|
||||
// oII: Input Inhibited / X SYSTEM -> White (255, 255, 255)
|
||||
assertEquals(new Color(255, 255, 255), ThemeManager.getOiaInputInhibited());
|
||||
|
||||
// oAI: Attention / Reminders / Message Waiting -> Yellow (255, 255, 0)
|
||||
assertEquals(new Color(255, 255, 0), ThemeManager.getOiaAttention());
|
||||
|
||||
// oEI: Error Checks / Comm Check -> Red (255, 0, 0)
|
||||
assertEquals(new Color(255, 0, 0), ThemeManager.getOiaCommCheck());
|
||||
|
||||
// oOB: OIA Separator / Background -> Black (0, 0, 0)
|
||||
assertEquals(new Color(0, 0, 0), ThemeManager.getOiaBackground());
|
||||
|
||||
// Light mode OIA colors should be legible and not white on grey
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited());
|
||||
assertEquals(new Color(20, 20, 20), ThemeManager.getOiaInputInhibited());
|
||||
assertEquals(new Color(0, 90, 200), ThemeManager.getOiaStatusSysAvail());
|
||||
assertEquals(new Color(180, 100, 0), ThemeManager.getOiaAttention());
|
||||
assertEquals(new Color(190, 20, 20), ThemeManager.getOiaCommCheck());
|
||||
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), ThemeManager.getOiaBackground());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Item 1.5: TerminalPanel Crosshair Ruler and Cursor Style controls")
|
||||
public void testTerminalPanelRulerAndCursorStyle() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
|
||||
// Default cursor style is BLOCK
|
||||
assertEquals(TerminalPanel.CursorStyle.BLOCK, panel.getCursorStyle());
|
||||
panel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE);
|
||||
assertEquals(TerminalPanel.CursorStyle.UNDERLINE, panel.getCursorStyle());
|
||||
|
||||
// Crosshair ruler toggling
|
||||
assertFalse(panel.isCrosshairRulerEnabled());
|
||||
panel.setCrosshairRulerEnabled(true);
|
||||
assertTrue(panel.isCrosshairRulerEnabled());
|
||||
panel.toggleCrosshairRuler();
|
||||
assertFalse(panel.isCrosshairRulerEnabled());
|
||||
|
||||
// Wallpaper image setter
|
||||
assertNull(panel.getWallpaperImage());
|
||||
} catch (HeadlessException e) {
|
||||
// Handled gracefully in headless CI environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Settings persistence for Crosshair Ruler and Cursor Style")
|
||||
public void testSettingsPersistence() {
|
||||
boolean originalRuler = Settings.getCrosshairRuler();
|
||||
String originalCursor = Settings.getCursorStyle();
|
||||
|
||||
try {
|
||||
Settings.setCrosshairRuler(true);
|
||||
assertTrue(Settings.getCrosshairRuler());
|
||||
|
||||
Settings.setCrosshairRuler(false);
|
||||
assertFalse(Settings.getCrosshairRuler());
|
||||
|
||||
Settings.setCursorStyle("UNDERLINE");
|
||||
assertEquals("UNDERLINE", Settings.getCursorStyle());
|
||||
|
||||
Settings.setCursorStyle("BLOCK");
|
||||
assertEquals("BLOCK", Settings.getCursorStyle());
|
||||
} finally {
|
||||
Settings.setCrosshairRuler(originalRuler);
|
||||
Settings.setCursorStyle(originalCursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.graphics.GocaConstants;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsPlane;
|
||||
import haus.nightmare.lib3270j.graphics.HODWallpaper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verification test suite for Phase 2 UI Vector Graphics, Wallpaper, and Cursor Overlays.
|
||||
*/
|
||||
public class Phase2UiGraphicsTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Test TerminalPanel Wallpaper Integration (Tile, Center, Stretch)")
|
||||
public void testTerminalPanelWallpaperModes() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
assertNull(panel.getHodWallpaper());
|
||||
assertNull(panel.getWallpaperImage());
|
||||
|
||||
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
panel.setWallpaperImage(img);
|
||||
assertNotNull(panel.getWallpaperImage());
|
||||
assertNotNull(panel.getHodWallpaper());
|
||||
assertEquals(HODWallpaper.HOD_STRETCH, panel.getHodWallpaper().getDisplay());
|
||||
|
||||
panel.setWallpaperMode(HODWallpaper.HOD_TILE);
|
||||
assertEquals(HODWallpaper.HOD_TILE, panel.getHodWallpaper().getDisplay());
|
||||
|
||||
panel.setWallpaperMode(HODWallpaper.HOD_CENTER);
|
||||
assertEquals(HODWallpaper.HOD_CENTER, panel.getHodWallpaper().getDisplay());
|
||||
|
||||
HODWallpaper customWp = new HODWallpaper(img, HODWallpaper.HOD_TILE);
|
||||
panel.setHodWallpaper(customWp);
|
||||
assertSame(customWp, panel.getHodWallpaper());
|
||||
|
||||
// Clear wallpaper
|
||||
panel.setWallpaperImage(null);
|
||||
assertNull(panel.getHodWallpaper());
|
||||
assertNull(panel.getWallpaperImage());
|
||||
} catch (HeadlessException e) {
|
||||
// Handled gracefully in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Graphics Plane Cursor Synchronization and Rendering")
|
||||
public void testGraphicsCursorOverlay() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(new Dimension(800, 600));
|
||||
panel.setClient(new haus.nightmare.lib3270j.Telnet3270Client(new haus.nightmare.lib3270j.ConnectionConfig("localhost", 23)));
|
||||
|
||||
GraphicsPlane plane = panel.getClient() != null ? panel.getClient().getGraphicsPlane() : null;
|
||||
if (plane != null) {
|
||||
// Attach graphics cursor
|
||||
plane.attachGraphicCursor(100, 100);
|
||||
plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_CROSSHAIR);
|
||||
assertTrue(plane.isGraphicCursorAttached());
|
||||
assertEquals(100, plane.getGraphicCursorX());
|
||||
assertEquals(100, plane.getGraphicCursorY());
|
||||
assertEquals(1, plane.getHodCursorShape());
|
||||
|
||||
// Paint component to verify no exceptions during rendering
|
||||
BufferedImage offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = offscreen.createGraphics();
|
||||
panel.paint(g2);
|
||||
g2.dispose();
|
||||
|
||||
// Change cursor shape to box
|
||||
plane.setHodCursorShape(GocaConstants.GCURSOR_SHAPE_BOX);
|
||||
assertEquals(2, plane.getHodCursorShape());
|
||||
offscreen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_ARGB);
|
||||
g2 = offscreen.createGraphics();
|
||||
panel.paint(g2);
|
||||
g2.dispose();
|
||||
|
||||
// Detach graphics cursor
|
||||
plane.detachGraphicCursor();
|
||||
assertFalse(plane.isGraphicCursorAttached());
|
||||
}
|
||||
} catch (HeadlessException e) {
|
||||
// Handled gracefully in headless environments
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Cursor;
|
||||
import java.awt.HeadlessException;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class SelectionHandlesAndTrimTest {
|
||||
|
||||
private boolean origRemainAfterEdit;
|
||||
private boolean origSizingHandles;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
origRemainAfterEdit = Settings.getTrimRectRemainAfterEdit();
|
||||
origSizingHandles = Settings.getTrimRectSizingHandles();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setTrimRectRemainAfterEdit(origRemainAfterEdit);
|
||||
Settings.setTrimRectSizingHandles(origSizingHandles);
|
||||
}
|
||||
|
||||
private TerminalPanel createTestPanel() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
panel.setClient(client);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Trailing whitespace is trimmed on copied lines in block selection mode")
|
||||
public void testBlockSelectionTrailingWhitespaceTrim() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
int cols = sb.getCols();
|
||||
|
||||
// Populate row 0: "HELLO "
|
||||
String text0 = "HELLO";
|
||||
for (int i = 0; i < text0.length(); i++) {
|
||||
sb.getCell(i).ucs4 = text0.charAt(i);
|
||||
}
|
||||
|
||||
// Populate row 1: "WORLD 123 "
|
||||
String text1 = "WORLD 123";
|
||||
for (int i = 0; i < text1.length(); i++) {
|
||||
sb.getCell(cols + i).ucs4 = text1.charAt(i);
|
||||
}
|
||||
|
||||
// Select rectangle from (row 0, col 0) to (row 1, col 20)
|
||||
panel.setBlockSelectMode(true);
|
||||
panel.setSelectionRange(0, cols + 20);
|
||||
|
||||
String copied = panel.getSelectedText();
|
||||
// Should be "HELLO\nWORLD 123" without the trailing spaces up to col 20
|
||||
assertEquals("HELLO\nWORLD 123", copied);
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Trailing whitespace is trimmed on copied lines in stream selection mode")
|
||||
public void testStreamSelectionTrailingWhitespaceTrim() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
int cols = sb.getCols();
|
||||
|
||||
// Populate row 0: "LINE1 "
|
||||
String text0 = "LINE1";
|
||||
for (int i = 0; i < text0.length(); i++) {
|
||||
sb.getCell(i).ucs4 = text0.charAt(i);
|
||||
}
|
||||
|
||||
// Populate row 1: "LINE2"
|
||||
String text1 = "LINE2";
|
||||
for (int i = 0; i < text1.length(); i++) {
|
||||
sb.getCell(cols + i).ucs4 = text1.charAt(i);
|
||||
}
|
||||
|
||||
panel.setBlockSelectMode(false);
|
||||
panel.setTrimRectSizingHandles(false);
|
||||
// Select from row 0, col 0 to row 1, col 10
|
||||
panel.setSelectionRange(0, cols + 10);
|
||||
|
||||
String copied = panel.getSelectedText();
|
||||
assertEquals("LINE1\nLINE2", copied);
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Drag handle detection identifies 8 resize handles, MOVE inside box, and NONE outside")
|
||||
public void testDragHandleDetection() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
panel.setBlockSelectMode(true);
|
||||
panel.setTrimRectSizingHandles(true);
|
||||
|
||||
// Without selection, should return NONE
|
||||
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(100, 100));
|
||||
|
||||
// Select rectangle: rows 2 to 5, cols 10 to 20
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
int cols = sb.getCols();
|
||||
panel.setSelectionRange(2 * cols + 10, 5 * cols + 20);
|
||||
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
int cw = panel.cellWidth;
|
||||
int ch = panel.cellHeight;
|
||||
|
||||
int x1 = ox + 10 * cw;
|
||||
int y1 = oy + 2 * ch;
|
||||
int x2 = ox + 21 * cw;
|
||||
int y2 = oy + 6 * ch;
|
||||
int xm = (x1 + x2) / 2;
|
||||
int ym = (y1 + y2) / 2;
|
||||
|
||||
// Test 8 handles
|
||||
assertEquals(TerminalPanel.DragHandle.NW, panel.getDragHandleAt(x1, y1));
|
||||
assertEquals(TerminalPanel.DragHandle.N, panel.getDragHandleAt(xm, y1));
|
||||
assertEquals(TerminalPanel.DragHandle.NE, panel.getDragHandleAt(x2, y1));
|
||||
assertEquals(TerminalPanel.DragHandle.E, panel.getDragHandleAt(x2, ym));
|
||||
assertEquals(TerminalPanel.DragHandle.SE, panel.getDragHandleAt(x2, y2));
|
||||
assertEquals(TerminalPanel.DragHandle.S, panel.getDragHandleAt(xm, y2));
|
||||
assertEquals(TerminalPanel.DragHandle.SW, panel.getDragHandleAt(x1, y2));
|
||||
assertEquals(TerminalPanel.DragHandle.W, panel.getDragHandleAt(x1, ym));
|
||||
|
||||
// Test inside box -> MOVE
|
||||
assertEquals(TerminalPanel.DragHandle.MOVE, panel.getDragHandleAt(xm, ym));
|
||||
|
||||
// Test far outside -> NONE
|
||||
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(0, 0));
|
||||
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(x2 + 50, y2 + 50));
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Cursor mapping for all drag handles")
|
||||
public void testCursorForDragHandles() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
assertEquals(Cursor.NW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NW).getType());
|
||||
assertEquals(Cursor.N_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.N).getType());
|
||||
assertEquals(Cursor.NE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NE).getType());
|
||||
assertEquals(Cursor.E_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.E).getType());
|
||||
assertEquals(Cursor.SE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SE).getType());
|
||||
assertEquals(Cursor.S_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.S).getType());
|
||||
assertEquals(Cursor.SW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SW).getType());
|
||||
assertEquals(Cursor.W_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.W).getType());
|
||||
assertEquals(Cursor.MOVE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.MOVE).getType());
|
||||
assertEquals(Cursor.DEFAULT_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NONE).getType());
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("trimRectRemainAfterEdit controls whether selection is retained after copy")
|
||||
public void testTrimRectRemainAfterEdit() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
|
||||
sb.getCell(0).ucs4 = 'A';
|
||||
panel.setSelectionRange(0, 5);
|
||||
assertTrue(panel.hasSelection());
|
||||
|
||||
// By default (false), copySelection() should clear the selection
|
||||
Settings.setTrimRectRemainAfterEdit(false);
|
||||
panel.copySelection();
|
||||
assertFalse(panel.hasSelection(), "Selection should be cleared after copy when remainAfterEdit is false");
|
||||
|
||||
// When set to true, copySelection() should preserve the selection
|
||||
Settings.setTrimRectRemainAfterEdit(true);
|
||||
panel.setSelectionRange(0, 5);
|
||||
assertTrue(panel.hasSelection());
|
||||
panel.copySelection();
|
||||
assertTrue(panel.hasSelection(), "Selection should remain active after copy when remainAfterEdit is true");
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("cutSelection clears unprotected cells, sets FA_MODIFY, and respects remainAfterEdit")
|
||||
public void testCutSelectionBehavior() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
|
||||
// Field at 0: Unprotected field
|
||||
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
String data = "TESTDATA";
|
||||
for (int i = 0; i < data.length(); i++) {
|
||||
sb.getCell(1 + i).ucs4 = data.charAt(i);
|
||||
}
|
||||
|
||||
// Select the field contents
|
||||
panel.setSelectionRange(1, 8);
|
||||
assertTrue(panel.hasSelection());
|
||||
|
||||
Settings.setTrimRectRemainAfterEdit(false);
|
||||
panel.cutSelection();
|
||||
|
||||
// Characters should be replaced with spaces
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
assertEquals(' ', sb.getCell(i).ucs4, "Cell " + i + " should be erased to space");
|
||||
}
|
||||
// FA_MODIFY should remain or be set
|
||||
assertTrue((sb.getCell(0).ec & FA_MODIFY) != 0, "FA_MODIFY should be set on field attribute");
|
||||
assertFalse(panel.hasSelection(), "Selection should be cleared when remainAfterEdit is false");
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("KeyBindings exportKmp generates HoD format and round-trips through parseKmp")
|
||||
public void testKeyBindingsExportAndParseRoundTrip() throws Exception {
|
||||
Map<String, String> keymap = new LinkedHashMap<>();
|
||||
keymap.put("ENTER", "ENTER");
|
||||
keymap.put("shift TAB", "TAB");
|
||||
keymap.put("CLEAR", "PAUSE");
|
||||
keymap.put("PF1", "F1");
|
||||
keymap.put("PF13", "shift F1");
|
||||
keymap.put("ESCAPE", "ESCAPE");
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
KeyBindings.exportKmp(writer, keymap);
|
||||
String exported = writer.toString();
|
||||
|
||||
assertTrue(exported.contains("[KeyRemap]"), "Exported KMP must contain [KeyRemap] section header");
|
||||
assertTrue(exported.contains("[enter]"), "Exported KMP must contain HoD mnemonics");
|
||||
assertTrue(exported.contains("[backtab]"), "Exported KMP must contain [backtab]");
|
||||
|
||||
// Roundtrip parse
|
||||
Map<String, String> parsed = KeyBindings.parseKmp(new StringReader(exported));
|
||||
assertNotNull(parsed);
|
||||
assertEquals("ENTER", parsed.get("ENTER"));
|
||||
assertEquals("TAB", parsed.get("shift TAB"));
|
||||
assertEquals("PAUSE", parsed.get("CLEAR"));
|
||||
assertEquals("F1", parsed.get("PF1"));
|
||||
assertEquals("shift F1", parsed.get("PF13"));
|
||||
assertEquals("ESCAPE", parsed.get("ESCAPE"));
|
||||
}
|
||||
}
|
||||
@@ -14,41 +14,94 @@ public class StatusBarTest {
|
||||
|
||||
@Test
|
||||
public void testStatusBarDoesNotContainLightPenButton() {
|
||||
StatusBar statusBar = new StatusBar();
|
||||
try {
|
||||
StatusBar statusBar = new StatusBar();
|
||||
|
||||
// Ensure no JButton exists in StatusBar components (lightpen button removed)
|
||||
for (Component comp : statusBar.getComponents()) {
|
||||
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
|
||||
// Ensure no JButton exists in StatusBar components (lightpen button removed)
|
||||
for (Component comp : statusBar.getComponents()) {
|
||||
assertFalse(comp instanceof JButton, "StatusBar should not contain any JButton (lightpen removed from bottom bar)");
|
||||
}
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusBarUpdatesWithClient() {
|
||||
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
|
||||
config.setLuName("TSU001");
|
||||
config.setCodePage("1047");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
try {
|
||||
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_4, false);
|
||||
config.setLuName("TSU001");
|
||||
config.setCodePage("1047");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
|
||||
StatusBar statusBar = new StatusBar();
|
||||
statusBar.setClient(client, null);
|
||||
statusBar.updateStatus();
|
||||
StatusBar statusBar = new StatusBar();
|
||||
statusBar.setClient(client, null);
|
||||
statusBar.updateStatus();
|
||||
|
||||
// Check labels
|
||||
boolean foundCodePage = false;
|
||||
boolean foundModel = false;
|
||||
for (Component comp : statusBar.getComponents()) {
|
||||
if (comp instanceof JLabel) {
|
||||
JLabel label = (JLabel) comp;
|
||||
if ("CP1047".equals(label.getText())) {
|
||||
foundCodePage = true;
|
||||
}
|
||||
if (label.getText() != null && label.getText().contains("3279-4")) {
|
||||
foundModel = true;
|
||||
// Check labels
|
||||
boolean foundCodePage = false;
|
||||
boolean foundModel = false;
|
||||
for (Component comp : statusBar.getComponents()) {
|
||||
if (comp instanceof JLabel) {
|
||||
JLabel label = (JLabel) comp;
|
||||
if ("CP1047".equals(label.getText())) {
|
||||
foundCodePage = true;
|
||||
}
|
||||
if (label.getText() != null && label.getText().contains("3279-4")) {
|
||||
foundModel = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(foundCodePage, "Should display CP1047");
|
||||
assertTrue(foundModel, "Should display IBM-3279-4 model info");
|
||||
assertTrue(foundCodePage, "Should display CP1047");
|
||||
assertTrue(foundModel, "Should display IBM-3279-4 model info");
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusBarXSystemAndXProt() {
|
||||
try {
|
||||
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_2, false);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
|
||||
|
||||
StatusBar statusBar = new StatusBar();
|
||||
statusBar.setClient(client, null);
|
||||
statusBar.updateStatus();
|
||||
|
||||
// Initially not locked
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 1. Setup protected field and type into it -> triggers X PROT
|
||||
client.getScreenBuffer().setFieldAttribute(0, (byte) haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT);
|
||||
client.getScreenBuffer().setCursorAddress(1);
|
||||
client.typeCharacter('A');
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X PROT", statusBar.getLockStatusText());
|
||||
|
||||
// 2. Reset -> clears X PROT
|
||||
client.reset();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 3. Send Enter -> triggers X SYSTEM
|
||||
client.sendEnter();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X SYSTEM", statusBar.getLockStatusText());
|
||||
|
||||
// 4. Host unlocks keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
statusBar.updateStatus();
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 5. Subsequent Enter -> triggers X SYSTEM again
|
||||
client.sendEnter();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X SYSTEM", statusBar.getLockStatusText());
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TerminalPanelSelectionTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
|
||||
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
|
||||
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
|
||||
String headerText = " ISPF PRIMARY OPTION MENU";
|
||||
for (int i = 0; i < headerText.length(); i++) {
|
||||
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
|
||||
sb.getCell(1 + i).ec = (byte) (0x40 + i);
|
||||
}
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
// Cell dimensions in panel
|
||||
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
|
||||
MouseListener[] mouseListeners = panel.getMouseListeners();
|
||||
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
|
||||
assertTrue(mouseListeners.length > 0);
|
||||
assertTrue(motionListeners.length > 0);
|
||||
|
||||
MouseListener ml = mouseListeners[0];
|
||||
MouseMotionListener mml = motionListeners[0];
|
||||
|
||||
// 1. Mouse Pressed at cell (row 0, col 2)
|
||||
int startX = ox + 2 * cellW + cellW / 2;
|
||||
int startY = oy + cellH / 2;
|
||||
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, startX, startY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mousePressed(pressEvent);
|
||||
|
||||
// 2. Drag to cell (row 0, col 15)
|
||||
int dragX = ox + 15 * cellW + cellW / 2;
|
||||
int dragY = oy + cellH / 2;
|
||||
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
|
||||
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
|
||||
mml.mouseDragged(dragEvent);
|
||||
|
||||
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
|
||||
|
||||
// 3. Mouse Released at cell (row 0, col 15)
|
||||
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mouseReleased(releaseEvent);
|
||||
|
||||
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
|
||||
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
|
||||
String selected = panel.getSelectedText();
|
||||
assertNotNull(selected);
|
||||
assertTrue(selected.length() > 0, "Selected text must not be empty");
|
||||
|
||||
// 4. A subsequent single click without drag should clear the selection and move the cursor
|
||||
int clickX = ox + 5 * cellW + cellW / 2;
|
||||
int clickY = oy + cellH / 2;
|
||||
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mousePressed(clickPress);
|
||||
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mouseReleased(clickRelease);
|
||||
|
||||
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
|
||||
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
|
||||
|
||||
} catch (HeadlessException e) {
|
||||
// Handled gracefully in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Mouse press and release on TerminalPanel requests focus")
|
||||
public void testTerminalPanelMousePressRequestsFocus() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
assertTrue(panel.isFocusable());
|
||||
|
||||
MouseListener[] mouseListeners = panel.getMouseListeners();
|
||||
assertTrue(mouseListeners.length > 0);
|
||||
|
||||
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, 50, 50, 1, false, MouseEvent.BUTTON1);
|
||||
mouseListeners[0].mousePressed(pressEvent);
|
||||
|
||||
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, 50, 50, 1, false, MouseEvent.BUTTON1);
|
||||
mouseListeners[0].mouseReleased(releaseEvent);
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ThemeManagerTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUIThemeEnum() {
|
||||
assertEquals("Dark Mode", UITheme.DARK.getDisplayName());
|
||||
assertEquals("Light Mode", UITheme.LIGHT.getDisplayName());
|
||||
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("DARK"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("dark"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("Dark Mode"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("LIGHT"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("light"));
|
||||
assertEquals(UITheme.LIGHT, UITheme.fromString("Light Mode"));
|
||||
assertEquals(UITheme.DARK, UITheme.fromString("invalid_value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testColorTokensContrast() {
|
||||
for (UITheme theme : UITheme.values()) {
|
||||
Color bg = ThemeManager.getBgMain(theme);
|
||||
Color fg = ThemeManager.getFgMain(theme);
|
||||
assertNotNull(bg);
|
||||
assertNotNull(fg);
|
||||
|
||||
// Compute luminance contrast
|
||||
double bgLum = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue());
|
||||
double fgLum = (0.299 * fg.getRed() + 0.587 * fg.getGreen() + 0.114 * fg.getBlue());
|
||||
double lumDiff = Math.abs(bgLum - fgLum);
|
||||
|
||||
// Minimum luminance difference to guarantee readability
|
||||
assertTrue(lumDiff > 120, "Theme " + theme + " text contrast luminance difference " + lumDiff + " must be > 120");
|
||||
|
||||
// Verify Menu Bar colors
|
||||
Color menuBg = ThemeManager.getMenuBarBg(theme);
|
||||
Color menuFg = ThemeManager.getMenuItemFg(theme);
|
||||
double menuBgLum = (0.299 * menuBg.getRed() + 0.587 * menuBg.getGreen() + 0.114 * menuBg.getBlue());
|
||||
double menuFgLum = (0.299 * menuFg.getRed() + 0.587 * menuFg.getGreen() + 0.114 * menuFg.getBlue());
|
||||
assertTrue(Math.abs(menuBgLum - menuFgLum) > 100, "Menu contrast in " + theme + " must be high");
|
||||
|
||||
// Verify Status Bar colors
|
||||
Color sbBg = ThemeManager.getStatusBarBg(theme);
|
||||
Color sbNormal = ThemeManager.getOiaFgNormal(theme);
|
||||
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);
|
||||
Color btnDefFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.DEFAULT, theme);
|
||||
assertNotNull(btnDefBg);
|
||||
assertNotNull(btnDefFg);
|
||||
|
||||
Color btnPriBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.PRIMARY, theme);
|
||||
Color btnPriFg = ThemeManager.getButtonFg(ThemeManager.ButtonVariant.PRIMARY, theme);
|
||||
assertNotNull(btnPriBg);
|
||||
assertNotNull(btnPriFg);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThemeChangeListeners() {
|
||||
AtomicReference<UITheme> notifiedTheme = new AtomicReference<>(null);
|
||||
ThemeManager.addThemeChangeListener(notifiedTheme::set);
|
||||
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
assertEquals(UITheme.LIGHT, ThemeManager.getTheme());
|
||||
assertEquals(UITheme.LIGHT, notifiedTheme.get());
|
||||
assertTrue(ThemeManager.isLight());
|
||||
assertFalse(ThemeManager.isDark());
|
||||
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
assertEquals(UITheme.DARK, ThemeManager.getTheme());
|
||||
assertEquals(UITheme.DARK, notifiedTheme.get());
|
||||
assertTrue(ThemeManager.isDark());
|
||||
assertFalse(ThemeManager.isLight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComponentStyling() {
|
||||
try {
|
||||
JButton btn = new JButton("Test");
|
||||
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
assertNotNull(btn.getUI());
|
||||
assertTrue(btn.getUI() instanceof ThemeManager.StyledButtonUI);
|
||||
|
||||
JTextField tf = new JTextField("Test");
|
||||
ThemeManager.styleTextField(tf);
|
||||
assertNotNull(tf.getCaretColor());
|
||||
|
||||
JTextArea ta = new JTextArea("Test area");
|
||||
ThemeManager.styleTextArea(ta);
|
||||
assertNotNull(ta.getCaretColor());
|
||||
|
||||
JComboBox<String> cb = new JComboBox<>(new String[]{"A", "B"});
|
||||
ThemeManager.styleComboBox(cb);
|
||||
assertNotNull(cb.getUI());
|
||||
|
||||
JTable table = new JTable(new DefaultTableModel(new Object[]{"Col1"}, 1));
|
||||
ThemeManager.styleTable(table);
|
||||
assertNotNull(table.getSelectionBackground());
|
||||
|
||||
JTabbedPane tp = new JTabbedPane();
|
||||
tp.addTab("Tab1", new JPanel());
|
||||
ThemeManager.styleTabbedPane(tp);
|
||||
assertNotNull(tp.getUI());
|
||||
|
||||
JMenuBar mb = new JMenuBar();
|
||||
ThemeManager.styleMenuBar(mb);
|
||||
|
||||
JMenu menu = new JMenu("File");
|
||||
ThemeManager.styleMenu(menu);
|
||||
|
||||
JMenuItem mi = new JMenuItem("Open");
|
||||
ThemeManager.styleMenuItem(mi);
|
||||
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
ThemeManager.stylePopupMenu(popup);
|
||||
|
||||
JCheckBox chk = new JCheckBox("Check");
|
||||
ThemeManager.styleCheckBox(chk);
|
||||
|
||||
JRadioButton rb = new JRadioButton("Radio");
|
||||
ThemeManager.styleRadioButton(rb);
|
||||
|
||||
Border tb = ThemeManager.createTitledBorder("Title");
|
||||
assertNotNull(tb);
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveApplyTheme() {
|
||||
try {
|
||||
JPanel root = new JPanel(new BorderLayout());
|
||||
JButton b = new JButton("OK");
|
||||
JTextField f = new JTextField("Data");
|
||||
JTabbedPane tp = new JTabbedPane();
|
||||
JPanel tabContent = new JPanel();
|
||||
JLabel lbl = new JLabel("Label");
|
||||
tabContent.add(lbl);
|
||||
tp.addTab("T1", tabContent);
|
||||
|
||||
root.add(b, BorderLayout.NORTH);
|
||||
root.add(f, BorderLayout.CENTER);
|
||||
root.add(tp, BorderLayout.SOUTH);
|
||||
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
ThemeManager.applyTheme(root);
|
||||
|
||||
assertEquals(ThemeManager.getBgPanel(UITheme.LIGHT), root.getBackground());
|
||||
assertEquals(ThemeManager.getFgMain(UITheme.LIGHT), lbl.getForeground());
|
||||
|
||||
ThemeManager.setTheme(UITheme.DARK);
|
||||
ThemeManager.applyTheme(root);
|
||||
|
||||
assertEquals(ThemeManager.getBgPanel(UITheme.DARK), root.getBackground());
|
||||
assertEquals(ThemeManager.getFgMain(UITheme.DARK), lbl.getForeground());
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsPersistence() throws Exception {
|
||||
Settings.setJavaUiTheme(UITheme.LIGHT);
|
||||
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
|
||||
|
||||
File tmpIni = File.createTempFile("j3270_test_config", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
// Write INI file with javaUiTheme = dark
|
||||
try (PrintWriter pw = new PrintWriter(tmpIni)) {
|
||||
pw.println("[j3270]");
|
||||
pw.println("javaUiTheme = dark");
|
||||
pw.println("fontFamily = Monospaced");
|
||||
pw.println("fontSize = 16");
|
||||
}
|
||||
|
||||
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
|
||||
assertEquals(UITheme.DARK, Settings.getJavaUiTheme());
|
||||
|
||||
// Switch to light and export
|
||||
Settings.setJavaUiTheme(UITheme.LIGHT);
|
||||
File exportFile = File.createTempFile("j3270_export_config", ".ini");
|
||||
exportFile.deleteOnExit();
|
||||
|
||||
Settings.exportToIniFile(exportFile.getAbsolutePath());
|
||||
String exportedContent = new String(Files.readAllBytes(exportFile.toPath()));
|
||||
assertTrue(exportedContent.toLowerCase().contains("javauitheme = light"));
|
||||
|
||||
// Reload exported file
|
||||
Settings.loadFromIniFile(exportFile.getAbsolutePath());
|
||||
assertEquals(UITheme.LIGHT, Settings.getJavaUiTheme());
|
||||
} finally {
|
||||
tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusBarLightModeLegibility() {
|
||||
try {
|
||||
ThemeManager.setTheme(UITheme.LIGHT);
|
||||
StatusBar sb = new StatusBar();
|
||||
sb.applyTheme(UITheme.LIGHT);
|
||||
|
||||
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), sb.getBackground());
|
||||
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited());
|
||||
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited(UITheme.LIGHT));
|
||||
|
||||
// Verify recursive applyTheme does not break StatusBar background
|
||||
ThemeManager.applyTheme(sb, UITheme.LIGHT);
|
||||
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), sb.getBackground());
|
||||
} catch (HeadlessException e) {
|
||||
// Handled in headless CI
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUiFontRetrievalAndDefaults() {
|
||||
String prevFamily = Settings.getUiFontFamily();
|
||||
int prevSize = Settings.getUiFontSize();
|
||||
|
||||
try {
|
||||
Settings.setUiFontFamily("Dialog");
|
||||
Settings.setUiFontSize(16);
|
||||
|
||||
Font uiFont = ThemeManager.getUiFont();
|
||||
assertEquals("Dialog", uiFont.getFamily());
|
||||
assertEquals(16, uiFont.getSize());
|
||||
assertEquals(Font.PLAIN, uiFont.getStyle());
|
||||
|
||||
Font boldOffset = ThemeManager.getUiFont(Font.BOLD, 2);
|
||||
assertEquals(18, boldOffset.getSize());
|
||||
assertEquals(Font.BOLD, boldOffset.getStyle());
|
||||
|
||||
Font monoFont = ThemeManager.getMonospacedUiFont();
|
||||
assertEquals(Font.MONOSPACED, monoFont.getName());
|
||||
assertEquals(16, monoFont.getSize());
|
||||
|
||||
// Test applyUIManagerDefaults sets font properties
|
||||
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
|
||||
assertEquals(uiFont, UIManager.get("Button.font"));
|
||||
assertEquals(uiFont, UIManager.get("Label.font"));
|
||||
assertEquals(uiFont, UIManager.get("Menu.font"));
|
||||
assertEquals(uiFont, UIManager.get("Table.font"));
|
||||
assertEquals(monoFont, UIManager.get("TextArea.font"));
|
||||
} finally {
|
||||
Settings.setUiFontFamily(prevFamily);
|
||||
Settings.setUiFontSize(prevSize);
|
||||
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLabelFontNotIncrementedOnRepeatedThemeApplication() {
|
||||
String prevFamily = Settings.getUiFontFamily();
|
||||
int prevSize = Settings.getUiFontSize();
|
||||
|
||||
try {
|
||||
Settings.setUiFontFamily("Dialog");
|
||||
Settings.setUiFontSize(16);
|
||||
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
|
||||
|
||||
JLabel label = new JLabel("Settings Label");
|
||||
ThemeManager.applyTheme(label);
|
||||
assertEquals(16, label.getFont().getSize(), "Initial label font size must match configured uiFontSize");
|
||||
|
||||
// Repeated theme applications (e.g. subsequent clicks of 'Apply' in Settings dialog)
|
||||
// must never increment font size
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
ThemeManager.applyTheme(label);
|
||||
assertEquals(16, label.getFont().getSize(),
|
||||
"Label font size must remain 16 on subsequent application #" + i);
|
||||
}
|
||||
|
||||
// Test label with custom delta
|
||||
JLabel headerLabel = new JLabel("Header Label");
|
||||
headerLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
|
||||
headerLabel.putClientProperty("uiFontSizeDelta", 3);
|
||||
ThemeManager.applyTheme(headerLabel);
|
||||
assertEquals(19, headerLabel.getFont().getSize(), "Custom header label should have base + 3 size");
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
ThemeManager.applyTheme(headerLabel);
|
||||
assertEquals(19, headerLabel.getFont().getSize(),
|
||||
"Header label font size must remain 19 on subsequent application #" + i);
|
||||
}
|
||||
} finally {
|
||||
Settings.setUiFontFamily(prevFamily);
|
||||
Settings.setUiFontSize(prevSize);
|
||||
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Communications event matching IBM Host On-Demand ECLCommEvent.
|
||||
*/
|
||||
public class ECLCommEvent extends ECLEvent {
|
||||
|
||||
private final boolean status;
|
||||
private final int eventType;
|
||||
|
||||
public ECLCommEvent(Object object, boolean status) {
|
||||
super(object);
|
||||
this.status = status;
|
||||
this.eventType = status ? 1 : 2;
|
||||
}
|
||||
|
||||
public ECLCommEvent(Object object, int eventType, boolean status) {
|
||||
super(object);
|
||||
this.eventType = eventType;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public boolean connected() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public int getEventType() {
|
||||
return this.eventType;
|
||||
}
|
||||
|
||||
public ECLConnection GetConnection() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLConnection) return (ECLConnection) src;
|
||||
if (src instanceof haus.nightmare.lib3270j.ecl.ECLConnection) {
|
||||
return new ECLConnection((haus.nightmare.lib3270j.ecl.ECLConnection) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Communications notification interface matching IBM Host On-Demand ECLCommNotify.
|
||||
*/
|
||||
public interface ECLCommNotify {
|
||||
void NotifyEvent(ECLConnection conn, boolean connected);
|
||||
default void NotifyStop(ECLConnection conn, int reason) {}
|
||||
default void NotifyError(ECLConnection conn, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Communications and connection management facade matching IBM Host On-Demand ECLConnection.
|
||||
*/
|
||||
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection implements ECLConstants {
|
||||
|
||||
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
|
||||
|
||||
public ECLConnection() {
|
||||
super();
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(Properties props) {
|
||||
super(props);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(String host, int port) {
|
||||
super(host, port);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) {
|
||||
super(session);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLConnection delegate) {
|
||||
super(delegate != null ? delegate.getSession() : null);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
private void setupListener() {
|
||||
RegisterCommEvent(new haus.nightmare.lib3270j.ecl.ECLCommListener() {
|
||||
@Override
|
||||
public void CommNotifyEvent(haus.nightmare.lib3270j.ecl.ECLCommEvent event) {
|
||||
boolean conn = event != null && event.isConnected();
|
||||
for (ECLCommNotify n : commNotifies) {
|
||||
try {
|
||||
n.NotifyEvent(ECLConnection.this, conn);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void RegisterCommEvent(ECLCommNotify notify) {
|
||||
if (notify != null && !commNotifies.contains(notify)) {
|
||||
commNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerCommEvent(ECLCommNotify notify) { RegisterCommEvent(notify); }
|
||||
|
||||
public void UnregisterCommEvent(ECLCommNotify notify) {
|
||||
if (notify != null) {
|
||||
commNotifies.remove(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
|
||||
|
||||
@Override
|
||||
public void StartCommunication() throws IOException {
|
||||
super.StartCommunication();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StopCommunication() {
|
||||
super.StopCommunication();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* IBM Host Access Class Library (HACL) common constants.
|
||||
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLConstants.
|
||||
*/
|
||||
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
|
||||
|
||||
// Presentation space logical planes (HoD bitmask values)
|
||||
int TEXT_PLANE = 1;
|
||||
int COLOR_PLANE = 2;
|
||||
int DBCS_PLANE = 4;
|
||||
int GRID_PLANE = 8;
|
||||
int FIELD_PLANE = 16;
|
||||
int EXFIELD_PLANE = 32;
|
||||
int NLS_PLANE = 64;
|
||||
int HOST_PLANE = 128;
|
||||
int RAWTEXT_PLANE = 256;
|
||||
int COLOR_ATTRIBUTES_PLANE = 512;
|
||||
int ALL_PLANES = 319;
|
||||
|
||||
// Search directions
|
||||
int SEARCH_FORWARD = 1;
|
||||
int SEARCH_BACKWARD = 2;
|
||||
|
||||
// Field filters
|
||||
int ALL_FIELDS = 0;
|
||||
int MODIFIED_FIELD = 1;
|
||||
int ALPHANUM_FIELD = 2;
|
||||
int HILIGHT_FIELD = 16;
|
||||
int PROTECTED_FIELD = 32;
|
||||
int DISPLAYABLE_FIELD = 64;
|
||||
int PENDETECTABLE_FIELD = 128;
|
||||
int UNMODIFIED_FIELD = 256;
|
||||
int NUMERIC_FIELD = 512;
|
||||
int LOLIGHT_FIELD = 4096;
|
||||
int UNPROTECTED_FIELD = 8192;
|
||||
int NON_DISPLAYABLE_FIELD = 16384;
|
||||
int NON_PENDETECTABLE_FIELD = 32768;
|
||||
|
||||
// Standard Mnemonics Constants
|
||||
String ENTER_STR = "[enter]";
|
||||
String CLEAR_STR = "[clear]";
|
||||
String RESET_STR = "[reset]";
|
||||
String TAB_STR = "[tab]";
|
||||
String FWDTAB_STR = "[tab]";
|
||||
String BACKTAB_STR = "[backtab]";
|
||||
String BACKSP_STR = "[backspace]";
|
||||
String DELCHAR_STR = "[delete]";
|
||||
String INSERT_STR = "[insert]";
|
||||
String HOME_STR = "[home]";
|
||||
String NEWLINE_STR = "[newline]";
|
||||
String ENDLINE_STR = "[eof]";
|
||||
String ERASEEOF_STR = "[eraseeof]";
|
||||
String ERASEFLD_STR = "[erasefld]";
|
||||
String ERASEINPUT_STR = "[erinp]";
|
||||
String ATTN_STR = "[attn]";
|
||||
String SYSREQ_STR = "[sysreq]";
|
||||
String DUP_STR = "[dup]";
|
||||
String FLDMRK_STR = "[fieldmark]";
|
||||
String PA1_STR = "[pa1]";
|
||||
String PA2_STR = "[pa2]";
|
||||
String PA3_STR = "[pa3]";
|
||||
String CURUP_STR = "[up]";
|
||||
String CURDOWN_STR = "[down]";
|
||||
String CURLEFT_STR = "[left]";
|
||||
String CURRIGHT_STR = "[right]";
|
||||
String PAGEUP_STR = "[pageup]";
|
||||
String PAGEDWN_STR = "[pagedn]";
|
||||
String COPY_STR = "[copy]";
|
||||
String PASTE_STR = "[paste]";
|
||||
String CUT_STR = "[cut]";
|
||||
String SELECTALL_STR = "[selectall]";
|
||||
|
||||
// Function keys F1 - F24
|
||||
String F1_STR = "[pf1]";
|
||||
String F2_STR = "[pf2]";
|
||||
String F3_STR = "[pf3]";
|
||||
String F4_STR = "[pf4]";
|
||||
String F5_STR = "[pf5]";
|
||||
String F6_STR = "[pf6]";
|
||||
String F7_STR = "[pf7]";
|
||||
String F8_STR = "[pf8]";
|
||||
String F9_STR = "[pf9]";
|
||||
String F10_STR = "[pf10]";
|
||||
String F11_STR = "[pf11]";
|
||||
String F12_STR = "[pf12]";
|
||||
String F13_STR = "[pf13]";
|
||||
String F14_STR = "[pf14]";
|
||||
String F15_STR = "[pf15]";
|
||||
String F16_STR = "[pf16]";
|
||||
String F17_STR = "[pf17]";
|
||||
String F18_STR = "[pf18]";
|
||||
String F19_STR = "[pf19]";
|
||||
String F20_STR = "[pf20]";
|
||||
String F21_STR = "[pf21]";
|
||||
String F22_STR = "[pf22]";
|
||||
String F23_STR = "[pf23]";
|
||||
String F24_STR = "[pf24]";
|
||||
|
||||
// Shift F1 - F24
|
||||
String SHIFT_F1_STR = "[Spf1]";
|
||||
String SHIFT_F2_STR = "[Spf2]";
|
||||
String SHIFT_F3_STR = "[Spf3]";
|
||||
String SHIFT_F4_STR = "[Spf4]";
|
||||
String SHIFT_F5_STR = "[Spf5]";
|
||||
String SHIFT_F6_STR = "[Spf6]";
|
||||
String SHIFT_F7_STR = "[Spf7]";
|
||||
String SHIFT_F8_STR = "[Spf8]";
|
||||
String SHIFT_F9_STR = "[Spf9]";
|
||||
String SHIFT_F10_STR = "[Spf10]";
|
||||
String SHIFT_F11_STR = "[Spf11]";
|
||||
String SHIFT_F12_STR = "[Spf12]";
|
||||
String SHIFT_F13_STR = "[Spf13]";
|
||||
String SHIFT_F14_STR = "[Spf14]";
|
||||
String SHIFT_F15_STR = "[Spf15]";
|
||||
String SHIFT_F16_STR = "[Spf16]";
|
||||
String SHIFT_F17_STR = "[Spf17]";
|
||||
String SHIFT_F18_STR = "[Spf18]";
|
||||
String SHIFT_F19_STR = "[Spf19]";
|
||||
String SHIFT_F20_STR = "[Spf20]";
|
||||
|
||||
// Field attributes
|
||||
short ATTR_PROTECTED = 32;
|
||||
short ATTR_ALPHANUM = 16;
|
||||
short ATTR_DISPLAY1 = 8;
|
||||
short ATTR_DISPLAY2 = 4;
|
||||
short ATTR_MDT = 1;
|
||||
|
||||
// Extended attributes
|
||||
short EXT_ATTR_3270_UNDERLINE = 192;
|
||||
short EXT_ATTR_3270_REVERSE = 128;
|
||||
short EXT_ATTR_3270_BLINK = 64;
|
||||
short EXT_ATTR_3270_EBCDIC = 0;
|
||||
|
||||
// Timeout
|
||||
int NO_TIMEOUT = -1;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* IBM Host Access Class Library (HACL) ECLErr checked exception facade.
|
||||
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLErr.
|
||||
*/
|
||||
public class ECLErr extends haus.nightmare.lib3270j.ecl.ECLErr {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLErr() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLErr(String text) {
|
||||
super(text);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum) {
|
||||
super(location, msgNum);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert) {
|
||||
super(location, msgNum, insert);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2) {
|
||||
super(location, msgNum, insert1, insert2);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
|
||||
super(location, msgNum, insert1, insert2, log);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
|
||||
super(location, msgNum, insert1, insert2, insert3);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, int reasonCode) {
|
||||
super(location, msgNum, reasonCode);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
|
||||
super(location, msgNum, insert1, reasonCode);
|
||||
}
|
||||
|
||||
public ECLErr(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ECLErr(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Discrete 3270 field facade matching IBM Host On-Demand ECLField.
|
||||
*/
|
||||
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements ECLConstants {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLField delegate;
|
||||
|
||||
public ECLField(haus.nightmare.lib3270j.ecl.ECLField delegate) {
|
||||
super(delegate != null ? delegate.getPS() : null,
|
||||
delegate != null ? delegate.getStart() : 0,
|
||||
delegate != null ? delegate.getDataStart() : 0,
|
||||
delegate != null ? delegate.getEnd() : 0,
|
||||
delegate != null ? delegate.getLength() : 0,
|
||||
(byte) (delegate != null ? delegate.getAttribute() : 0));
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public int GetStart() {
|
||||
return delegate != null ? delegate.GetStart() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStart() { return GetStart(); }
|
||||
|
||||
public int GetStartRow() {
|
||||
return delegate != null ? delegate.GetStartRow() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStartRow() { return GetStartRow(); }
|
||||
|
||||
public int GetStartCol() {
|
||||
return delegate != null ? delegate.GetStartCol() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStartCol() { return GetStartCol(); }
|
||||
|
||||
public int GetEnd() {
|
||||
return delegate != null ? delegate.GetEnd() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEnd() { return GetEnd(); }
|
||||
|
||||
public int GetEndRow() {
|
||||
return delegate != null ? delegate.GetEndRow() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEndRow() { return GetEndRow(); }
|
||||
|
||||
public int GetEndCol() {
|
||||
return delegate != null ? delegate.GetEndCol() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEndCol() { return GetEndCol(); }
|
||||
|
||||
public int GetLength() {
|
||||
return delegate != null ? delegate.GetLength() : 0;
|
||||
}
|
||||
|
||||
public int getLength() { return GetLength(); }
|
||||
|
||||
public String GetString() {
|
||||
return delegate != null ? delegate.getString() : "";
|
||||
}
|
||||
|
||||
public String getString() { return GetString(); }
|
||||
|
||||
public int GetString(char[] buffer, int len) throws ECLErr {
|
||||
if (buffer == null) throw new ECLErr("ECLField:GetString", "ECL0011", "1");
|
||||
String text = GetString();
|
||||
int count = Math.min(len, Math.min(buffer.length, text.length()));
|
||||
for (int i = 0; i < count; i++) {
|
||||
buffer[i] = text.charAt(i);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public void SetString(String text) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
delegate.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetText(String text) throws ECLErr {
|
||||
SetString(text);
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
if (delegate != null) {
|
||||
delegate.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean IsModified() {
|
||||
return delegate != null && delegate.isModified();
|
||||
}
|
||||
|
||||
public boolean isModified() { return IsModified(); }
|
||||
|
||||
public boolean IsProtected() {
|
||||
return delegate != null && delegate.isProtected();
|
||||
}
|
||||
|
||||
public boolean isProtected() { return IsProtected(); }
|
||||
|
||||
public boolean IsNumeric() {
|
||||
return delegate != null && delegate.isNumeric();
|
||||
}
|
||||
|
||||
public boolean isNumeric() { return IsNumeric(); }
|
||||
|
||||
public boolean IsHighIntensity() {
|
||||
return delegate != null && delegate.isHighIntensity();
|
||||
}
|
||||
|
||||
public boolean isHighIntensity() { return IsHighIntensity(); }
|
||||
|
||||
public boolean IsPenDetectable() {
|
||||
return delegate != null && delegate.isPenDetectable();
|
||||
}
|
||||
|
||||
public boolean isPenDetectable() { return IsPenDetectable(); }
|
||||
|
||||
public boolean IsDisplay() {
|
||||
return delegate != null && delegate.isDisplay();
|
||||
}
|
||||
|
||||
public boolean isDisplay() { return IsDisplay(); }
|
||||
|
||||
public short GetAttribute() {
|
||||
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
|
||||
}
|
||||
|
||||
public boolean IsDBCS() {
|
||||
return delegate != null && delegate.isDBCS();
|
||||
}
|
||||
|
||||
public boolean isDBCS() { return IsDBCS(); }
|
||||
|
||||
public boolean IsDBCSOnly() {
|
||||
return delegate != null && delegate.isDBCSOnly();
|
||||
}
|
||||
|
||||
public boolean isDBCSOnly() { return IsDBCSOnly(); }
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Collection of 3270 fields matching IBM Host On-Demand ECLFieldList.
|
||||
*/
|
||||
public class ECLFieldList extends Vector<ECLField> implements ECLConstants {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLFieldList delegate;
|
||||
private final ECLPS ps;
|
||||
|
||||
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate, ECLPS ps) {
|
||||
this.delegate = delegate;
|
||||
this.ps = ps;
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate) {
|
||||
this(delegate, null);
|
||||
}
|
||||
|
||||
private void refreshFields() {
|
||||
clear();
|
||||
if (delegate != null) {
|
||||
delegate.refresh();
|
||||
for (haus.nightmare.lib3270j.ecl.ECLField f : delegate.getFields()) {
|
||||
add(new ECLField(f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh() {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public void Refresh(int planes) throws ECLErr {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public int GetFieldCount() {
|
||||
return size();
|
||||
}
|
||||
|
||||
public int getFieldCount() { return GetFieldCount(); }
|
||||
|
||||
public ECLField GetFirstField() {
|
||||
return GetFirstField(0);
|
||||
}
|
||||
|
||||
public ECLField GetFirstField(int filter) {
|
||||
for (ECLField f : this) {
|
||||
if (matchesFilter(f, filter)) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField GetNextField(ECLField current) {
|
||||
return GetNextField(current, 0);
|
||||
}
|
||||
|
||||
public ECLField GetNextField(ECLField current, int filter) {
|
||||
if (current == null) return GetFirstField(filter);
|
||||
int idx = indexOf(current);
|
||||
if (idx >= 0) {
|
||||
for (int i = idx + 1; i < size(); i++) {
|
||||
ECLField f = get(i);
|
||||
if (matchesFilter(f, filter)) return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField FindField(int pos) throws ECLErr {
|
||||
if (ps != null && (pos < 1 || pos > ps.GetSize())) {
|
||||
throw new ECLErr("ECLFieldList:FindField", "ECL0010", "1", String.valueOf(pos));
|
||||
}
|
||||
for (ECLField f : this) {
|
||||
int start = f.GetStart();
|
||||
int end = f.GetEnd();
|
||||
if (start <= end) {
|
||||
if (pos >= start && pos <= end) return f;
|
||||
} else {
|
||||
// Wrapped field
|
||||
if (pos >= start || pos <= end) return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField FindField(int row, int col) throws ECLErr {
|
||||
if (ps == null) return null;
|
||||
int pos = ps.ConvertRowColToPos(row, col);
|
||||
return FindField(pos);
|
||||
}
|
||||
|
||||
private boolean matchesFilter(ECLField f, int filter) {
|
||||
if (filter == 0 || filter == ALL_FIELDS) return true;
|
||||
if ((filter & MODIFIED_FIELD) != 0 && !f.IsModified()) return false;
|
||||
if ((filter & PROTECTED_FIELD) != 0 && !f.IsProtected()) return false;
|
||||
if ((filter & UNPROTECTED_FIELD) != 0 && f.IsProtected()) return false;
|
||||
if ((filter & NUMERIC_FIELD) != 0 && !f.IsNumeric()) return false;
|
||||
if ((filter & HILIGHT_FIELD) != 0 && !f.IsHighIntensity()) return false;
|
||||
if ((filter & DISPLAYABLE_FIELD) != 0 && !f.IsDisplay()) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Host Print Session facade matching IBM Host On-Demand ECLHostPrintSession.
|
||||
*/
|
||||
public class ECLHostPrintSession extends ECLSession {
|
||||
|
||||
public static final String SESSION_PRINT_SESSION_LABEL_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_TYPE_DEFAULT = "5";
|
||||
public static final String SESSION_PRINT_SESSION_HOST_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_HOST_PORT_DEFAULT = "23";
|
||||
public static final String SESSION_PRINT_SESSION_LU_NAME_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_AUTOCONNECT_DEFAULT = "true";
|
||||
public static final String SESSION_PRINT_SESSION_AUTORECONNECT_DEFAULT = "true";
|
||||
public static final String SESSION_LU_TYPE = "printerLUType";
|
||||
|
||||
public ECLHostPrintSession(Properties properties) throws ECLErr {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public ECLHostPrintSession() throws ECLErr {
|
||||
super();
|
||||
}
|
||||
|
||||
public void cancelPrintJob() {
|
||||
ECLPS ps = GetPS();
|
||||
if (ps != null) {
|
||||
try {
|
||||
ps.SendKeys("[cancelpjob]");
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelPrintJob() {
|
||||
cancelPrintJob();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Operator Information Area facade matching IBM Host On-Demand ECLOIA.
|
||||
*/
|
||||
public class ECLOIA implements ECLConstants {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLOIA delegate;
|
||||
private final List<ECLOIANotify> oiaNotifies = new CopyOnWriteArrayList<>();
|
||||
|
||||
public ECLOIA(haus.nightmare.lib3270j.ecl.ECLOIA delegate) {
|
||||
this.delegate = delegate;
|
||||
if (delegate != null) {
|
||||
delegate.RegisterOIAEvent(new haus.nightmare.lib3270j.ecl.ECLOIAListener() {
|
||||
@Override
|
||||
public void OIANotifyEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent event) {
|
||||
ECLOIAEvent facadeEvt = new ECLOIAEvent(event);
|
||||
for (ECLOIANotify n : oiaNotifies) {
|
||||
try {
|
||||
n.OIANotifyEvent(facadeEvt);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterOIAEvent(ECLOIANotify notify) {
|
||||
if (notify != null && !oiaNotifies.contains(notify)) {
|
||||
oiaNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerOIAEvent(ECLOIANotify notify) { RegisterOIAEvent(notify); }
|
||||
|
||||
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
|
||||
if (delegate != null) {
|
||||
delegate.RegisterOIAEvent(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterOIAEvent(ECLOIANotify notify) {
|
||||
if (notify != null) {
|
||||
oiaNotifies.remove(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterOIAEvent(ECLOIANotify notify) { UnregisterOIAEvent(notify); }
|
||||
|
||||
public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
|
||||
if (delegate != null) {
|
||||
delegate.UnregisterOIAEvent(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean IsAlphanumeric() {
|
||||
return delegate != null && delegate.IsAlphanumeric();
|
||||
}
|
||||
|
||||
public boolean isAlphanumeric() { return IsAlphanumeric(); }
|
||||
|
||||
public boolean IsNumeric() {
|
||||
return delegate != null && delegate.IsNumeric();
|
||||
}
|
||||
|
||||
public boolean isNumeric() { return IsNumeric(); }
|
||||
|
||||
public boolean IsDBCS() {
|
||||
return delegate != null && delegate.IsDBCS();
|
||||
}
|
||||
|
||||
public boolean isDBCS() { return IsDBCS(); }
|
||||
|
||||
public boolean IsInsertMode() {
|
||||
return delegate != null && delegate.IsInsertMode();
|
||||
}
|
||||
|
||||
public boolean isInsertMode() { return IsInsertMode(); }
|
||||
|
||||
public boolean IsCommError() {
|
||||
return delegate != null && delegate.IsCommError();
|
||||
}
|
||||
|
||||
public boolean isCommError() { return IsCommError(); }
|
||||
|
||||
public boolean IsKeyLocked() {
|
||||
return delegate != null && delegate.isKeyLocked();
|
||||
}
|
||||
|
||||
public boolean isKeyLocked() { return IsKeyLocked(); }
|
||||
|
||||
public boolean IsMessageWaiting() {
|
||||
return delegate != null && delegate.IsMessageWaiting();
|
||||
}
|
||||
|
||||
public boolean isMessageWaiting() { return IsMessageWaiting(); }
|
||||
|
||||
public int GetInputInhibited() {
|
||||
return delegate != null ? delegate.getInputInhibited() : 0;
|
||||
}
|
||||
|
||||
public int getInputInhibited() { return GetInputInhibited(); }
|
||||
|
||||
public int GetAlphanumericType() {
|
||||
return delegate != null ? delegate.getAlphanumericType() : 0;
|
||||
}
|
||||
|
||||
public int getAlphanumericType() { return GetAlphanumericType(); }
|
||||
|
||||
public boolean WaitForInput(long timeout) {
|
||||
return delegate != null && delegate.WaitForInput(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForSystemAvailable(long timeout) {
|
||||
return delegate != null && delegate.WaitForSystemAvailable(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForAppAvailable(long timeout) {
|
||||
return delegate != null && delegate.WaitForAppAvailable(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForTransition(long timeout) {
|
||||
return delegate != null && delegate.WaitForTransition(timeout);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLOIA getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Event object dispatched on Operator Information Area (ECLOIA) status changes matching IBM HoD.
|
||||
*/
|
||||
public class ECLOIAEvent extends ECLEvent {
|
||||
|
||||
public String data;
|
||||
public long newState;
|
||||
public long changedMask;
|
||||
|
||||
private int inputInhibited = 0;
|
||||
private int alphanumericType = 0;
|
||||
private boolean insertMode = false;
|
||||
|
||||
public ECLOIAEvent(Object source, long newState, long changedMask, String data) {
|
||||
super(source);
|
||||
this.newState = newState;
|
||||
this.changedMask = changedMask;
|
||||
this.data = data != null ? data : "";
|
||||
}
|
||||
|
||||
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
|
||||
boolean insertMode, String statusString) {
|
||||
super(source);
|
||||
this.inputInhibited = inputInhibited;
|
||||
this.alphanumericType = alphanumericType;
|
||||
this.insertMode = insertMode;
|
||||
this.data = statusString != null ? statusString : "";
|
||||
this.newState = inputInhibited;
|
||||
this.changedMask = eventType;
|
||||
}
|
||||
|
||||
public ECLOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent delegate) {
|
||||
super(delegate != null ? delegate.getSource() : null);
|
||||
if (delegate != null) {
|
||||
this.inputInhibited = delegate.getInputInhibited();
|
||||
this.alphanumericType = delegate.getAlphanumericType();
|
||||
this.insertMode = delegate.isInsertMode();
|
||||
this.data = delegate.getStatusString();
|
||||
this.newState = delegate.getInputInhibited();
|
||||
this.changedMask = delegate.getEventType();
|
||||
}
|
||||
}
|
||||
|
||||
public ECLOIA GetOIA() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLOIA) {
|
||||
return (ECLOIA) src;
|
||||
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLOIA) {
|
||||
return new ECLOIA((haus.nightmare.lib3270j.ecl.ECLOIA) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLOIA getOIA() { return GetOIA(); }
|
||||
|
||||
public int GetInputInhibited() { return inputInhibited; }
|
||||
public int getInputInhibited() { return inputInhibited; }
|
||||
|
||||
public int GetAlphanumericType() { return alphanumericType; }
|
||||
public int getAlphanumericType() { return alphanumericType; }
|
||||
|
||||
public boolean IsInsertMode() { return insertMode; }
|
||||
public boolean isInsertMode() { return insertMode; }
|
||||
|
||||
public boolean IsInputInhibited() { return inputInhibited != 0; }
|
||||
public boolean isInputInhibited() { return IsInputInhibited(); }
|
||||
|
||||
public String GetStatusString() { return data; }
|
||||
public String getStatusString() { return data; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ECLOIAEvent: changedMask=0x" + Long.toHexString(this.changedMask) +
|
||||
", newState=0x" + Long.toHexString(this.newState) + ", data=" + this.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Operator Information Area notification interface matching IBM Host On-Demand ECLOIANotify.
|
||||
*/
|
||||
public interface ECLOIANotify {
|
||||
void NotifyEvent(ECLOIA oia, int inhibitReason, int state, String text);
|
||||
default void NotifyStop(ECLOIA oia, int reason) {}
|
||||
default void NotifyError(ECLOIA oia, ECLErr err) {}
|
||||
default void OIANotifyStop(ECLOIA oia, int reason) { NotifyStop(oia, reason); }
|
||||
default void OIANotifyError(ECLOIA oia, ECLErr err) { NotifyError(oia, err); }
|
||||
default void OIANotifyEvent(ECLOIAEvent event) {
|
||||
if (event != null) {
|
||||
NotifyEvent(event.GetOIA(), event.GetInputInhibited(), 0, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLPSListener;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Presentation Space facade matching IBM Host On-Demand ECLPS.
|
||||
* Provides 1-based indexing, method signature parity, and checked ECLErr exception semantics.
|
||||
*/
|
||||
public class ECLPS implements ECLConstants, Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLPS delegate;
|
||||
|
||||
public ECLPS(haus.nightmare.lib3270j.ecl.ECLPS delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public ECLPS(haus.nightmare.lib3270j.ecl.ECLSession session) {
|
||||
this(session != null ? session.GetPS() : null);
|
||||
}
|
||||
|
||||
public ECLPS(ECLSession session) {
|
||||
this(session != null ? session.getDelegate() : null);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLPS getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
// ========== Dimensions & Position Conversion (1-based) ==========
|
||||
|
||||
public int GetSize() {
|
||||
return delegate != null ? delegate.GetSize() : 0;
|
||||
}
|
||||
|
||||
public int getSize() { return GetSize(); }
|
||||
|
||||
public int GetRows() {
|
||||
return delegate != null ? delegate.GetRows() : 0;
|
||||
}
|
||||
|
||||
public int getRows() { return GetRows(); }
|
||||
|
||||
public int GetCols() {
|
||||
return delegate != null ? delegate.GetCols() : 0;
|
||||
}
|
||||
|
||||
public int getCols() { return GetCols(); }
|
||||
|
||||
public int GetSizeRows() { return GetRows(); }
|
||||
public int GetSizeCols() { return GetCols(); }
|
||||
|
||||
public int ConvertRowColToPos(int row, int col) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertRowColToPos(row, col);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertRowColToPos(int row, int col) throws ECLErr {
|
||||
return ConvertRowColToPos(row, col);
|
||||
}
|
||||
|
||||
public int ConvertPosToRow(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToRow(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertPosToRow(int pos) throws ECLErr {
|
||||
return ConvertPosToRow(pos);
|
||||
}
|
||||
|
||||
public int ConvertPosToCol(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToCol(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertPosToCol(int pos) throws ECLErr {
|
||||
return ConvertPosToCol(pos);
|
||||
}
|
||||
|
||||
public void ConvertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.ConvertPosToRowCol(pos, rowCol);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void convertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
|
||||
ConvertPosToRowCol(pos, rowCol);
|
||||
}
|
||||
|
||||
public int[] ConvertPosToRowCol(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToRowCol(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return new int[]{1, 1};
|
||||
}
|
||||
|
||||
public int[] convertPosToRowCol(int pos) throws ECLErr {
|
||||
return ConvertPosToRowCol(pos);
|
||||
}
|
||||
|
||||
// ========== Cursor Operations (1-based) ==========
|
||||
|
||||
public int GetCursorPos() {
|
||||
return delegate != null ? delegate.GetCursorPos() : 1;
|
||||
}
|
||||
|
||||
public int getCursorPos() { return GetCursorPos(); }
|
||||
|
||||
public int GetCursorRow() {
|
||||
return delegate != null ? delegate.GetCursorRow() : 1;
|
||||
}
|
||||
|
||||
public int getCursorRow() { return GetCursorRow(); }
|
||||
|
||||
public int GetCursorCol() {
|
||||
return delegate != null ? delegate.GetCursorCol() : 1;
|
||||
}
|
||||
|
||||
public int getCursorCol() { return GetCursorCol(); }
|
||||
|
||||
public void SetCursorPos(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetCursorPos(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursorPos(int pos) throws ECLErr {
|
||||
SetCursorPos(pos);
|
||||
}
|
||||
|
||||
public void SetCursorPos(int row, int col) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetCursorPos(row, col);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursorPos(int row, int col) throws ECLErr {
|
||||
SetCursorPos(row, col);
|
||||
}
|
||||
|
||||
// ========== GetString & SetString Operations (1-based) ==========
|
||||
|
||||
public int GetString(char[] buffer, int len, int pos) throws ECLErr {
|
||||
return GetString(buffer, len, pos, len);
|
||||
}
|
||||
|
||||
public int GetString(char[] buffer, int len, int pos, int length) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.GetString(buffer, len, pos, length);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int GetString(char[] buffer, int len, int row, int col, int length) throws ECLErr {
|
||||
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
|
||||
}
|
||||
|
||||
public String GetString(int pos, int length) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.GetString(pos, length);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getString(int pos, int length) throws ECLErr {
|
||||
return GetString(pos, length);
|
||||
}
|
||||
|
||||
public String GetString(int row, int col, int length) throws ECLErr {
|
||||
return GetString(ConvertRowColToPos(row, col), length);
|
||||
}
|
||||
|
||||
public String getString(int row, int col, int length) throws ECLErr {
|
||||
return GetString(row, col, length);
|
||||
}
|
||||
|
||||
public void SetString(String text, int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetString(text, pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setString(String text, int pos) throws ECLErr {
|
||||
SetString(text, pos);
|
||||
}
|
||||
|
||||
public void SetString(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, ConvertRowColToPos(row, col));
|
||||
}
|
||||
|
||||
public void setString(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, row, col);
|
||||
}
|
||||
|
||||
public void SetString(String text) throws ECLErr {
|
||||
SetString(text, GetCursorPos());
|
||||
}
|
||||
|
||||
public void setString(String text) throws ECLErr {
|
||||
SetString(text);
|
||||
}
|
||||
|
||||
public void SetText(String text, int pos) throws ECLErr {
|
||||
SetString(text, pos);
|
||||
}
|
||||
|
||||
public void SetText(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, row, col);
|
||||
}
|
||||
|
||||
public void setText(String text, int pos) {
|
||||
if (delegate != null) delegate.setText(text, pos);
|
||||
}
|
||||
|
||||
public void setText(String text, int row, int col) {
|
||||
if (delegate != null) delegate.setText(text, row, col);
|
||||
}
|
||||
|
||||
// ========== Search Operations (1-based) ==========
|
||||
|
||||
public int SearchText(String text, int dir, int startPos) {
|
||||
return delegate != null ? delegate.SearchText(text, dir, startPos) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text, int dir) {
|
||||
return delegate != null ? delegate.SearchText(text, dir) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text) {
|
||||
return delegate != null ? delegate.SearchText(text) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text, int row, int col, int dir) throws ECLErr {
|
||||
return SearchText(text, dir, ConvertRowColToPos(row, col));
|
||||
}
|
||||
|
||||
public int SearchString(String text, int dir, int startPos) {
|
||||
return SearchText(text, dir, startPos);
|
||||
}
|
||||
|
||||
public int SearchString(String text, int dir) {
|
||||
return SearchText(text, dir);
|
||||
}
|
||||
|
||||
public int SearchString(String text) {
|
||||
return SearchText(text);
|
||||
}
|
||||
|
||||
public int SearchString(String text, int row, int col, int dir) throws ECLErr {
|
||||
return SearchText(text, row, col, dir);
|
||||
}
|
||||
|
||||
public int SearchPS(String text) {
|
||||
return delegate != null ? delegate.SearchPS(text) : 0;
|
||||
}
|
||||
|
||||
public int SearchPS(String text, int startPos) {
|
||||
return delegate != null ? delegate.SearchPS(text, startPos) : 0;
|
||||
}
|
||||
|
||||
public int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
|
||||
return delegate != null ? delegate.SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap) : 0;
|
||||
}
|
||||
|
||||
// ========== Keystrokes & Mnemonics ==========
|
||||
|
||||
public void SendKeys(String keys) {
|
||||
if (delegate != null) delegate.SendKeys(keys);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys) { SendKeys(keys); }
|
||||
|
||||
public void SendKeys(String keys, int pos) {
|
||||
if (delegate != null) delegate.SendKeys(keys, pos);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys, int pos) { SendKeys(keys, pos); }
|
||||
|
||||
public void SendKeys(String keys, int row, int col) {
|
||||
if (delegate != null) delegate.SendKeys(keys, row, col);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys, int row, int col) { SendKeys(keys, row, col); }
|
||||
|
||||
public void SendCharacters(String keys) {
|
||||
if (delegate != null) delegate.SendCharacters(keys);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys) { SendCharacters(keys); }
|
||||
|
||||
public void SendCharacters(String keys, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int delayMs) { SendCharacters(keys, delayMs); }
|
||||
|
||||
public void SendCharacters(String keys, int pos, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, pos, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int pos, int delayMs) { SendCharacters(keys, pos, delayMs); }
|
||||
|
||||
public void SendCharacters(String keys, int row, int col, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, row, col, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int row, int col, int delayMs) { SendCharacters(keys, row, col, delayMs); }
|
||||
|
||||
public String[] GetSendKeyMnemonics() {
|
||||
return delegate != null ? delegate.GetSendKeyMnemonics() : new String[0];
|
||||
}
|
||||
|
||||
public String[] getSendKeyMnemonics() { return GetSendKeyMnemonics(); }
|
||||
|
||||
// ========== Field Operations ==========
|
||||
|
||||
public ECLFieldList GetFieldList() {
|
||||
haus.nightmare.lib3270j.ecl.ECLFieldList fl = delegate != null ? delegate.GetFieldList() : null;
|
||||
return new ECLFieldList(fl, this);
|
||||
}
|
||||
|
||||
public ECLFieldList getFieldList() { return GetFieldList(); }
|
||||
|
||||
public ECLField GetField(int pos) throws ECLErr {
|
||||
return GetFieldList().FindField(pos);
|
||||
}
|
||||
|
||||
public ECLField getField(int pos) throws ECLErr { return GetField(pos); }
|
||||
|
||||
public ECLField GetField(int row, int col) throws ECLErr {
|
||||
return GetFieldList().FindField(row, col);
|
||||
}
|
||||
|
||||
public ECLField getField(int row, int col) throws ECLErr { return GetField(row, col); }
|
||||
|
||||
public ECLField GetFirstField() {
|
||||
return GetFieldList().GetFirstField();
|
||||
}
|
||||
|
||||
public ECLField getFirstField() { return GetFirstField(); }
|
||||
|
||||
public ECLField GetNextField(ECLField prev) {
|
||||
return GetFieldList().GetNextField(prev);
|
||||
}
|
||||
|
||||
public ECLField getNextField(ECLField prev) { return GetNextField(prev); }
|
||||
|
||||
// ========== Listener Overloads for HoD Interfaces ==========
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify) { RegisterPSEvent(notify); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, int eventType) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, eventType);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, int eventType) { RegisterPSEvent(notify, eventType); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, desc);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { RegisterPSEvent(notify, desc); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, desc, eventType);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { RegisterPSEvent(notify, desc, eventType); }
|
||||
|
||||
public void UnregisterPSEvent(ECLPSNotify notify) {
|
||||
if (delegate != null) delegate.UnregisterPSEvent(notify);
|
||||
}
|
||||
|
||||
public void unregisterPSEvent(ECLPSNotify notify) { UnregisterPSEvent(notify); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSListener listener) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); }
|
||||
|
||||
public void UnregisterPSEvent(ECLPSListener listener) {
|
||||
if (delegate != null) delegate.UnregisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); }
|
||||
|
||||
// ========== Synchronization & Waits ==========
|
||||
|
||||
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForScreen(desc, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
|
||||
|
||||
public boolean WaitForScreen(ECLScreenDesc desc) {
|
||||
return delegate != null && delegate.WaitForScreen(desc);
|
||||
}
|
||||
|
||||
public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); }
|
||||
|
||||
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitWhileScreen(desc, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); }
|
||||
|
||||
public boolean WaitWhileScreen(ECLScreenDesc desc) {
|
||||
return delegate != null && delegate.WaitWhileScreen(desc);
|
||||
}
|
||||
|
||||
public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); }
|
||||
|
||||
public boolean WaitForString(String text, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForString(text, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, long timeoutMs) { return WaitForString(text, timeoutMs); }
|
||||
|
||||
public boolean WaitForString(String text) {
|
||||
return delegate != null && delegate.WaitForString(text);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text) { return WaitForString(text); }
|
||||
|
||||
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForString(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, int row, int col, long timeoutMs) { return WaitForString(text, row, col, timeoutMs); }
|
||||
|
||||
public boolean WaitForCursor(int row, int col, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForCursor(row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
|
||||
|
||||
// ========== Screen Update Inquiries ==========
|
||||
|
||||
public ECLPSUpdate GetScreen(int startRow, int startCol, int length) throws ECLErr {
|
||||
return GetScreen(startRow, startCol, length, false);
|
||||
}
|
||||
|
||||
public ECLPSUpdate GetScreen(int startRow, int startCol, int length, boolean fillNulls) throws ECLErr {
|
||||
int pos = ConvertRowColToPos(startRow, startCol);
|
||||
char[] buf = new char[length];
|
||||
GetString(buf, length, pos, length);
|
||||
if (fillNulls) {
|
||||
for (int i = 0; i < buf.length; i++) {
|
||||
if (buf[i] == '\0') buf[i] = ' ';
|
||||
}
|
||||
}
|
||||
int endPos = pos + length - 1;
|
||||
int endRow = ConvertPosToRow(endPos);
|
||||
int endCol = ConvertPosToCol(endPos);
|
||||
return new ECLPSUpdate(this, startRow, startCol, endRow, endCol, new String(buf));
|
||||
}
|
||||
|
||||
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
|
||||
return delegate.GetScreen(buffer, len, row, col, length, plane);
|
||||
}
|
||||
|
||||
// ========== DBCS Character Status ==========
|
||||
|
||||
public boolean IsDBCSChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCSChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCSChar(int pos) { return IsDBCSChar(pos); }
|
||||
|
||||
public boolean IsDBCSChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCSChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCSChar(int row, int col) { return IsDBCSChar(row, col); }
|
||||
|
||||
public boolean IsDBCS1stChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCS1stChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCS1stChar(int pos) { return IsDBCS1stChar(pos); }
|
||||
|
||||
public boolean IsDBCS1stChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCS1stChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCS1stChar(int row, int col) { return IsDBCS1stChar(row, col); }
|
||||
|
||||
public boolean IsDBCS2ndChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCS2ndChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCS2ndChar(int pos) { return IsDBCS2ndChar(pos); }
|
||||
|
||||
public boolean IsDBCS2ndChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCS2ndChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCS2ndChar(int row, int col) { return IsDBCS2ndChar(row, col); }
|
||||
|
||||
public boolean IsSOChar(int pos) {
|
||||
return delegate != null && delegate.IsSOChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSOChar(int pos) { return IsSOChar(pos); }
|
||||
|
||||
public boolean IsSIChar(int pos) {
|
||||
return delegate != null && delegate.IsSIChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSIChar(int pos) { return IsSIChar(pos); }
|
||||
|
||||
public boolean IsSOSIChar(int pos) {
|
||||
return delegate != null && delegate.IsSOSIChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSOSIChar(int pos) { return IsSOSIChar(pos); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Presentation space notification interface matching IBM Host On-Demand ECLPSNotify.
|
||||
*/
|
||||
public interface ECLPSNotify {
|
||||
void NotifyEvent(ECLPS ps);
|
||||
default void NotifyStop(ECLPS ps, int reason) {}
|
||||
default void NotifyError(ECLPS ps, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Conforms to IBM Host On-Demand ECLPSUpdate.
|
||||
*/
|
||||
public class ECLPSUpdate implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate;
|
||||
private final int startRow;
|
||||
private final int startCol;
|
||||
private final int endRow;
|
||||
private final int endCol;
|
||||
private final int start;
|
||||
private final int end;
|
||||
private final boolean fullUpdate;
|
||||
private final String text;
|
||||
private final transient ECLPS ps;
|
||||
|
||||
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol,
|
||||
int start, int end, boolean fullUpdate, String text) {
|
||||
this.ps = ps;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.endRow = endRow;
|
||||
this.endCol = endCol;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.fullUpdate = fullUpdate;
|
||||
this.text = text != null ? text : "";
|
||||
this.delegate = null;
|
||||
}
|
||||
|
||||
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, String text) {
|
||||
this(ps, startRow, startCol, endRow, endCol, 0, 0, false, text);
|
||||
}
|
||||
|
||||
public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, String text) {
|
||||
this(null, startRow, startCol, endRow, endCol, 0, 0, false, text);
|
||||
}
|
||||
|
||||
public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate) {
|
||||
this.delegate = delegate;
|
||||
this.ps = (delegate != null && delegate.getPS() != null) ? new ECLPS(delegate.getPS()) : null;
|
||||
this.startRow = delegate != null ? delegate.getStartRow() : 0;
|
||||
this.startCol = delegate != null ? delegate.getStartCol() : 0;
|
||||
this.endRow = delegate != null ? delegate.getEndRow() : 0;
|
||||
this.endCol = delegate != null ? delegate.getEndCol() : 0;
|
||||
this.start = delegate != null ? delegate.getStart() : 0;
|
||||
this.end = delegate != null ? delegate.getEnd() : 0;
|
||||
this.fullUpdate = delegate != null && delegate.isFullUpdate();
|
||||
this.text = delegate != null ? delegate.getText() : "";
|
||||
}
|
||||
|
||||
public ECLPS getPS() { return ps; }
|
||||
public ECLPS GetPS() { return ps; }
|
||||
|
||||
public int getStartRow() { return startRow; }
|
||||
public int GetStartRow() { return startRow + 1; }
|
||||
|
||||
public int getStartCol() { return startCol; }
|
||||
public int GetStartCol() { return startCol + 1; }
|
||||
|
||||
public int getEndRow() { return endRow; }
|
||||
public int GetEndRow() { return endRow + 1; }
|
||||
|
||||
public int getEndCol() { return endCol; }
|
||||
public int GetEndCol() { return endCol + 1; }
|
||||
|
||||
public int getStart() { return start; }
|
||||
public int GetStart() { return start + 1; }
|
||||
|
||||
public int getEnd() { return end; }
|
||||
public int GetEnd() { return end + 1; }
|
||||
|
||||
public boolean isFullUpdate() { return fullUpdate; }
|
||||
public boolean IsFullUpdate() { return fullUpdate; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public String GetText() { return text; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ECLPSUpdate[start=(%d,%d), end=(%d,%d), full=%b]",
|
||||
startRow, startCol, endRow, endCol, fullUpdate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching engine facade matching IBM Host On-Demand ECLScreenDesc.
|
||||
*/
|
||||
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLScreenDesc() {
|
||||
super();
|
||||
}
|
||||
|
||||
public boolean Matches(ECLPS ps, ECLOIA oia) {
|
||||
haus.nightmare.lib3270j.ecl.ECLPS p = (ps != null) ? ps.getDelegate() : null;
|
||||
haus.nightmare.lib3270j.ecl.ECLOIA o = (oia != null) ? oia.getDelegate() : null;
|
||||
return super.Matches(p, o);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Listener interface for ECLScreenReco matching notifications matching IBM HoD.
|
||||
*/
|
||||
public interface ECLScreenNotify {
|
||||
void screenMatching(ECLScreenRecoEvent event);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Screen recognition facade matching IBM Host On-Demand ECLScreenReco.
|
||||
*/
|
||||
public class ECLScreenReco {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLScreenReco delegate;
|
||||
private final ECLPS ps;
|
||||
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
|
||||
|
||||
public ECLScreenReco() {
|
||||
this((ECLPS) null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLSession session) {
|
||||
this(session != null ? session.GetPS() : null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLPS ps) {
|
||||
this(ps, (ECLOIA) null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
|
||||
this.ps = ps;
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLScreenReco(ps != null ? ps.getDelegate() : null,
|
||||
oia != null ? oia.getDelegate() : null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLScreenReco delegate) {
|
||||
this.delegate = delegate;
|
||||
this.ps = null;
|
||||
}
|
||||
|
||||
public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
|
||||
if (desc != null && notify != null) {
|
||||
notifyMap.put(desc, notify);
|
||||
if (delegate != null) {
|
||||
delegate.RegisterScreen(desc, event -> notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
|
||||
RegisterScreen(desc, notify);
|
||||
}
|
||||
|
||||
public void UnregisterScreen(ECLScreenDesc desc) {
|
||||
if (desc != null) {
|
||||
notifyMap.remove(desc);
|
||||
if (delegate != null) {
|
||||
delegate.UnregisterScreen(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterScreen(ECLScreenDesc desc) {
|
||||
UnregisterScreen(desc);
|
||||
}
|
||||
|
||||
public boolean IsMatch(ECLScreenDesc desc) {
|
||||
if (delegate != null) {
|
||||
return delegate.IsMatch(desc);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) {
|
||||
if (ps == null) return false;
|
||||
if (desc == null) return true;
|
||||
return desc.Matches(ps, (ECLOIA) null);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLScreenReco getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Event delivered when an ECLScreenDesc matches the Presentation Space.
|
||||
*/
|
||||
public class ECLScreenRecoEvent extends ECLEvent {
|
||||
|
||||
private final ECLScreenReco source;
|
||||
private final ECLScreenDesc screenDesc;
|
||||
private final ECLPS ps;
|
||||
|
||||
public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) {
|
||||
super(source);
|
||||
this.source = source;
|
||||
this.screenDesc = screenDesc;
|
||||
this.ps = ps;
|
||||
}
|
||||
|
||||
public ECLScreenRecoEvent(Object source, ECLPS ps) {
|
||||
super(source);
|
||||
this.source = (source instanceof ECLScreenReco) ? (ECLScreenReco) source : null;
|
||||
this.screenDesc = null;
|
||||
this.ps = ps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object GetSource() { return source != null ? source : super.GetSource(); }
|
||||
|
||||
public ECLScreenReco GetScreenReco() { return source; }
|
||||
public ECLScreenReco getScreenReco() { return source; }
|
||||
|
||||
public ECLScreenDesc GetScreenDesc() { return screenDesc; }
|
||||
public ECLScreenDesc getScreenDesc() { return screenDesc; }
|
||||
|
||||
public ECLPS GetPS() { return ps; }
|
||||
public ECLPS getPS() { return ps; }
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Top-level session facade matching IBM Host On-Demand ECLSession.
|
||||
* Extends ECLConnection and provides access to Presentation Space (ECLPS),
|
||||
* Operator Information Area (ECLOIA), File Transfer (ECLXfer), and Screen Recognition (ECLScreenReco).
|
||||
*/
|
||||
public class ECLSession extends ECLConnection implements ECLConstants {
|
||||
|
||||
public static final String SESSION_NAME = "SESSION_NAME";
|
||||
public static final String SESSION_LABEL = "SESSION_LABEL";
|
||||
public static final String SESSION_TYPE = "SESSION_TYPE";
|
||||
public static final String SESSION_HOST = "SESSION_HOST";
|
||||
public static final String SESSION_HOST_PORT = "SESSION_HOST_PORT";
|
||||
public static final String SESSION_PORT = "SESSION_PORT";
|
||||
public static final String SESSION_PS_SIZE = "SESSION_PS_SIZE";
|
||||
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
|
||||
public static final String SESSION_SSL = "SESSION_SSL";
|
||||
public static final String SESSION_SECURITY_PROTOCOL = "SESSION_SECURITY_PROTOCOL";
|
||||
public static final String SESSION_TLS_PROTOCOL_VERSION = "tlsProtocolVersion";
|
||||
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
|
||||
public static final String SESSION_TN3270E = "SESSION_TN3270E";
|
||||
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
|
||||
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
|
||||
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
|
||||
public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE";
|
||||
public static final String KEY_KEEPALIVE_TYPE = "keepAliveType";
|
||||
public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout";
|
||||
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
|
||||
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
|
||||
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
|
||||
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
|
||||
public static final String BLOCK_PASTE = "blockPaste";
|
||||
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
|
||||
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
|
||||
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLSession delegate;
|
||||
private final ECLPS ps;
|
||||
private final ECLOIA oia;
|
||||
private final ECLXfer xfer;
|
||||
private final ECLScreenReco screenReco;
|
||||
|
||||
public ECLSession(Properties properties) throws ECLErr {
|
||||
super(properties);
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(properties);
|
||||
this.ps = new ECLPS(delegate.getPS());
|
||||
this.oia = new ECLOIA(delegate.getOIA());
|
||||
this.xfer = new ECLXfer(delegate.getXfer());
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLSession() throws ECLErr {
|
||||
this(new Properties());
|
||||
}
|
||||
|
||||
public ECLSession(String host, int port) throws ECLErr {
|
||||
super(host, port);
|
||||
Properties props = new Properties();
|
||||
if (host != null) props.setProperty(SESSION_HOST, host);
|
||||
props.setProperty(SESSION_PORT, String.valueOf(port));
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(props);
|
||||
this.ps = new ECLPS(delegate.getPS());
|
||||
this.oia = new ECLOIA(delegate.getOIA());
|
||||
this.xfer = new ECLXfer(delegate.getXfer());
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLSession(haus.nightmare.lib3270j.ecl.ECLSession delegate) {
|
||||
super(delegate);
|
||||
this.delegate = delegate;
|
||||
this.ps = new ECLPS(delegate != null ? delegate.getPS() : null);
|
||||
this.oia = new ECLOIA(delegate != null ? delegate.getOIA() : null);
|
||||
this.xfer = new ECLXfer(delegate != null ? delegate.getXfer() : null);
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLPS GetPS() {
|
||||
return ps;
|
||||
}
|
||||
|
||||
public ECLPS getPS() {
|
||||
return ps;
|
||||
}
|
||||
|
||||
public ECLOIA GetOIA() {
|
||||
return oia;
|
||||
}
|
||||
|
||||
public ECLOIA getOIA() {
|
||||
return oia;
|
||||
}
|
||||
|
||||
public ECLXfer GetXfer() {
|
||||
return xfer;
|
||||
}
|
||||
|
||||
public ECLXfer getXfer() {
|
||||
return xfer;
|
||||
}
|
||||
|
||||
public ECLScreenReco GetScreenReco() {
|
||||
return screenReco;
|
||||
}
|
||||
|
||||
public ECLScreenReco getScreenReco() {
|
||||
return screenReco;
|
||||
}
|
||||
|
||||
public ECLConnection GetConnection() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ECLConnection getConnection() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLSession getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StartCommunication() throws IOException {
|
||||
if (delegate != null) {
|
||||
delegate.StartCommunication();
|
||||
} else {
|
||||
super.StartCommunication();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StopCommunication() {
|
||||
if (delegate != null) {
|
||||
delegate.StopCommunication();
|
||||
} else {
|
||||
super.StopCommunication();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean StartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null);
|
||||
}
|
||||
|
||||
public boolean startCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout);
|
||||
}
|
||||
|
||||
public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.StartCommunicationWithBlocking(timeout, desc);
|
||||
} catch (IOException e) {
|
||||
throw new ECLErr("ECLSession.StartCommunicationWithBlocking", "ECL0001", e.getMessage());
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout, desc);
|
||||
}
|
||||
|
||||
public boolean RestartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.RestartCommunicationWithBlocking(timeout);
|
||||
} catch (IOException e) {
|
||||
throw new ECLErr("ECLSession.RestartCommunicationWithBlocking", "ECL0001", e.getMessage());
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean restartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return RestartCommunicationWithBlocking(timeout);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (delegate != null) {
|
||||
delegate.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLXferListener;
|
||||
|
||||
/**
|
||||
* File transfer facade matching IBM Host On-Demand ECLXfer.
|
||||
*/
|
||||
public class ECLXfer {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLXfer delegate;
|
||||
|
||||
public ECLXfer(ECLSession session, Boolean bidi) {
|
||||
this.delegate = (session != null && session.getDelegate() != null)
|
||||
? session.getDelegate().GetXfer() : null;
|
||||
}
|
||||
|
||||
public ECLXfer(haus.nightmare.lib3270j.ecl.ECLXfer delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public void SendFile(String localFile, String hostFile, String options) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
int rc = delegate.SendFile(localFile, hostFile, options);
|
||||
if (rc != 0) {
|
||||
throw new ECLErr("ECLXfer:SendFile", "ECL0150", String.valueOf(rc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ReceiveFile(String localFile, String hostFile, String options) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
int rc = delegate.ReceiveFile(localFile, hostFile, options);
|
||||
if (rc != 0) {
|
||||
throw new ECLErr("ECLXfer:ReceiveFile", "ECL0150", String.valueOf(rc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel() {
|
||||
if (delegate != null) {
|
||||
delegate.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterXferEvent(ECLXferListener listener) {
|
||||
// Can bridge listener if needed
|
||||
}
|
||||
|
||||
public void UnregisterXferEvent(ECLXferListener listener) {
|
||||
// Can bridge listener if needed
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLXfer getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
/**
|
||||
* Base event class matching IBM Host On-Demand ECLEvent.
|
||||
*/
|
||||
public class ECLEvent {
|
||||
private final Object originator;
|
||||
|
||||
public ECLEvent(Object object) {
|
||||
this.originator = object;
|
||||
}
|
||||
|
||||
public Object GetSource() {
|
||||
return this.originator;
|
||||
}
|
||||
|
||||
public Object getSource() {
|
||||
return this.originator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + ": " + (this.originator != null ? this.originator.toString() : "null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLPS;
|
||||
import com.ibm.eNetwork.ECL.ECLPSUpdate;
|
||||
|
||||
/**
|
||||
* Presentation Space event class matching IBM Host On-Demand ECLPSEvent.
|
||||
*/
|
||||
public class ECLPSEvent extends ECLEvent {
|
||||
|
||||
public static final int USER_EVENTS = 1;
|
||||
public static final int HOST_EVENTS = 2;
|
||||
public static final int ALL_EVENTS = 3;
|
||||
|
||||
private int type = 0;
|
||||
private int start = 1;
|
||||
private int end = 1;
|
||||
private int startRow = 1;
|
||||
private int startCol = 1;
|
||||
private int endRow = 1;
|
||||
private int endCol = 1;
|
||||
private int oldCursorPos = 1;
|
||||
private int newCursorPos = 1;
|
||||
private boolean fullUpdate = true;
|
||||
private boolean cursorVisible = true;
|
||||
private int ringCounter = 0;
|
||||
private ECLPSUpdate psUpdate = null;
|
||||
private haus.nightmare.lib3270j.ecl.ECLPSEvent delegate = null;
|
||||
|
||||
public ECLPSEvent(Object source) {
|
||||
super(source);
|
||||
if (source instanceof ECLPS) {
|
||||
ECLPS ps = (ECLPS) source;
|
||||
this.end = ps.GetSize();
|
||||
this.endRow = ps.GetRows();
|
||||
this.endCol = ps.GetCols();
|
||||
this.newCursorPos = ps.GetCursorPos();
|
||||
} else if (source instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
|
||||
haus.nightmare.lib3270j.ecl.ECLPS ps = (haus.nightmare.lib3270j.ecl.ECLPS) source;
|
||||
this.end = ps.GetSize();
|
||||
this.endRow = ps.GetRows();
|
||||
this.endCol = ps.GetCols();
|
||||
this.newCursorPos = ps.GetCursorPos();
|
||||
}
|
||||
}
|
||||
|
||||
public ECLPSEvent(Object source, int type) {
|
||||
this(source);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ECLPSEvent(haus.nightmare.lib3270j.ecl.ECLPSEvent delegate) {
|
||||
super(delegate != null ? delegate.getSource() : null);
|
||||
this.delegate = delegate;
|
||||
if (delegate != null) {
|
||||
this.type = delegate.getType();
|
||||
this.start = delegate.getStart();
|
||||
this.end = delegate.getEnd();
|
||||
this.startRow = delegate.getStartRow();
|
||||
this.startCol = delegate.getStartCol();
|
||||
this.endRow = delegate.getEndRow();
|
||||
this.endCol = delegate.getEndCol();
|
||||
this.oldCursorPos = delegate.getOldCursorAddress() + 1;
|
||||
this.newCursorPos = delegate.getNewCursorAddress() + 1;
|
||||
this.fullUpdate = delegate.isFullUpdate();
|
||||
this.cursorVisible = delegate.isCursorVisible();
|
||||
this.ringCounter = delegate.getRingCounter();
|
||||
if (delegate.getPSUpdate() != null) {
|
||||
this.psUpdate = new ECLPSUpdate(delegate.getPSUpdate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetType() { return type; }
|
||||
public int getType() { return type; }
|
||||
|
||||
public ECLPS GetPS() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLPS) {
|
||||
return (ECLPS) src;
|
||||
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
|
||||
return new ECLPS((haus.nightmare.lib3270j.ecl.ECLPS) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetStart(int start) { this.start = start; }
|
||||
public int GetStart() { return start; }
|
||||
public int getStart() { return start; }
|
||||
|
||||
public int GetStartRow() { return startRow; }
|
||||
public int getStartRow() { return startRow; }
|
||||
|
||||
public int GetStartCol() { return startCol; }
|
||||
public int getStartCol() { return startCol; }
|
||||
|
||||
public void SetEnd(int end) { this.end = end; }
|
||||
public int GetEnd() { return end; }
|
||||
public int getEnd() { return end; }
|
||||
|
||||
public int GetEndRow() { return endRow; }
|
||||
public int getEndRow() { return endRow; }
|
||||
|
||||
public int GetEndCol() { return endCol; }
|
||||
public int getEndCol() { return endCol; }
|
||||
|
||||
public int GetOldCursorPos() { return oldCursorPos; }
|
||||
public int getOldCursorPos() { return oldCursorPos; }
|
||||
|
||||
public int GetNewCursorPos() { return newCursorPos; }
|
||||
public int getNewCursorPos() { return newCursorPos; }
|
||||
|
||||
public boolean IsFullUpdate() { return fullUpdate; }
|
||||
public boolean isFullUpdate() { return fullUpdate; }
|
||||
|
||||
public boolean IsCursorVisible() { return cursorVisible; }
|
||||
public boolean isCursorVisible() { return cursorVisible; }
|
||||
|
||||
public int GetRingCounter() { return ringCounter; }
|
||||
public int getRingCounter() { return ringCounter; }
|
||||
|
||||
public ECLPSUpdate GetPSUpdate() { return psUpdate; }
|
||||
public ECLPSUpdate getPSUpdate() { return psUpdate; }
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLPSEvent getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLErr;
|
||||
import com.ibm.eNetwork.ECL.ECLPS;
|
||||
|
||||
/**
|
||||
* Presentation Space listener interface matching IBM Host On-Demand ECLPSListener.
|
||||
*/
|
||||
public interface ECLPSListener {
|
||||
void PSNotifyEvent(ECLPSEvent event);
|
||||
default void PSNotifyStop(ECLPS ps, int reason) {}
|
||||
default void PSNotifyError(ECLPS ps, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLXfer;
|
||||
|
||||
/**
|
||||
* File transfer event matching IBM Host On-Demand ECLXferEvent.
|
||||
*/
|
||||
public class ECLXferEvent extends ECLEvent {
|
||||
|
||||
private final String sourceFile;
|
||||
private final String targetFile;
|
||||
private final boolean receive;
|
||||
private final long unitsDone;
|
||||
private final long unitsToDo;
|
||||
|
||||
public ECLXferEvent(Object source, long unitsDone, String sourceFile, String targetFile, boolean receive) {
|
||||
this(source, unitsDone, -1L, sourceFile, targetFile, receive);
|
||||
}
|
||||
|
||||
public ECLXferEvent(Object source, long unitsDone, long unitsToDo, String sourceFile, String targetFile, boolean receive) {
|
||||
super(source);
|
||||
this.unitsDone = unitsDone;
|
||||
this.unitsToDo = unitsToDo;
|
||||
this.sourceFile = sourceFile;
|
||||
this.targetFile = targetFile;
|
||||
this.receive = receive;
|
||||
}
|
||||
|
||||
public ECLXfer GetECLXfer() {
|
||||
Object src = GetSource();
|
||||
return (src instanceof ECLXfer) ? (ECLXfer) src : null;
|
||||
}
|
||||
|
||||
public String GetSourceFile() { return sourceFile; }
|
||||
public String getSourceFile() { return sourceFile; }
|
||||
|
||||
public String GetTargetFile() { return targetFile; }
|
||||
public String getTargetFile() { return targetFile; }
|
||||
|
||||
public long GetBytesSent() { return unitsDone; }
|
||||
public long getBytesSent() { return unitsDone; }
|
||||
|
||||
public long GetBytesToSend() { return unitsToDo; }
|
||||
public long getBytesToSend() { return unitsToDo; }
|
||||
|
||||
public boolean IsReceive() { return receive; }
|
||||
public boolean isReceive() { return receive; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLErr;
|
||||
import com.ibm.eNetwork.ECL.ECLXfer;
|
||||
|
||||
/**
|
||||
* File transfer listener interface matching IBM Host On-Demand ECLXferListener.
|
||||
*/
|
||||
public interface ECLXferListener {
|
||||
void XferNotifyEvent(ECLXferEvent event);
|
||||
default void XferNotifyStop(ECLXfer xfer, int reason) {}
|
||||
default void XferNotifyError(ECLXfer xfer, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.ECL.macrovariable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroExpressionParser.
|
||||
*/
|
||||
public class MacroExpressionParser extends haus.nightmare.lib3270j.macro.variable.MacroExpressionParser implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroExpressionParser() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.ECL.macrovariable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroVariables.
|
||||
*/
|
||||
public class MacroVariables extends haus.nightmare.lib3270j.macro.variable.MacroVariables implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroVariables() {
|
||||
super();
|
||||
}
|
||||
}
|
||||