14 Commits

Author SHA1 Message Date
rudi aa724fbcef Hotspot detection and screen buffer
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m28s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
2026-09-18 18:18:53 +00:00
rudi 8a30efe9c0 Add bugfix for ATTN
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 35s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s
2026-09-18 17:33:58 +00:00
rudi 5049d8df74 Add facade classes for 3rd party programs
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m18s
2026-09-18 16:48:49 +00:00
rudi badd026055 Cleanup datastream and Graphics
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s
2026-09-18 00:55:54 +00:00
rudi ef70ea0f5a Bugfixes from zOS testing
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 37s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m16s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m52s
2026-09-17 23:09:43 +00:00
rudi 30b1a77b61 Datastream bug fixes w/ not recognizing bytes
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m9s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m10s
Release j3270 / Build & Publish Release (push) Successful in 1m11s
2026-09-16 21:08:22 +00:00
rudi 9bd1676ec1 Fix setting storage, and add proxy options like a3270
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m11s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s
2026-09-16 14:43:30 +00:00
rudi cfb32bdca5 Build Gitea release against Java 11 (lowest supported Java)
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m13s
Release j3270 / Build & Publish Release (push) Successful in 1m12s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m37s
2026-09-15 23:52:21 +00:00
rudi 754b1de7cb Rearrange settings, update sample config
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m8s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m38s
Release j3270 / Build & Publish Release (push) Successful in 1m13s
2026-09-15 17:59:26 +00:00
rudi 7892229a2c Add icon
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m12s
2026-09-14 13:01:41 +00:00
rudi 14bf7ba4b1 Add bugfixes from testing
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 37s
Release j3270 / Build & Publish Release (push) Successful in 43s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m14s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m12s
2026-09-12 17:21:21 -04:00
rudi bdfe6eec2a IMON GRAPHIC!
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 31s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 42s
2026-09-12 15:33:12 +00:00
rudi c28c097e25 Fix bugs reported in Discord
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s
2026-09-12 14:58:48 +00:00
rudi 46a022d86b Add bugfixes from discord
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m9s
Release j3270 / Build & Publish Release (push) Successful in 32s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 42s
2026-09-10 09:24:39 -04:00
195 changed files with 24700 additions and 1345 deletions
+5 -5
View File
@@ -20,21 +20,21 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Java JDK (Temurin 17) - name: Set up Java JDK (Temurin 11)
uses: actions/setup-java@v3 uses: actions/setup-java@v3
continue-on-error: true continue-on-error: true
with: with:
java-version: '17' java-version: '11'
distribution: 'temurin' distribution: 'temurin'
- name: Ensure Java Available - name: Ensure Java Available
run: | run: |
if ! command -v javac >/dev/null 2>&1; then if ! command -v javac >/dev/null 2>&1; then
echo "Installing OpenJDK 17 via package manager..." echo "Installing OpenJDK 11 via package manager..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y openjdk-17-jdk-headless || true apt-get update && apt-get install -y openjdk-11-jdk-headless || true
elif command -v apk >/dev/null 2>&1; then elif command -v apk >/dev/null 2>&1; then
apk add --no-cache openjdk17 || true apk add --no-cache openjdk11 || true
fi fi
fi fi
echo "Java compiler:" echo "Java compiler:"
+146
View File
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="124 738 320 320"
width="320"
height="320"
xml:space="preserve"
id="svg_j3270_icon"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="124" y="738" width="320" height="320" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

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

After

Width:  |  Height:  |  Size: 11 KiB

+6
View File
@@ -3,6 +3,11 @@
; or: ./run.sh -c j3270.ini ; or: ./run.sh -c j3270.ini
[appearance] [appearance]
; UI font settings (menus, dialogs, status bar)
; uiFontFamily = SansSerif
; uiFontSize = 13
;
; Terminal font settings (3270 screen display)
; fontFamily = IBM 3270 ; fontFamily = IBM 3270
; fontSize = 18 ; fontSize = 18
@@ -10,6 +15,7 @@
; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING ; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING
; autoConnectHost = mainframe.example.com ; autoConnectHost = mainframe.example.com
; autoConnectPort = 23 ; autoConnectPort = 23
; blockPaste = true
[colors] [colors]
; Host colors 0-15 (hex RGB) ; Host colors 0-15 (hex RGB)
@@ -42,8 +42,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// Dialog instances // Dialog instances
private FindDialog findDialog; private FindDialog findDialog;
private ScriptDialog scriptDialog; private ScriptDialog scriptDialog;
private MacroDialog macroDialog;
private FieldInspectorDialog fieldInspectorDialog; private FieldInspectorDialog fieldInspectorDialog;
private PrinterSessionDialog printerSessionDialog; private PrinterSessionDialog printerSessionDialog;
private ScreenHistoryDialog screenHistoryDialog;
// Modes menu items // Modes menu items
private JCheckBoxMenuItem docModeItem; private JCheckBoxMenuItem docModeItem;
@@ -60,6 +62,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
super("j3270 — Java TN3270 Terminal Emulator"); super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBackground(Color.BLACK); setBackground(Color.BLACK);
AppIcon.applyTo(this);
buildUI(); buildUI();
buildMenuBar(); buildMenuBar();
@@ -93,6 +96,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (printerSessionDialog != null) { if (printerSessionDialog != null) {
printerSessionDialog.dispose(); printerSessionDialog.dispose();
} }
if (screenHistoryDialog != null) {
screenHistoryDialog.dispose();
}
terminalPanel.dispose(); terminalPanel.dispose();
} }
@@ -103,6 +109,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
} }
}); });
// Ensure clicking anywhere inside the frame brings focus to window and terminalPanel
Toolkit.getDefaultToolkit().addAWTEventListener(event -> {
if (event instanceof MouseEvent && event.getID() == MouseEvent.MOUSE_PRESSED) {
MouseEvent me = (MouseEvent) event;
Object src = me.getSource();
if (src instanceof Component) {
Window win = SwingUtilities.getWindowAncestor((Component) src);
if (win == J3270App.this && !isAnyChildDialogActive()) {
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
J3270App.this.toFront();
J3270App.this.requestFocus();
}
boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu)
|| SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null
|| SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null;
if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
}
}
}
}, AWTEvent.MOUSE_EVENT_MASK);
} }
private boolean isAnyChildDialogActive() { private boolean isAnyChildDialogActive() {
@@ -112,8 +141,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
if (findDialog != null && findDialog.isVisible()) return true; if (findDialog != null && findDialog.isVisible()) return true;
if (scriptDialog != null && scriptDialog.isVisible()) return true; if (scriptDialog != null && scriptDialog.isVisible()) return true;
if (macroDialog != null && macroDialog.isVisible()) return true;
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true; if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true; if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
if (screenHistoryDialog != null && screenHistoryDialog.isVisible()) return true;
for (Window owned : getOwnedWindows()) { for (Window owned : getOwnedWindows()) {
if (owned != null && owned.isVisible()) { if (owned != null && owned.isVisible()) {
return true; return true;
@@ -180,6 +211,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// 2. Edit menu // 2. Edit menu
JMenu editMenu = createMenu("Edit"); JMenu editMenu = createMenu("Edit");
editMenu.add(createMenuItem("Cut", KeyEvent.VK_X, () -> terminalPanel.cutSelection()));
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection())); editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard())); editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog)); editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
@@ -311,6 +343,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
viewMenu.add(gfxMenu); viewMenu.add(gfxMenu);
viewMenu.addSeparator(); viewMenu.addSeparator();
viewMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog)); viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
menuBar.add(viewMenu); menuBar.add(viewMenu);
@@ -429,6 +462,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}, true)); }, true));
actionsMenu.addSeparator(); actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
actionsMenu.add(createMenuItem("Macro Runtime (<HASCRIPT>)...", KeyEvent.VK_M, this::showMacroDialog, true));
actionsMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true)); actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
menuBar.add(actionsMenu); menuBar.add(actionsMenu);
@@ -535,6 +570,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
scriptDialog.setVisible(true); scriptDialog.setVisible(true);
} }
private void showMacroDialog() {
if (macroDialog == null) {
macroDialog = new MacroDialog(this, client, terminalPanel);
}
macroDialog.setVisible(true);
}
private void showFieldInspectorDialog() { private void showFieldInspectorDialog() {
if (client == null) { if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
@@ -558,6 +600,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
printerSessionDialog.setVisible(true); printerSessionDialog.setVisible(true);
} }
private void showScreenHistoryDialog() {
if (screenHistoryDialog == null) {
screenHistoryDialog = new ScreenHistoryDialog(this, client);
} else {
if (client != null && client.getScreenHistoryBuffer() != null) {
screenHistoryDialog.refreshHistoryView(client.getScreenHistoryBuffer().size() - 1);
}
}
screenHistoryDialog.setVisible(true);
screenHistoryDialog.toFront();
}
private void showPasteLineWrapDialog() { private void showPasteLineWrapDialog() {
if (client == null || !client.getConnectionState().isFullSession()) { if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
@@ -693,6 +747,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls()); dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert()); dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage()); dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
dialog.setVisible(true); dialog.setVisible(true);
if (dialog.isConfirmed()) { if (dialog.isConfirmed()) {
@@ -712,6 +767,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return terminalPanel; return terminalPanel;
} }
public Telnet3270Client getClient() {
return client;
}
public StatusBar getStatusBar() {
return statusBar;
}
void connect(ConnectionConfig config) { void connect(ConnectionConfig config) {
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
@@ -721,6 +784,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls()); haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert()); haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage()); haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
if (config.getGraphicsMode() != null) {
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
}
disconnect(); disconnect();
@@ -764,6 +830,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
fileTransfer.cancel(); fileTransfer.cancel();
fileTransfer = null; fileTransfer = null;
} }
if (macroDialog != null) {
macroDialog.stopMacro();
}
Telnet3270Client c = client; Telnet3270Client c = client;
client = null; client = null;
new Thread(() -> { new Thread(() -> {
@@ -941,7 +1010,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(area); ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp); ThemeManager.styleScrollPane(sp);
@@ -974,7 +1043,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); area.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(area); ThemeManager.styleTextArea(area);
JScrollPane sp = new JScrollPane(area); JScrollPane sp = new JScrollPane(area);
ThemeManager.styleScrollPane(sp); ThemeManager.styleScrollPane(sp);
@@ -998,55 +1067,23 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"About j3270", JOptionPane.INFORMATION_MESSAGE); "About j3270", JOptionPane.INFORMATION_MESSAGE);
} }
// ========== Main ========== /**
* Configure application logging. When debug is false, disables all logging
public static void main(String[] args) { * to console and file, preventing creation of j3270.log.
boolean debug = false; */
boolean cliTls = false; public static void configureLogging(boolean debug) {
boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null;
Boolean cliAutoSysUnlock = null;
GraphicsMode cliGraphicsMode = null;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("-d".equals(arg) || "--debug".equals(arg)) {
debug = true;
} else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
cliTls = true;
} else if ("--no-verify-cert".equals(arg) || "--insecure".equals(arg) || "-k".equals(arg)) {
cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(arg) || "--plain-tn3270".equals(arg) || "--plain".equals(arg) || "-P".equals(arg) || "-p".equals(arg) || "--non-e".equals(arg)) {
cliTn3270e = false;
} else if ("--tn3270e".equals(arg)) {
cliTn3270e = true;
} else if ("--auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = true;
} else if ("--no-auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = false;
} else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(arg)) {
cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
configFile = args[++i];
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
} else {
remainingArgs.add(arg);
}
}
Level logLevel = debug ? Level.FINE : Level.INFO;
Logger globalRoot = Logger.getLogger(""); Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) { for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h); globalRoot.removeHandler(h);
try {
h.close();
} catch (Exception ignored) {
} }
}
if (debug) {
Level logLevel = Level.ALL;
globalRoot.setLevel(Level.ALL);
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null && java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm")); (record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
@@ -1077,6 +1114,117 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} catch (Exception e) { } catch (Exception e) {
System.err.println("Could not create j3270.log: " + e.getMessage()); System.err.println("Could not create j3270.log: " + e.getMessage());
} }
} else {
globalRoot.setLevel(Level.OFF);
for (String pkg : new String[]{"haus.nightmare", "haus.nightmare.j3270", "haus.nightmare.lib3270j", "org.pubvm"}) {
Logger l = Logger.getLogger(pkg);
l.setLevel(Level.OFF);
for (java.util.logging.Handler h : l.getHandlers()) {
l.removeHandler(h);
try {
h.close();
} catch (Exception ignored) {
}
}
}
}
}
private static void applyProxyUrl(ConnectionConfig config, String proxyUrl) {
if (config == null || proxyUrl == null || proxyUrl.trim().isEmpty()) return;
try {
java.net.URI uri = new java.net.URI(proxyUrl.trim());
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
ConnectionConfig.ProxyType pType;
int pPort;
if (scheme.equals("http") || scheme.equals("https")) {
pType = ConnectionConfig.ProxyType.HTTP;
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
pType = ConnectionConfig.ProxyType.SOCKS4;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
pType = ConnectionConfig.ProxyType.SOCKS5;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else {
return;
}
String pHost = uri.getHost();
String pUser = null;
String pPass = null;
String userInfo = uri.getUserInfo();
if (userInfo != null) {
int colon = userInfo.indexOf(':');
if (colon >= 0) {
pUser = userInfo.substring(0, colon);
pPass = userInfo.substring(colon + 1);
} else {
pUser = userInfo;
}
}
if (pHost != null && !pHost.isEmpty()) {
config.setProxy(pType, pHost, pPort, pUser, pPass);
}
} catch (Exception e) {
log.warning("Failed to parse CLI proxy URL: " + proxyUrl);
}
}
// ========== Main ==========
public static void main(String[] args) {
boolean debug = false;
boolean cliTls = false;
boolean cliNoVerifyCert = false;
Boolean cliTn3270e = null;
Boolean cliAutoSysUnlock = null;
GraphicsMode cliGraphicsMode = null;
String configFile = null;
String cliProxyUrl = null;
String cliProfileName = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("-d".equals(arg) || "--debug".equals(arg)) {
debug = true;
} else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
cliTls = true;
} else if ("--no-verify-cert".equals(arg) || "--insecure".equals(arg) || "-k".equals(arg)) {
cliNoVerifyCert = true;
} else if ("--no-tn3270e".equals(arg) || "--plain-tn3270".equals(arg) || "--plain".equals(arg) || "-P".equals(arg) || "-p".equals(arg) || "--non-e".equals(arg)) {
cliTn3270e = false;
} else if ("--tn3270e".equals(arg)) {
cliTn3270e = true;
} else if ("--auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = true;
} else if ("--no-auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = false;
} else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliGraphicsMode = GraphicsMode.fromString(args[++i]);
} else if ("--no-graphics".equals(arg)) {
cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
configFile = args[++i];
} else if (arg.startsWith("--proxy=") || arg.startsWith("-proxy=")) {
cliProxyUrl = arg.substring(arg.indexOf('=') + 1).trim();
} else if (("--proxy".equals(arg) || "-proxy".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliProxyUrl = args[++i].trim();
} else if (arg.startsWith("--profile=")) {
cliProfileName = arg.substring(arg.indexOf('=') + 1).trim();
} else if ("--profile".equals(arg) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliProfileName = args[++i].trim();
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
} else {
remainingArgs.add(arg);
}
}
configureLogging(debug);
if (configFile != null) { if (configFile != null) {
try { try {
@@ -1104,11 +1252,33 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final Boolean finalTn3270e = cliTn3270e; final Boolean finalTn3270e = cliTn3270e;
final Boolean finalAutoSysUnlock = cliAutoSysUnlock; final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
final GraphicsMode finalGraphicsMode = cliGraphicsMode; final GraphicsMode finalGraphicsMode = cliGraphicsMode;
final String finalProxyUrl = cliProxyUrl;
final String finalProfileName = cliProfileName;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App(); J3270App app = new J3270App();
app.setVisible(true); app.setVisible(true);
if (finalProfileName != null) {
haus.nightmare.j3270.storage.SavedHost profile = haus.nightmare.j3270.storage.HostStorage.findByName(finalProfileName);
if (profile == null) {
profile = haus.nightmare.j3270.storage.HostStorage.findById(finalProfileName);
}
if (profile != null) {
ConnectionConfig config = profile.toConnectionConfig();
if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
app.connect(config);
return;
} else {
System.err.println("Profile not found: " + finalProfileName);
}
}
if (!remainingArgs.isEmpty()) { if (!remainingArgs.isEmpty()) {
String hostArg = remainingArgs.get(0); String hostArg = remainingArgs.get(0);
int port = finalTls ? 992 : 23; int port = finalTls ? 992 : 23;
@@ -1149,6 +1319,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} else { } else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
} }
if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl);
}
app.connect(config); app.connect(config);
} else { } else {
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
@@ -1158,6 +1331,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
SwingUtilities.invokeLater(app::showConnectDialog); SwingUtilities.invokeLater(app::showConnectDialog);
break; break;
case AUTO_CONNECT: case AUTO_CONNECT:
haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost();
if (autoHost != null) {
ConnectionConfig config = autoHost.toConnectionConfig();
if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
app.connect(config);
} else {
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost(); String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort(); int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls(); boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
@@ -1167,16 +1351,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify); config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e); config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock()); config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
if (finalGraphicsMode != null) { if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode); config.setGraphicsMode(finalGraphicsMode);
} else { } else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
} }
if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl);
}
app.connect(config); app.connect(config);
} else { } else {
SwingUtilities.invokeLater(app::showConnectDialog); SwingUtilities.invokeLater(app::showConnectDialog);
} }
}
break; break;
case DO_NOTHING: case DO_NOTHING:
default: default:
@@ -45,6 +45,24 @@ public class Settings {
flushPrefs(); 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() { private static void flushPrefs() {
try { try {
prefs.flush(); prefs.flush();
@@ -141,6 +159,14 @@ public class Settings {
flushPrefs(); flushPrefs();
} }
public static boolean getAutoReconnect() {
return getAutoConnectAutoReconnect();
}
public static void setAutoReconnect(boolean autoReconnect) {
setAutoConnectAutoReconnect(autoReconnect);
}
public static int getAutoConnectReconnectMaxRetries() { public static int getAutoConnectReconnectMaxRetries() {
return prefs.getInt("autoConnectReconnectMaxRetries", 5); return prefs.getInt("autoConnectReconnectMaxRetries", 5);
} }
@@ -150,6 +176,32 @@ public class Settings {
flushPrefs(); flushPrefs();
} }
public static boolean getInputMask() {
return prefs.getBoolean("inputMask", true);
}
public static void setInputMask(boolean mask) {
prefs.putBoolean("inputMask", mask);
flushPrefs();
}
public static boolean getInputMaskEnabled() {
return getInputMask();
}
public static void setInputMaskEnabled(boolean enabled) {
setInputMask(enabled);
}
public static String getInputMaskChar() {
return prefs.get("inputMaskChar", "*");
}
public static void setInputMaskChar(String ch) {
prefs.put("inputMaskChar", (ch != null && !ch.trim().isEmpty()) ? ch.trim().substring(0, 1) : "*");
flushPrefs();
}
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() { public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name()); String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr); return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
@@ -356,6 +408,17 @@ public class Settings {
prefs.putBoolean("blockSelectMode", block); prefs.putBoolean("blockSelectMode", block);
} }
// ========== Block paste ==========
public static boolean getBlockPaste() {
return prefs.getBoolean("blockPaste", true);
}
public static void setBlockPaste(boolean block) {
prefs.putBoolean("blockPaste", block);
flushPrefs();
}
// ========== Clipboard & Tabular Paste ========== // ========== Clipboard & Tabular Paste ==========
public static boolean getEnablePasteFromExcel() { public static boolean getEnablePasteFromExcel() {
@@ -376,6 +439,33 @@ public class Settings {
flushPrefs(); 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 ========== // ========== Crosshair Ruler ==========
public static boolean getCrosshairRuler() { public static boolean getCrosshairRuler() {
@@ -386,6 +476,71 @@ public class Settings {
prefs.putBoolean("crosshairRuler", 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 ========== // ========== Status Bar Visibility ==========
public static boolean getStatusBarVisible() { public static boolean getStatusBarVisible() {
@@ -507,6 +662,16 @@ public class Settings {
switch (key) { switch (key) {
case "fontFamily": setFontFamily(value); break; case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break; case "fontSize": setFontSize(Integer.parseInt(value)); break;
case "uiFontFamily":
case "uifontfamily":
case "uifont":
case "uiFont":
setUiFontFamily(value);
break;
case "uiFontSize":
case "uifontsize":
setUiFontSize(Integer.parseInt(value));
break;
case "javaUiTheme": case "javaUiTheme":
case "theme": case "theme":
case "uiTheme": case "uiTheme":
@@ -574,10 +739,34 @@ public class Settings {
setDynamicCols(Integer.parseInt(value)); setDynamicCols(Integer.parseInt(value));
break; break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
case "autoReconnect":
case "auto_reconnect":
case "autoConnectAutoReconnect":
setAutoConnectAutoReconnect(Boolean.parseBoolean(value));
break;
case "reconnectMaxRetries":
case "autoConnectReconnectMaxRetries":
setAutoConnectReconnectMaxRetries(Integer.parseInt(value));
break;
case "inputMask":
case "input_mask":
case "inputMaskEnabled":
case "maskInput":
setInputMask(Boolean.parseBoolean(value));
break;
case "inputMaskChar":
case "input_mask_char":
case "maskChar":
setInputMaskChar(value);
break;
case "autoSysUnlock": case "autoSysUnlock":
case "auto_sys_unlock": case "auto_sys_unlock":
setAutoSysUnlock(Boolean.parseBoolean(value)); setAutoSysUnlock(Boolean.parseBoolean(value));
break; break;
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel": case "enablePasteFromExcel":
case "pasteFromExcel": case "pasteFromExcel":
case "excelPaste": case "excelPaste":
@@ -588,6 +777,18 @@ public class Settings {
case "pasteStopAtProtected": case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value)); setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break; break;
case "trimRectSizingHandles":
case "sizingHandles":
setTrimRectSizingHandles(Boolean.parseBoolean(value));
break;
case "trimRectRemainAfterEdit":
case "remainAfterEdit":
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
break;
case "pasteWordBreak":
case "wordBreak":
setPasteWordBreak(Boolean.parseBoolean(value));
break;
default: default:
log.warning("Unknown behavior/connection key: " + key); log.warning("Unknown behavior/connection key: " + key);
} }
@@ -595,6 +796,10 @@ public class Settings {
case "clipboard": case "clipboard":
switch (key) { switch (key) {
case "blockPaste":
case "blockPasteMode":
setBlockPaste(Boolean.parseBoolean(value));
break;
case "enablePasteFromExcel": case "enablePasteFromExcel":
case "pasteFromExcel": case "pasteFromExcel":
case "excelPaste": case "excelPaste":
@@ -605,6 +810,18 @@ public class Settings {
case "pasteStopAtProtected": case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value)); setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break; break;
case "trimRectSizingHandles":
case "sizingHandles":
setTrimRectSizingHandles(Boolean.parseBoolean(value));
break;
case "trimRectRemainAfterEdit":
case "remainAfterEdit":
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
break;
case "pasteWordBreak":
case "wordBreak":
setPasteWordBreak(Boolean.parseBoolean(value));
break;
default: default:
log.warning("Unknown clipboard key: " + key); log.warning("Unknown clipboard key: " + key);
} }
@@ -707,6 +924,8 @@ public class Settings {
// [appearance] // [appearance]
w.println("[appearance]"); w.println("[appearance]");
w.println("javaUiTheme = " + getJavaUiTheme().name()); w.println("javaUiTheme = " + getJavaUiTheme().name());
w.println("uiFontFamily = " + getUiFontFamily());
w.println("uiFontSize = " + getUiFontSize());
w.println("fontFamily = " + getFontFamily()); w.println("fontFamily = " + getFontFamily());
w.println("fontSize = " + getFontSize()); w.println("fontSize = " + getFontSize());
w.println("crosshairRuler = " + getCrosshairRuler()); w.println("crosshairRuler = " + getCrosshairRuler());
@@ -729,9 +948,17 @@ public class Settings {
w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols()); w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
w.println("blockPaste = " + getBlockPaste());
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
w.println("inputMask = " + getInputMask());
w.println("inputMaskChar = " + getInputMaskChar());
w.println("autoSysUnlock = " + getAutoSysUnlock()); w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel()); w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine()); w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
w.println("trimRectSizingHandles = " + getTrimRectSizingHandles());
w.println("trimRectRemainAfterEdit = " + getTrimRectRemainAfterEdit());
w.println("pasteWordBreak = " + getPasteWordBreak());
w.println(); w.println();
// [entryassist] // [entryassist]
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.ft; package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.AppIcon;
import haus.nightmare.j3270.ui.HostDirectoryDialog; import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager; import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
@@ -92,6 +93,7 @@ public class FileTransferDialog extends JDialog {
super(owner, "File Transfer (IND$FILE)", false); super(owner, "File Transfer (IND$FILE)", false);
this.owner = owner; this.owner = owner;
this.coordinator = coordinator; this.coordinator = coordinator;
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
@@ -0,0 +1,433 @@
package haus.nightmare.j3270.storage;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
/**
* Storage manager for saved host profiles, maintaining 1:1 format compatibility with a3270.
* Persists profiles in Java user preferences with JSON serialization.
*/
public class HostStorage {
private static final Logger log = Logger.getLogger(HostStorage.class.getName());
private static final Preferences prefs = Preferences.userNodeForPackage(HostStorage.class);
public static final String KEY_SAVED_HOSTS = "saved_hosts";
public static synchronized List<SavedHost> getSavedHosts() {
String jsonStr = prefs.get(KEY_SAVED_HOSTS, null);
if (jsonStr == null || jsonStr.trim().isEmpty()) {
return new ArrayList<>();
}
try {
return parseHostsJson(jsonStr);
} catch (Exception e) {
log.log(Level.WARNING, "Failed to parse saved hosts JSON", e);
return new ArrayList<>();
}
}
public static synchronized void saveHost(SavedHost host) {
if (host == null) return;
List<SavedHost> current = new ArrayList<>(getSavedHosts());
int index = -1;
for (int i = 0; i < current.size(); i++) {
if (Objects.equals(current.get(i).getId(), host.getId())) {
index = i;
break;
}
}
// If this host is set to autoConnect, clear autoConnect on all others
SavedHost hostToSave = host.copy();
if (hostToSave.isAutoConnect()) {
for (int i = 0; i < current.size(); i++) {
current.get(i).setAutoConnect(false);
}
}
if (index >= 0) {
current.set(index, hostToSave);
} else {
current.add(hostToSave);
}
saveAll(current);
}
public static synchronized void deleteHost(String hostId) {
if (hostId == null) return;
List<SavedHost> current = new ArrayList<>(getSavedHosts());
current.removeIf(h -> Objects.equals(h.getId(), hostId));
saveAll(current);
}
public static synchronized SavedHost getAutoConnectHost() {
for (SavedHost h : getSavedHosts()) {
if (h.isAutoConnect()) {
return h;
}
}
return null;
}
public static synchronized SavedHost findById(String id) {
if (id == null) return null;
for (SavedHost h : getSavedHosts()) {
if (id.equals(h.getId())) {
return h;
}
}
return null;
}
public static synchronized SavedHost findByName(String name) {
if (name == null) return null;
for (SavedHost h : getSavedHosts()) {
if (name.equalsIgnoreCase(h.getName())) {
return h;
}
}
return null;
}
public static synchronized void saveAll(List<SavedHost> hosts) {
if (hosts == null) return;
String json = serializeHostsJson(hosts);
prefs.put(KEY_SAVED_HOSTS, json);
try {
prefs.flush();
} catch (Exception ignored) {
}
}
public static synchronized void clearAll() {
prefs.remove(KEY_SAVED_HOSTS);
try {
prefs.flush();
} catch (Exception ignored) {
}
}
// ================= JSON Serialization =================
public static String serializeHostsJson(List<SavedHost> hosts) {
StringBuilder sb = new StringBuilder();
sb.append("[\n");
for (int i = 0; i < hosts.size(); i++) {
SavedHost h = hosts.get(i);
sb.append(" ").append(serializeHostJson(h));
if (i < hosts.size() - 1) {
sb.append(",");
}
sb.append("\n");
}
sb.append("]");
return sb.toString();
}
public static String serializeHostJson(SavedHost h) {
StringBuilder sb = new StringBuilder();
sb.append("{");
appendField(sb, "id", h.getId(), true);
appendField(sb, "name", h.getName(), true);
appendField(sb, "host", h.getHost(), true);
appendField(sb, "port", h.getPort(), true);
appendField(sb, "model", h.getModel(), true);
appendField(sb, "dynamicRows", h.getDynamicRows(), true);
appendField(sb, "dynamicCols", h.getDynamicCols(), true);
appendField(sb, "luName", h.getLuName(), true);
appendField(sb, "autoConnect", h.isAutoConnect(), true);
appendField(sb, "hostType", h.getHostType(), true);
appendField(sb, "useTls", h.isUseTls(), true);
appendField(sb, "tlsVerifyCert", h.isTlsVerifyCert(), true);
appendField(sb, "tn3270e", h.isTn3270e(), true);
appendField(sb, "graphicsMode", h.getGraphicsMode(), true);
appendField(sb, "codePage", h.getCodePage(), true);
appendField(sb, "proxyType", h.getProxyType(), true);
appendField(sb, "proxyHost", h.getProxyHost(), true);
appendField(sb, "proxyPort", h.getProxyPort(), true);
appendField(sb, "proxyUsername", h.getProxyUsername(), true);
appendField(sb, "proxyPassword", h.getProxyPassword(), false);
sb.append("}");
return sb.toString();
}
private static void appendField(StringBuilder sb, String key, String val, boolean comma) {
sb.append("\"").append(key).append("\":\"").append(escapeJson(val != null ? val : "")).append("\"");
if (comma) sb.append(",");
}
private static void appendField(StringBuilder sb, String key, int val, boolean comma) {
sb.append("\"").append(key).append("\":").append(val);
if (comma) sb.append(",");
}
private static void appendField(StringBuilder sb, String key, boolean val, boolean comma) {
sb.append("\"").append(key).append("\":").append(val);
if (comma) sb.append(",");
}
private static String escapeJson(String s) {
if (s == null) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 32) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
break;
}
}
return sb.toString();
}
// ================= JSON Deserialization =================
public static List<SavedHost> parseHostsJson(String json) {
List<SavedHost> list = new ArrayList<>();
if (json == null || json.trim().isEmpty()) return list;
SimpleJsonParser parser = new SimpleJsonParser(json);
Object root = parser.parseValue();
if (root instanceof List) {
for (Object item : (List<?>) root) {
if (item instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) item;
list.add(fromMap(map));
}
}
} else if (root instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) root;
list.add(fromMap(map));
}
return list;
}
public static SavedHost fromMap(Map<String, Object> map) {
SavedHost h = new SavedHost();
if (map.containsKey("id")) h.setId(String.valueOf(map.get("id")));
if (map.containsKey("name")) h.setName(String.valueOf(map.get("name")));
if (map.containsKey("host")) h.setHost(String.valueOf(map.get("host")));
if (map.containsKey("port")) h.setPort(parseInt(map.get("port"), 23));
if (map.containsKey("model")) h.setModel(parseInt(map.get("model"), 4));
if (map.containsKey("dynamicRows")) h.setDynamicRows(parseInt(map.get("dynamicRows"), 62));
if (map.containsKey("dynamicCols")) h.setDynamicCols(parseInt(map.get("dynamicCols"), 160));
if (map.containsKey("luName")) h.setLuName(String.valueOf(map.get("luName")));
if (map.containsKey("autoConnect")) h.setAutoConnect(parseBool(map.get("autoConnect"), false));
if (map.containsKey("hostType")) h.setHostType(String.valueOf(map.get("hostType")));
if (map.containsKey("useTls")) h.setUseTls(parseBool(map.get("useTls"), false));
if (map.containsKey("tlsVerifyCert")) h.setTlsVerifyCert(parseBool(map.get("tlsVerifyCert"), true));
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
if (map.containsKey("graphicsMode")) {
String gm = String.valueOf(map.get("graphicsMode"));
h.setGraphicsMode(gm);
}
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
if (map.containsKey("proxyType")) h.setProxyType(String.valueOf(map.get("proxyType")));
if (map.containsKey("proxyHost")) h.setProxyHost(String.valueOf(map.get("proxyHost")));
if (map.containsKey("proxyPort")) h.setProxyPort(parseInt(map.get("proxyPort"), 0));
if (map.containsKey("proxyUsername")) h.setProxyUsername(String.valueOf(map.get("proxyUsername")));
if (map.containsKey("proxyPassword")) h.setProxyPassword(String.valueOf(map.get("proxyPassword")));
return h;
}
private static int parseInt(Object obj, int def) {
if (obj == null) return def;
if (obj instanceof Number) return ((Number) obj).intValue();
try {
return Integer.parseInt(String.valueOf(obj).trim());
} catch (Exception e) {
return def;
}
}
private static boolean parseBool(Object obj, boolean def) {
if (obj == null) return def;
if (obj instanceof Boolean) return (Boolean) obj;
return Boolean.parseBoolean(String.valueOf(obj).trim());
}
/**
* Minimal pure-Java JSON parser for nested Maps, Lists, Strings, Numbers, and Booleans.
*/
static class SimpleJsonParser {
private final String src;
private int pos = 0;
SimpleJsonParser(String src) {
this.src = src;
}
Object parseValue() {
skipWhitespace();
if (pos >= src.length()) return null;
char c = src.charAt(pos);
if (c == '{') return parseObject();
if (c == '[') return parseArray();
if (c == '"') return parseString();
if (c == 't' || c == 'f') return parseBoolean();
if (c == 'n') return parseNull();
if (c == '-' || Character.isDigit(c)) return parseNumber();
throw new IllegalArgumentException("Unexpected char '" + c + "' at position " + pos);
}
private Map<String, Object> parseObject() {
match('{');
Map<String, Object> map = new LinkedHashMap<>();
skipWhitespace();
if (peek() == '}') {
pos++;
return map;
}
while (true) {
skipWhitespace();
String key = parseString();
skipWhitespace();
match(':');
Object val = parseValue();
map.put(key, val);
skipWhitespace();
char c = peek();
if (c == ',') {
pos++;
} else if (c == '}') {
pos++;
break;
} else {
break;
}
}
return map;
}
private List<Object> parseArray() {
match('[');
List<Object> list = new ArrayList<>();
skipWhitespace();
if (peek() == ']') {
pos++;
return list;
}
while (true) {
Object val = parseValue();
list.add(val);
skipWhitespace();
char c = peek();
if (c == ',') {
pos++;
} else if (c == ']') {
pos++;
break;
} else {
break;
}
}
return list;
}
private String parseString() {
match('"');
StringBuilder sb = new StringBuilder();
while (pos < src.length()) {
char c = src.charAt(pos++);
if (c == '"') {
return sb.toString();
} else if (c == '\\') {
if (pos >= src.length()) break;
char esc = src.charAt(pos++);
switch (esc) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case 'u':
if (pos + 4 <= src.length()) {
String hex = src.substring(pos, pos + 4);
pos += 4;
try {
sb.append((char) Integer.parseInt(hex, 16));
} catch (NumberFormatException e) {
sb.append("?");
}
}
break;
default:
sb.append(esc);
break;
}
} else {
sb.append(c);
}
}
return sb.toString();
}
private Boolean parseBoolean() {
if (src.startsWith("true", pos)) {
pos += 4;
return true;
} else if (src.startsWith("false", pos)) {
pos += 5;
return false;
}
throw new IllegalArgumentException("Expected boolean at " + pos);
}
private Object parseNull() {
if (src.startsWith("null", pos)) {
pos += 4;
return null;
}
throw new IllegalArgumentException("Expected null at " + pos);
}
private Number parseNumber() {
int start = pos;
if (src.charAt(pos) == '-') pos++;
while (pos < src.length() && (Character.isDigit(src.charAt(pos)) || src.charAt(pos) == '.' || src.charAt(pos) == 'e' || src.charAt(pos) == 'E' || src.charAt(pos) == '+' || src.charAt(pos) == '-')) {
pos++;
}
String numStr = src.substring(start, pos);
if (numStr.contains(".")) {
return Double.parseDouble(numStr);
}
return Long.parseLong(numStr);
}
private void skipWhitespace() {
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
pos++;
}
}
private char peek() {
return pos < src.length() ? src.charAt(pos) : '\0';
}
private void match(char expected) {
skipWhitespace();
if (pos >= src.length() || src.charAt(pos) != expected) {
throw new IllegalArgumentException("Expected '" + expected + "' at " + pos);
}
pos++;
}
}
}
@@ -0,0 +1,222 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import java.util.Objects;
import java.util.UUID;
/**
* Data model for a saved 3270 connection profile, matching the schema of a3270 SavedHost.
* Includes per-connection host, model, protocol, and proxy configuration.
*/
public class SavedHost {
private String id;
private String name;
private String host;
private int port;
private int model;
private int dynamicRows;
private int dynamicCols;
private String luName;
private boolean autoConnect;
private String hostType;
private boolean useTls;
private boolean tlsVerifyCert;
private boolean tn3270e;
private String graphicsMode;
private String codePage;
// Per-connection proxy configuration
private String proxyType;
private String proxyHost;
private int proxyPort;
private String proxyUsername;
private String proxyPassword;
public SavedHost() {
this.id = UUID.randomUUID().toString();
this.name = "Mainframe";
this.host = "127.0.0.1";
this.port = 23;
this.model = 4;
this.dynamicRows = 62;
this.dynamicCols = 160;
this.luName = "";
this.autoConnect = false;
this.hostType = "TSO";
this.useTls = false;
this.tlsVerifyCert = true;
this.tn3270e = true;
this.graphicsMode = "BOTH";
this.codePage = "037";
this.proxyType = "NONE";
this.proxyHost = "";
this.proxyPort = 0;
this.proxyUsername = "";
this.proxyPassword = "";
}
public SavedHost(String id, String name, String host, int port, int model,
int dynamicRows, int dynamicCols, String luName, boolean autoConnect,
String hostType, boolean useTls, boolean tlsVerifyCert, boolean tn3270e,
String graphicsMode, String codePage, String proxyType, String proxyHost,
int proxyPort, String proxyUsername, String proxyPassword) {
this.id = (id != null && !id.trim().isEmpty()) ? id : UUID.randomUUID().toString();
this.name = name != null ? name : "";
this.host = host != null ? host : "127.0.0.1";
this.port = port > 0 ? port : 23;
this.model = model;
this.dynamicRows = dynamicRows > 0 ? dynamicRows : 62;
this.dynamicCols = dynamicCols > 0 ? dynamicCols : 160;
this.luName = luName != null ? luName : "";
this.autoConnect = autoConnect;
this.hostType = hostType != null ? hostType : "TSO";
this.useTls = useTls;
this.tlsVerifyCert = tlsVerifyCert;
this.tn3270e = tn3270e;
this.graphicsMode = graphicsMode != null ? graphicsMode : "BOTH";
this.codePage = codePage != null ? codePage : "037";
this.proxyType = (proxyType != null && !proxyType.trim().isEmpty()) ? proxyType.toUpperCase() : "NONE";
this.proxyHost = proxyHost != null ? proxyHost : "";
this.proxyPort = proxyPort;
this.proxyUsername = proxyUsername != null ? proxyUsername : "";
this.proxyPassword = proxyPassword != null ? proxyPassword : "";
}
public SavedHost copy() {
return new SavedHost(id, name, host, port, model, dynamicRows, dynamicCols, luName,
autoConnect, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, codePage,
proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword);
}
/**
* Converts this saved host profile into an active ConnectionConfig.
*/
public ConnectionConfig toConnectionConfig() {
TerminalModel tm;
if (model == 0) {
tm = TerminalModel.IBM_DYNAMIC;
} else {
try {
tm = TerminalModel.forModel(model, true);
} catch (Exception e) {
tm = TerminalModel.IBM_3279_4;
}
}
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
if (model == 0 || tm.isDynamic()) {
config.setDynamicDimensions(dynamicRows, dynamicCols);
}
if (luName != null && !luName.trim().isEmpty()) {
config.setLuName(luName.trim());
}
config.setTlsVerifyCert(tlsVerifyCert);
config.setTn3270eEnabled(tn3270e);
if (graphicsMode != null && !graphicsMode.isEmpty()) {
config.setGraphicsMode(GraphicsMode.fromString(graphicsMode));
}
if (codePage != null && !codePage.isEmpty()) {
config.setCodePage(codePage);
}
// Apply per-connection proxy if configured
if (proxyType != null && !proxyType.equalsIgnoreCase("NONE") && proxyHost != null && !proxyHost.trim().isEmpty()) {
try {
ConnectionConfig.ProxyType pt = ConnectionConfig.ProxyType.valueOf(proxyType.toUpperCase());
int pPort = proxyPort > 0 ? proxyPort : (pt == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080);
config.setProxy(pt, proxyHost.trim(), pPort,
(proxyUsername != null && !proxyUsername.trim().isEmpty()) ? proxyUsername.trim() : null,
(proxyPassword != null && !proxyPassword.isEmpty()) ? proxyPassword : null);
} catch (IllegalArgumentException ignored) {
}
}
return config;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public int getModel() { return model; }
public void setModel(int model) { this.model = model; }
public int getDynamicRows() { return dynamicRows; }
public void setDynamicRows(int dynamicRows) { this.dynamicRows = dynamicRows; }
public int getDynamicCols() { return dynamicCols; }
public void setDynamicCols(int dynamicCols) { this.dynamicCols = dynamicCols; }
public String getLuName() { return luName; }
public void setLuName(String luName) { this.luName = luName; }
public boolean isAutoConnect() { return autoConnect; }
public void setAutoConnect(boolean autoConnect) { this.autoConnect = autoConnect; }
public String getHostType() { return hostType; }
public void setHostType(String hostType) { this.hostType = hostType; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
public boolean isTn3270e() { return tn3270e; }
public void setTn3270e(boolean tn3270e) { this.tn3270e = tn3270e; }
public String getGraphicsMode() { return graphicsMode; }
public void setGraphicsMode(String graphicsMode) { this.graphicsMode = graphicsMode; }
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) { this.codePage = codePage; }
public String getProxyType() { return proxyType != null ? proxyType : "NONE"; }
public void setProxyType(String proxyType) { this.proxyType = (proxyType != null) ? proxyType.toUpperCase() : "NONE"; }
public String getProxyHost() { return proxyHost != null ? proxyHost : ""; }
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
public int getProxyPort() { return proxyPort; }
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
public String getProxyUsername() { return proxyUsername != null ? proxyUsername : ""; }
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
public String getProxyPassword() { return proxyPassword != null ? proxyPassword : ""; }
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
@Override
public String toString() {
if (name != null && !name.trim().isEmpty()) {
return name;
}
return host + (port > 0 ? ":" + port : "");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SavedHost savedHost = (SavedHost) o;
return Objects.equals(id, savedHost.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -0,0 +1,138 @@
package haus.nightmare.j3270.ui;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility for loading and applying application window and taskbar icons.
* Loads multi-resolution lib3270j icons from resources.
*/
public final class AppIcon {
private static final Logger log = Logger.getLogger(AppIcon.class.getName());
private static final int[] ICON_SIZES = {16, 24, 32, 48, 64, 128, 256, 512};
private static List<Image> cachedIcons = null;
private static Image defaultIcon = null;
private AppIcon() {}
/**
* Retrieves the list of available application icon images in various resolutions.
*
* @return an unmodifiable list of Image objects, or empty list if none could be loaded.
*/
public static synchronized List<Image> getAppIcons() {
if (cachedIcons == null) {
loadIcons();
}
return cachedIcons;
}
/**
* Retrieves the default application icon image (256x256 or best available).
*
* @return the default Image, or null if no icon is available.
*/
public static synchronized Image getDefaultIcon() {
if (cachedIcons == null) {
loadIcons();
}
return defaultIcon;
}
/**
* Applies the application icons to a target Window (JFrame, JDialog, etc.)
* and updates the desktop Taskbar icon if supported.
*
* @param window the Window to set icon images for.
*/
public static void applyTo(Window window) {
if (GraphicsEnvironment.isHeadless()) {
return;
}
List<Image> icons = getAppIcons();
if (icons.isEmpty()) {
return;
}
if (window != null) {
window.setIconImages(icons);
}
// Apply to OS Taskbar / Dock if supported (Java 9+)
try {
if (Taskbar.isTaskbarSupported()) {
Taskbar taskbar = Taskbar.getTaskbar();
if (taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) {
Image bestIcon = defaultIcon != null ? defaultIcon : icons.get(icons.size() - 1);
taskbar.setIconImage(bestIcon);
}
}
} catch (Throwable t) {
log.log(Level.FINE, "Taskbar icon could not be set: {0}", t.getMessage());
}
}
private static void loadIcons() {
List<Image> icons = new ArrayList<>();
for (int size : ICON_SIZES) {
String path = "/icons/lib3270j_" + size + ".png";
Image img = loadImage(path);
if (img != null) {
icons.add(img);
}
}
if (icons.isEmpty()) {
Image img = loadImage("/icons/lib3270j.png");
if (img != null) {
icons.add(img);
}
}
if (!icons.isEmpty()) {
for (Image img : icons) {
if (img.getWidth(null) == 256) {
defaultIcon = img;
break;
}
}
if (defaultIcon == null) {
defaultIcon = icons.get(icons.size() - 1);
}
}
cachedIcons = Collections.unmodifiableList(icons);
}
private static Image loadImage(String resourcePath) {
try {
URL url = AppIcon.class.getResource(resourcePath);
if (url != null) {
BufferedImage img = ImageIO.read(url);
if (img != null) {
return img;
}
}
try (InputStream in = AppIcon.class.getResourceAsStream(resourcePath)) {
if (in != null) {
return ImageIO.read(in);
}
}
} catch (Exception e) {
log.log(Level.FINE, "Failed to load icon from " + resourcePath, e);
}
return null;
}
}
@@ -1,16 +1,31 @@
package haus.nightmare.j3270.ui; package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.lib3270j.ConnectionConfig; import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.TerminalModel;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*; import java.awt.*;
import java.util.List;
/** /**
* Connection dialog for entering host, port, model, and LU name. * Connection dialog for entering host, port, model, LU name, and per-connection proxy configuration.
* Supports loading and saving host profiles matching the a3270 schema.
*/ */
public class ConnectDialog extends JDialog { public class ConnectDialog extends JDialog {
private static final String NEW_PROFILE_LABEL = "<New / Custom Connection>";
// Profile selector
private JComboBox<Object> profileCombo;
private JButton saveProfileBtn;
private JButton deleteProfileBtn;
private String selectedProfileId = null;
// Connection parameters
private JTextField hostField; private JTextField hostField;
private JTextField portField; private JTextField portField;
private JComboBox<TerminalModel> modelCombo; private JComboBox<TerminalModel> modelCombo;
@@ -26,72 +41,131 @@ public class ConnectDialog extends JDialog {
private JCheckBox tn3270eCheckBox; private JCheckBox tn3270eCheckBox;
private JCheckBox keepAliveCheckBox; private JCheckBox keepAliveCheckBox;
private JCheckBox autoReconnectCheckBox; private JCheckBox autoReconnectCheckBox;
// Per-connection proxy configuration
private JComboBox<ConnectionConfig.ProxyType> proxyTypeCombo;
private JTextField proxyHostField;
private JTextField proxyPortField;
private JTextField proxyUserField;
private JPasswordField proxyPassField;
private boolean confirmed; private boolean confirmed;
private ConnectionConfig result; private ConnectionConfig result;
public ConnectDialog(Frame parent) { public ConnectDialog(Frame parent) {
super(parent, "Connect to Host", true); super(parent, "Connect to Host", true);
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
setResizable(false); setResizable(true);
} }
private void buildUI() { private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout()); JPanel contentPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
GridBagConstraints gbc = new GridBagConstraints(); GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 6, 5, 6); gbc.insets = new Insets(4, 5, 4, 5);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Host int gridy = 0;
// 0. Profile Row
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 0; gbc.gridy = gridy;
gbc.weightx = 0;
JLabel profileLabel = new JLabel("Profile:");
profileLabel.setFont(labelFont);
contentPanel.add(profileLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
JPanel profileBar = new JPanel(new BorderLayout(6, 0));
profileBar.setOpaque(false);
profileCombo = new JComboBox<>();
profileCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(profileCombo);
refreshProfileCombo();
profileCombo.addActionListener(e -> onProfileSelectionChanged());
profileBar.add(profileCombo, BorderLayout.CENTER);
JPanel profileBtnBar = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
profileBtnBar.setOpaque(false);
saveProfileBtn = new JButton("Save Profile...");
ThemeManager.styleButton(saveProfileBtn, ThemeManager.ButtonVariant.DEFAULT);
saveProfileBtn.setFont(labelFont);
saveProfileBtn.addActionListener(e -> onSaveProfile());
deleteProfileBtn = new JButton("Delete");
ThemeManager.styleButton(deleteProfileBtn, ThemeManager.ButtonVariant.CANCEL);
deleteProfileBtn.setFont(labelFont);
deleteProfileBtn.setEnabled(false);
deleteProfileBtn.addActionListener(e -> onDeleteProfile());
profileBtnBar.add(saveProfileBtn);
profileBtnBar.add(deleteProfileBtn);
profileBar.add(profileBtnBar, BorderLayout.EAST);
contentPanel.add(profileBar, gbc);
gridy++;
// 1. Host
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel hostLabel = new JLabel("Host:"); JLabel hostLabel = new JLabel("Host:");
hostLabel.setFont(labelFont); hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc); contentPanel.add(hostLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
hostField = createField(20); hostField = createField(20);
mainPanel.add(hostField, gbc); contentPanel.add(hostField, gbc);
// Port gridy++;
// 2. Port
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 1; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:"); JLabel portLabel = new JLabel("Port:");
portLabel.setFont(labelFont); portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc); contentPanel.add(portLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
portField = createField(6); portField = createField(6);
portField.setText("23"); portField.setText("23");
mainPanel.add(portField, gbc); contentPanel.add(portField, gbc);
// Model gridy++;
// 3. Model
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 2; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:"); JLabel modelLabel = new JLabel("Model:");
modelLabel.setFont(labelFont); modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc); contentPanel.add(modelLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values()); modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4); modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); modelCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(modelCombo); ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc); contentPanel.add(modelCombo, gbc);
// Dynamic Dimensions (shown when IBM-DYNAMIC is selected) gridy++;
// 4. Dynamic Dimensions
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 3; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
dynamicDimLabel = new JLabel("Screen Size:"); dynamicDimLabel = new JLabel("Screen Size:");
dynamicDimLabel.setFont(labelFont); dynamicDimLabel.setFont(labelFont);
mainPanel.add(dynamicDimLabel, gbc); contentPanel.add(dynamicDimLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
@@ -112,7 +186,7 @@ public class ConnectDialog extends JDialog {
dynamicDimPanel.add(dynamicRowsSpinner); dynamicDimPanel.add(dynamicRowsSpinner);
dynamicDimPanel.add(colsLabel); dynamicDimPanel.add(colsLabel);
dynamicDimPanel.add(dynamicColsSpinner); dynamicDimPanel.add(dynamicColsSpinner);
mainPanel.add(dynamicDimPanel, gbc); contentPanel.add(dynamicDimPanel, gbc);
Runnable updateDynamicVisibility = () -> { Runnable updateDynamicVisibility = () -> {
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem(); TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
@@ -124,40 +198,64 @@ public class ConnectDialog extends JDialog {
modelCombo.addActionListener(e -> updateDynamicVisibility.run()); modelCombo.addActionListener(e -> updateDynamicVisibility.run());
updateDynamicVisibility.run(); updateDynamicVisibility.run();
// LU Name gridy++;
// 5. LU Name
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 4; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:"); JLabel luLabel = new JLabel("LU Name:");
luLabel.setFont(labelFont); luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc); contentPanel.add(luLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
luField = createField(12); luField = createField(12);
mainPanel.add(luField, gbc); contentPanel.add(luField, gbc);
// Graphics Mode gridy++;
// 6. Graphics Mode
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 5; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:"); JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setFont(labelFont); graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc); contentPanel.add(graphicsLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values()); graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode()); graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); graphicsCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(graphicsCombo); ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc); graphicsCombo.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setBorder(new EmptyBorder(4, 8, 4, 8));
if (value instanceof haus.nightmare.lib3270j.graphics.GraphicsMode) {
setText(((haus.nightmare.lib3270j.graphics.GraphicsMode) value).getDescription());
}
if (isSelected) {
setBackground(ThemeManager.getSelectionBg());
setForeground(ThemeManager.getSelectionFg());
} else {
setBackground(ThemeManager.getBgComponent());
setForeground(ThemeManager.getFgMain());
}
return this;
}
});
contentPanel.add(graphicsCombo, gbc);
// Code Page gridy++;
// 7. Code Page
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 6; gbc.gridy = gridy;
gbc.weightx = 0; gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:"); JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setFont(labelFont); cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc); contentPanel.add(cpLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
String[] commonCodePages = { String[] commonCodePages = {
@@ -178,43 +276,44 @@ public class ConnectDialog extends JDialog {
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)", "1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)", "905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual", "420 - Arabic Bilingual",
"424 - Hebrew (with Lowercase)", "424 - Hebrew (with Latin)",
"803 - Hebrew Old / Standard", "803 - Hebrew Character Set",
"838 - Thai", "838 - Thai Extended",
"1160 - Thai (Euro \u20AC)", "1160 - Thai Euro \u20AC",
"1025 - Cyrillic Multilingual", "1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine", "1123 - Cyrillic Ukraine",
"1154 - Cyrillic Multilingual (Euro \u20AC)", "1154 - Cyrillic Euro \u20AC",
"880 - Cyrillic Russian", "880 - Cyrillic (Russian)",
"1140 - US / Canada (Euro \u20AC)", "1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)", "1141 - Germany / Austria (Euro \u20AC)",
"1142 - Denmark / Norway (Euro \u20AC)",
"1143 - Sweden / Finland (Euro \u20AC)",
"1144 - Italy (Euro \u20AC)",
"1145 - Spain / Latin America (Euro \u20AC)",
"1146 - United Kingdom (Euro \u20AC)",
"1147 - France (Euro \u20AC)",
"1148 - International (Euro \u20AC)", "1148 - International (Euro \u20AC)",
"1149 - Iceland (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS", "930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS", "939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS", "935 - Simplified Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS", "937 - Traditional Chinese Mixed DBCS",
"1371 - Traditional Chinese Extended Mixed DBCS", "1388 - Simplified Chinese Extended DBCS",
"1371 - Traditional Chinese Extended DBCS",
"933 - Korean Mixed DBCS" "933 - Korean Mixed DBCS"
}; };
codePageCombo = new JComboBox<>(commonCodePages); codePageCombo = new JComboBox<>(commonCodePages);
codePageCombo.setEditable(true); codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
String currentCp = haus.nightmare.j3270.config.Settings.getCodePage();
for (String item : commonCodePages) {
if (item.startsWith(currentCp + " ") || item.equals(currentCp)) {
codePageCombo.setSelectedItem(item);
break;
}
}
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
ThemeManager.styleComboBox(codePageCombo); ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc); setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
contentPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox gridy++;
// 8. TLS Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 7; gbc.gridy = gridy;
gbc.weightx = 1.0; tlsCheckBox = new JCheckBox("Use TLS / SSL Encryption");
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
ThemeManager.styleCheckBox(tlsCheckBox); ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setFont(labelFont); tlsCheckBox.setFont(labelFont);
tlsCheckBox.addActionListener(e -> { tlsCheckBox.addActionListener(e -> {
@@ -227,46 +326,169 @@ public class ConnectDialog extends JDialog {
portField.setText("23"); portField.setText("23");
} }
}); });
mainPanel.add(tlsCheckBox, gbc); contentPanel.add(tlsCheckBox, gbc);
// Verify Certificate Checkbox gridy++;
// 9. Verify Cert Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 8; gbc.gridy = gridy;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
ThemeManager.styleCheckBox(verifyCertCheckBox); ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); verifyCertCheckBox.setFont(ThemeManager.getUiFont());
verifyCertCheckBox.setSelected(true); verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false); verifyCertCheckBox.setEnabled(false);
mainPanel.add(verifyCertCheckBox, gbc); contentPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox gridy++;
// 10. TN3270E Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 9; gbc.gridy = gridy;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
ThemeManager.styleCheckBox(tn3270eCheckBox); ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); tn3270eCheckBox.setFont(ThemeManager.getUiFont());
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e()); tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
mainPanel.add(tn3270eCheckBox, gbc); contentPanel.add(tn3270eCheckBox, gbc);
// Keep-Alive Checkbox gridy++;
// 11. Keep-Alive Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 10; gbc.gridy = gridy;
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)"); keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
ThemeManager.styleCheckBox(keepAliveCheckBox); ThemeManager.styleCheckBox(keepAliveCheckBox);
keepAliveCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); keepAliveCheckBox.setFont(ThemeManager.getUiFont());
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive()); keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
mainPanel.add(keepAliveCheckBox, gbc); contentPanel.add(keepAliveCheckBox, gbc);
// Auto-Reconnect Checkbox gridy++;
// 12. Auto-Reconnect Checkbox
gbc.gridx = 1; gbc.gridx = 1;
gbc.gridy = 11; gbc.gridy = gridy;
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect"); autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
ThemeManager.styleCheckBox(autoReconnectCheckBox); ThemeManager.styleCheckBox(autoReconnectCheckBox);
autoReconnectCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); autoReconnectCheckBox.setFont(ThemeManager.getUiFont());
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect()); autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
mainPanel.add(autoReconnectCheckBox, gbc); contentPanel.add(autoReconnectCheckBox, gbc);
gridy++;
// 13. PER-CONNECTION PROXY CONFIGURATION PANEL
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
JPanel proxyGroup = new JPanel(new GridBagLayout());
proxyGroup.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(),
"Proxy Configuration (This Connection)",
TitledBorder.LEFT,
TitledBorder.TOP,
labelFont
));
GridBagConstraints pgbc = new GridBagConstraints();
pgbc.insets = new Insets(3, 6, 3, 6);
pgbc.fill = GridBagConstraints.HORIZONTAL;
// Proxy Type
pgbc.gridx = 0;
pgbc.gridy = 0;
pgbc.weightx = 0;
JLabel ptLabel = new JLabel("Proxy Type:");
ptLabel.setFont(labelFont);
proxyGroup.add(ptLabel, pgbc);
pgbc.gridx = 1;
pgbc.gridwidth = 3;
pgbc.weightx = 1.0;
proxyTypeCombo = new JComboBox<>(ConnectionConfig.ProxyType.values());
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
proxyTypeCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(proxyTypeCombo);
proxyGroup.add(proxyTypeCombo, pgbc);
// Proxy Host & Port
pgbc.gridx = 0;
pgbc.gridy = 1;
pgbc.gridwidth = 1;
pgbc.weightx = 0;
JLabel phLabel = new JLabel("Proxy Host:");
phLabel.setFont(labelFont);
proxyGroup.add(phLabel, pgbc);
pgbc.gridx = 1;
pgbc.weightx = 1.0;
proxyHostField = createField(14);
proxyGroup.add(proxyHostField, pgbc);
pgbc.gridx = 2;
pgbc.weightx = 0;
JLabel ppLabel = new JLabel("Port:");
ppLabel.setFont(labelFont);
proxyGroup.add(ppLabel, pgbc);
pgbc.gridx = 3;
pgbc.weightx = 0.4;
proxyPortField = createField(5);
proxyGroup.add(proxyPortField, pgbc);
// Proxy User & Password
pgbc.gridx = 0;
pgbc.gridy = 2;
pgbc.gridwidth = 1;
pgbc.weightx = 0;
JLabel puLabel = new JLabel("Username:");
puLabel.setFont(labelFont);
proxyGroup.add(puLabel, pgbc);
pgbc.gridx = 1;
pgbc.weightx = 1.0;
proxyUserField = createField(12);
proxyGroup.add(proxyUserField, pgbc);
pgbc.gridx = 2;
pgbc.weightx = 0;
JLabel pwLabel = new JLabel("Password:");
pwLabel.setFont(labelFont);
proxyGroup.add(pwLabel, pgbc);
pgbc.gridx = 3;
pgbc.weightx = 0.4;
proxyPassField = createPasswordField(10);
proxyGroup.add(proxyPassField, pgbc);
// Dynamic enable/disable based on Proxy Type
Runnable updateProxyFields = () -> {
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
boolean active = (pt != null && pt != ConnectionConfig.ProxyType.NONE);
proxyHostField.setEnabled(active);
proxyPortField.setEnabled(active);
proxyUserField.setEnabled(active);
proxyPassField.setEnabled(active && pt != ConnectionConfig.ProxyType.SOCKS4);
if (active) {
String currentPPort = proxyPortField.getText().trim();
if (currentPPort.isEmpty() || "0".equals(currentPPort)) {
proxyPortField.setText(pt == ConnectionConfig.ProxyType.HTTP ? "8080" : "1080");
}
}
};
proxyTypeCombo.addActionListener(e -> updateProxyFields.run());
updateProxyFields.run();
contentPanel.add(proxyGroup, gbc);
gridy++;
// 14. Action Buttons
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
JButton connectBtn = new JButton("Connect"); JButton connectBtn = new JButton("Connect");
@@ -284,13 +506,14 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(cancelBtn); buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn); buttonPanel.add(connectBtn);
contentPanel.add(buttonPanel, gbc);
gbc.gridx = 0; JScrollPane scrollPane = new JScrollPane(contentPanel);
gbc.gridy = 12; scrollPane.setBorder(BorderFactory.createEmptyBorder());
gbc.gridwidth = 2; scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
mainPanel.add(buttonPanel, gbc); scrollPane.getVerticalScrollBar().setUnitIncrement(14);
setContentPane(scrollPane);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this); ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect // Enter key triggers connect
@@ -299,11 +522,194 @@ public class ConnectDialog extends JDialog {
private JTextField createField(int cols) { private JTextField createField(int cols) {
JTextField field = new JTextField(cols); JTextField field = new JTextField(cols);
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field); ThemeManager.styleTextField(field);
return field; return field;
} }
private JPasswordField createPasswordField(int cols) {
JPasswordField field = new JPasswordField(cols);
field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field);
return field;
}
// ================= Profile Management =================
private void refreshProfileCombo() {
profileCombo.removeAllItems();
profileCombo.addItem(NEW_PROFILE_LABEL);
List<SavedHost> saved = HostStorage.getSavedHosts();
for (SavedHost h : saved) {
profileCombo.addItem(h);
}
if (selectedProfileId != null) {
for (int i = 0; i < profileCombo.getItemCount(); i++) {
Object item = profileCombo.getItemAt(i);
if (item instanceof SavedHost && ((SavedHost) item).getId().equals(selectedProfileId)) {
profileCombo.setSelectedIndex(i);
return;
}
}
}
profileCombo.setSelectedIndex(0);
}
private void onProfileSelectionChanged() {
Object sel = profileCombo.getSelectedItem();
if (sel instanceof SavedHost) {
SavedHost h = (SavedHost) sel;
selectedProfileId = h.getId();
deleteProfileBtn.setEnabled(true);
loadProfileFields(h);
} else {
selectedProfileId = null;
deleteProfileBtn.setEnabled(false);
}
}
public void loadProfile(SavedHost h) {
if (h == null) return;
selectedProfileId = h.getId();
refreshProfileCombo();
loadProfileFields(h);
}
private void loadProfileFields(SavedHost h) {
if (h == null) return;
hostField.setText(h.getHost());
portField.setText(String.valueOf(h.getPort()));
if (h.getModel() == 0) {
modelCombo.setSelectedItem(TerminalModel.IBM_DYNAMIC);
} else {
try {
modelCombo.setSelectedItem(TerminalModel.forModel(h.getModel(), true));
} catch (Exception ignored) {
}
}
dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62);
dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160);
luField.setText(h.getLuName() != null ? h.getLuName() : "");
tlsCheckBox.setSelected(h.isUseTls());
verifyCertCheckBox.setEnabled(h.isUseTls());
verifyCertCheckBox.setSelected(h.isTlsVerifyCert());
tn3270eCheckBox.setSelected(h.isTn3270e());
setInitialCodePage(h.getCodePage());
if (h.getGraphicsMode() != null) {
graphicsCombo.setSelectedItem(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(h.getGraphicsMode()));
}
// Per-connection proxy parameters
String ptStr = h.getProxyType();
try {
ConnectionConfig.ProxyType pt = (ptStr != null && !ptStr.trim().isEmpty())
? ConnectionConfig.ProxyType.valueOf(ptStr.toUpperCase())
: ConnectionConfig.ProxyType.NONE;
proxyTypeCombo.setSelectedItem(pt);
} catch (IllegalArgumentException e) {
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
}
proxyHostField.setText(h.getProxyHost() != null ? h.getProxyHost() : "");
proxyPortField.setText(h.getProxyPort() > 0 ? String.valueOf(h.getProxyPort()) : "");
proxyUserField.setText(h.getProxyUsername() != null ? h.getProxyUsername() : "");
proxyPassField.setText(h.getProxyPassword() != null ? h.getProxyPassword() : "");
}
private void onSaveProfile() {
String hStr = hostField.getText().trim();
if (hStr.isEmpty()) {
hostField.requestFocus();
JOptionPane.showMessageDialog(this, "Please specify a host before saving profile.", "Missing Host", JOptionPane.WARNING_MESSAGE);
return;
}
String defaultName = hStr;
Object currentSel = profileCombo.getSelectedItem();
if (currentSel instanceof SavedHost) {
defaultName = ((SavedHost) currentSel).getName();
}
String profileName = (String) JOptionPane.showInputDialog(
this,
"Enter Host Profile Name:",
"Save Profile",
JOptionPane.PLAIN_MESSAGE,
null,
null,
defaultName
);
if (profileName == null || profileName.trim().isEmpty()) {
return;
}
profileName = profileName.trim();
SavedHost hostToSave = buildCurrentSavedHost(profileName);
HostStorage.saveHost(hostToSave);
selectedProfileId = hostToSave.getId();
refreshProfileCombo();
}
private void onDeleteProfile() {
if (selectedProfileId == null) return;
int opt = JOptionPane.showConfirmDialog(this,
"Are you sure you want to delete this host profile?",
"Delete Profile",
JOptionPane.YES_NO_OPTION);
if (opt == JOptionPane.YES_OPTION) {
HostStorage.deleteHost(selectedProfileId);
selectedProfileId = null;
refreshProfileCombo();
}
}
private SavedHost buildCurrentSavedHost(String profileName) {
String id = selectedProfileId != null ? selectedProfileId : java.util.UUID.randomUUID().toString();
String h = hostField.getText().trim();
int port = 23;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException ignored) {}
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
int mNum = (m != null) ? m.getModelNumber() : 4;
int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue();
String lu = luField.getText().trim();
boolean tls = tlsCheckBox.isSelected();
boolean verify = verifyCertCheckBox.isSelected();
boolean tn3270e = tn3270eCheckBox.isSelected();
String gfx = "BOTH";
if (graphicsCombo.getSelectedItem() != null) {
gfx = ((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()).name();
}
String cp = "037";
Object cpSel = codePageCombo.getSelectedItem();
if (cpSel != null) {
String s = cpSel.toString().trim();
int dash = s.indexOf(" -");
cp = (dash > 0) ? s.substring(0, dash).trim() : s;
}
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
String ptStr = (pt != null) ? pt.name() : "NONE";
String pHost = proxyHostField.getText().trim();
int pPort = 0;
try {
pPort = Integer.parseInt(proxyPortField.getText().trim());
} catch (NumberFormatException ignored) {}
String pUser = proxyUserField.getText().trim();
String pPass = new String(proxyPassField.getPassword());
return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false,
"TSO", tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
}
// ================= Connect Action =================
private void onConnect() { private void onConnect() {
String host = hostField.getText().trim(); String host = hostField.getText().trim();
if (host.isEmpty()) { if (host.isEmpty()) {
@@ -320,19 +726,34 @@ public class ConnectDialog extends JDialog {
} }
TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem(); TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
if (selectedModel == null) {
selectedModel = TerminalModel.IBM_3279_4;
}
// Support full host string parsing if the user pasted flags or prefixes into host field
if (host.contains(" ") || host.startsWith("L:") || host.startsWith("P:") || host.startsWith("--proxy=") || host.startsWith("-proxy=")) {
result = ConnectionConfig.parseHostString(host, port, selectedModel);
} else {
result = new ConnectionConfig(host, port, selectedModel); result = new ConnectionConfig(host, port, selectedModel);
if (selectedModel != null && selectedModel.isDynamic()) { }
if (selectedModel.isDynamic()) {
int dRows = (Integer) dynamicRowsSpinner.getValue(); int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue(); int dCols = (Integer) dynamicColsSpinner.getValue();
result.setDynamicDimensions(dRows, dCols); result.setDynamicDimensions(dRows, dCols);
haus.nightmare.j3270.config.Settings.setDynamicRows(dRows); haus.nightmare.j3270.config.Settings.setDynamicRows(dRows);
haus.nightmare.j3270.config.Settings.setDynamicCols(dCols); haus.nightmare.j3270.config.Settings.setDynamicCols(dCols);
} }
String lu = luField.getText().trim(); String lu = luField.getText().trim();
if (!lu.isEmpty()) { if (!lu.isEmpty()) {
result.setLuName(lu); result.setLuName(lu);
} }
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()); if (graphicsCombo.getSelectedItem() != null) {
haus.nightmare.lib3270j.graphics.GraphicsMode gm = (haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem();
result.setGraphicsMode(gm);
haus.nightmare.j3270.config.Settings.setGraphicsMode(gm);
}
Object cpSelection = codePageCombo.getSelectedItem(); Object cpSelection = codePageCombo.getSelectedItem();
if (cpSelection != null) { if (cpSelection != null) {
@@ -360,6 +781,30 @@ public class ConnectDialog extends JDialog {
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries()); result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar); haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
// Apply per-connection proxy parameters strictly to this connection
ConnectionConfig.ProxyType pType = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
String pHost = proxyHostField.getText().trim();
if (pType != null && pType != ConnectionConfig.ProxyType.NONE && !pHost.isEmpty()) {
int pPort = 0;
try {
pPort = Integer.parseInt(proxyPortField.getText().trim());
} catch (NumberFormatException ignored) {}
if (pPort <= 0) {
pPort = (pType == ConnectionConfig.ProxyType.HTTP) ? 8080 : 1080;
}
String pUser = proxyUserField.getText().trim();
String pPass = new String(proxyPassField.getPassword());
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
}
if (selectedProfileId != null) {
SavedHost existing = HostStorage.findById(selectedProfileId);
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
? existing.getName() : host;
SavedHost updated = buildCurrentSavedHost(pName);
HostStorage.saveHost(updated);
}
confirmed = true; confirmed = true;
dispose(); dispose();
} }
@@ -394,6 +839,12 @@ public class ConnectDialog extends JDialog {
tn3270eCheckBox.setSelected(tn3270e); tn3270eCheckBox.setSelected(tn3270e);
} }
public void setInitialGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
if (mode != null && selectedProfileId == null && graphicsCombo != null) {
graphicsCombo.setSelectedItem(mode);
}
}
public void setInitialCodePage(String cp) { public void setInitialCodePage(String cp) {
if (cp != null && codePageCombo != null) { if (cp != null && codePageCombo != null) {
for (int i = 0; i < codePageCombo.getItemCount(); i++) { for (int i = 0; i < codePageCombo.getItemCount(); i++) {
@@ -406,4 +857,49 @@ public class ConnectDialog extends JDialog {
codePageCombo.setSelectedItem(cp); codePageCombo.setSelectedItem(cp);
} }
} }
/**
* Set initial proxy settings for this connection.
*/
public void setInitialProxy(ConnectionConfig.ProxyType type, String host, int port, String user, String pass) {
if (proxyTypeCombo != null) {
proxyTypeCombo.setSelectedItem(type != null ? type : ConnectionConfig.ProxyType.NONE);
}
if (proxyHostField != null) {
proxyHostField.setText(host != null ? host : "");
}
if (proxyPortField != null) {
proxyPortField.setText(port > 0 ? String.valueOf(port) : "");
}
if (proxyUserField != null) {
proxyUserField.setText(user != null ? user : "");
}
if (proxyPassField != null) {
proxyPassField.setText(pass != null ? pass : "");
}
}
public ConnectionConfig.ProxyType getSelectedProxyType() {
return (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
}
public String getProxyHost() {
return proxyHostField.getText().trim();
}
public int getProxyPort() {
try {
return Integer.parseInt(proxyPortField.getText().trim());
} catch (Exception e) {
return 0;
}
}
public String getProxyUsername() {
return proxyUserField.getText().trim();
}
public String getProxyPassword() {
return new String(proxyPassField.getPassword());
}
} }
@@ -32,6 +32,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
super(parent, "3270 Presentation Space Field Inspector", false); super(parent, "3270 Presentation Space Field Inspector", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
setSize(850, 420); setSize(850, 420);
@@ -60,7 +61,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
JPanel topPanel = new JPanel(new BorderLayout()); JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setOpaque(false); topPanel.setOpaque(false);
countLabel = new JLabel("0 fields detected on screen"); countLabel = new JLabel("0 fields detected on screen");
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); countLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
topPanel.add(countLabel, BorderLayout.WEST); topPanel.add(countLabel, BorderLayout.WEST);
JButton refreshBtn = new JButton("Refresh"); JButton refreshBtn = new JButton("Refresh");
@@ -83,12 +84,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
table = new JTable(tableModel); table = new JTable(tableModel);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
// Column widths // Column widths
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260}; int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
@@ -32,6 +32,7 @@ public class FindDialog extends JDialog {
super(parent, "Find on Screen", false); super(parent, "Find on Screen", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
pack(); pack();
@@ -50,7 +51,7 @@ public class FindDialog extends JDialog {
gbc.insets = new Insets(4, 4, 4, 4); gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Search text // Search text
gbc.gridx = 0; gbc.gridx = 0;
@@ -63,7 +64,7 @@ public class FindDialog extends JDialog {
gbc.weightx = 1.0; gbc.weightx = 1.0;
searchField = new JTextField(20); searchField = new JTextField(20);
searchField.setText(lastSearchText); searchField.setText(lastSearchText);
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); searchField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(searchField); ThemeManager.styleTextField(searchField);
formPanel.add(searchField, gbc); formPanel.add(searchField, gbc);
@@ -102,7 +103,7 @@ public class FindDialog extends JDialog {
gbc.gridwidth = 2; gbc.gridwidth = 2;
statusLabel = new JLabel(" "); statusLabel = new JLabel(" ");
statusLabel.setForeground(ThemeManager.getOiaFgAlert()); statusLabel.setForeground(ThemeManager.getOiaFgAlert());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(labelFont);
formPanel.add(statusLabel, gbc); formPanel.add(statusLabel, gbc);
mainPanel.add(formPanel, BorderLayout.CENTER); mainPanel.add(formPanel, BorderLayout.CENTER);
@@ -40,6 +40,7 @@ public class HostDirectoryDialog extends JDialog {
super(parent, "Host Catalog & Directory Browser", true); super(parent, "Host Catalog & Directory Browser", true);
this.client = client; this.client = client;
this.initialHostType = hostType; this.initialHostType = hostType;
AppIcon.applyTo(this);
buildUI(initialQuery); buildUI(initialQuery);
setSize(700, 420); setSize(700, 420);
@@ -50,7 +51,7 @@ public class HostDirectoryDialog extends JDialog {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12)); mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font font = ThemeManager.getUiFont();
// Top Query bar // Top Query bar
JPanel topPanel = new JPanel(new GridBagLayout()); JPanel topPanel = new JPanel(new GridBagLayout());
@@ -78,7 +79,7 @@ public class HostDirectoryDialog extends JDialog {
gbc.gridx = 3; gbc.gridx = 3;
gbc.weightx = 1.0; gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); queryField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(queryField); ThemeManager.styleTextField(queryField);
topPanel.add(queryField, gbc); topPanel.add(queryField, gbc);
@@ -96,12 +97,12 @@ public class HostDirectoryDialog extends JDialog {
tableModel = new DefaultTableModel(); tableModel = new DefaultTableModel();
table = new JTable(tableModel); table = new JTable(tableModel);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setRowHeight(20); table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
JTableHeader header = table.getTableHeader(); JTableHeader header = table.getTableHeader();
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
table.addMouseListener(new MouseAdapter() { table.addMouseListener(new MouseAdapter() {
@Override @Override
@@ -122,7 +123,7 @@ public class HostDirectoryDialog extends JDialog {
statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(font);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
@@ -0,0 +1,391 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Real-time hotspot and URL auto-detection engine matching IBM Host On-Demand
* ShowURLOptions and ScreenText parity.
*
* Scans screen text using regular expressions for:
* 1. URLs (http://, https://, ftp://, file://, mailto:, www.)
* 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
* 3. Menu choices ("1.", "2.", "===> 3", "=> 4")
*
* Provides hit-testing for mouse hover/clicks and generates keystroke actions.
*/
public class HotspotEngine {
public enum HotspotType {
URL,
PF_KEY,
ENTER,
MENU_CHOICE
}
/**
* Represents an active interactive hotspot region on the terminal screen.
*/
public static class Hotspot {
private final HotspotType type;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final String displayText;
private final String action;
private final String tooltip;
public Hotspot(HotspotType type, int startRow, int startCol, int endRow, int endCol,
String displayText, String action, String tooltip) {
this.type = type;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.displayText = displayText;
this.action = action;
this.tooltip = tooltip;
}
public HotspotType getType() {
return type;
}
public int getStartRow() {
return startRow;
}
public int getStartCol() {
return startCol;
}
public int getEndRow() {
return endRow;
}
public int getEndCol() {
return endCol;
}
public String getDisplayText() {
return displayText;
}
public String getAction() {
return action;
}
public String getTooltip() {
return tooltip;
}
/**
* Test whether the given row/col coordinate falls within this hotspot.
*/
public boolean contains(int row, int col) {
if (row < startRow || row > endRow) {
return false;
}
if (startRow == endRow) {
return col >= startCol && col <= endCol;
}
if (row == startRow) {
return col >= startCol;
}
if (row == endRow) {
return col <= endCol;
}
return true; // Intermediate rows
}
@Override
public String toString() {
return "Hotspot{" + type + " [" + startRow + "," + startCol + "]-[" + endRow + "," + endCol + "] '" + displayText + "'}";
}
}
// Configurable scan enablement
private boolean urlEnabled = true;
private boolean pfKeyEnabled = true;
private boolean menuChoiceEnabled = true;
// Regular expression patterns
// 1. URLs
private static final Pattern URL_PATTERN = Pattern.compile(
"(?:https?|ftp|file)://[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|www\\.[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|mailto:[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+",
Pattern.CASE_INSENSITIVE
);
// 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
private static final Pattern PF_KEY_PATTERN = Pattern.compile(
"(?:(?<=\\W)|^)(PF(?:[1-9]|1[0-9]|2[0-4])|F(?:[1-9]|1[0-9]|2[0-4])|PA[1-3]|\\[ENTER\\]|ENTER)(?:(?=\\W)|$)",
Pattern.CASE_INSENSITIVE
);
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5"
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
);
private static final Pattern MENU_PROMPT_PATTERN = Pattern.compile(
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
);
// Caching state to avoid rescanning unchanged screens
private String cachedScreenText = "";
private int cachedRows = 0;
private int cachedCols = 0;
private List<Hotspot> cachedHotspots = Collections.emptyList();
public HotspotEngine() {
}
public boolean isUrlEnabled() {
return urlEnabled;
}
public void setUrlEnabled(boolean urlEnabled) {
this.urlEnabled = urlEnabled;
invalidateCache();
}
public boolean isPfKeyEnabled() {
return pfKeyEnabled;
}
public void setPfKeyEnabled(boolean pfKeyEnabled) {
this.pfKeyEnabled = pfKeyEnabled;
invalidateCache();
}
public boolean isMenuChoiceEnabled() {
return menuChoiceEnabled;
}
public void setMenuChoiceEnabled(boolean menuChoiceEnabled) {
this.menuChoiceEnabled = menuChoiceEnabled;
invalidateCache();
}
public void invalidateCache() {
cachedScreenText = "";
cachedHotspots = Collections.emptyList();
}
/**
* Scan the given ScreenBuffer for interactive hotspots.
*/
public synchronized List<Hotspot> scan(ScreenBuffer sb) {
if (sb == null) return Collections.emptyList();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
// Build plain text representation
StringBuilder fullText = new StringBuilder(rows * (cols + 1));
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getDisplayCell(baddr);
if (ea == null || ea.isFieldAttribute() || ea.ucs4 <= 0x20 || ea.ucs4 == 0xFF) {
fullText.append(' ');
} else {
fullText.append((char) ea.ucs4);
}
}
if (r < rows - 1) {
fullText.append('\n');
}
}
return scanText(fullText.toString(), rows, cols);
}
/**
* Scan formatted screen text (lines separated by '\n') for hotspots.
*/
public synchronized List<Hotspot> scanText(String screenText, int rows, int cols) {
if (screenText == null || rows <= 0 || cols <= 0) {
return Collections.emptyList();
}
if (screenText.equals(cachedScreenText) && rows == cachedRows && cols == cachedCols) {
return cachedHotspots;
}
List<Hotspot> hotspots = new ArrayList<>();
String[] lines = screenText.split("\n", -1);
int actualRows = Math.min(rows, lines.length);
// Scan line by line for precise row/column tracking
for (int r = 0; r < actualRows; r++) {
String line = lines[r];
boolean[] occupied = new boolean[line.length() + 1];
// 1. Scan URLs
if (urlEnabled) {
Matcher mUrl = URL_PATTERN.matcher(line);
while (mUrl.find()) {
int startCol = mUrl.start();
int endCol = mUrl.end();
String match = mUrl.group();
// Trim trailing punctuation like . , ; : ) ]
while (match.length() > 1) {
char last = match.charAt(match.length() - 1);
if (last == '.' || last == ',' || last == ';' || last == ':' || last == ')' || last == ']' || last == '>') {
match = match.substring(0, match.length() - 1);
endCol--;
} else {
break;
}
}
if (match.isEmpty()) continue;
String actionUrl = match;
if (match.toLowerCase().startsWith("www.")) {
actionUrl = "http://" + match;
}
Hotspot h = new Hotspot(
HotspotType.URL,
r, startCol, r, endCol - 1,
match, actionUrl, "Open URL: " + actionUrl
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
// 2. Scan PF / Function keys
if (pfKeyEnabled) {
Matcher mPf = PF_KEY_PATTERN.matcher(line);
while (mPf.find()) {
int startCol = mPf.start(1);
int endCol = mPf.end(1);
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String keyText = mPf.group(1).toUpperCase();
String action;
String tooltip;
if (keyText.equals("ENTER") || keyText.equals("[ENTER]")) {
action = "[enter]";
tooltip = "Send [ENTER]";
} else if (keyText.startsWith("PF")) {
String numStr = keyText.substring(2);
action = "[pf" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else if (keyText.startsWith("F")) {
String numStr = keyText.substring(1);
action = "[pf" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else if (keyText.startsWith("PA")) {
String numStr = keyText.substring(2);
action = "[pa" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else {
continue;
}
Hotspot h = new Hotspot(
keyText.contains("ENTER") ? HotspotType.ENTER : HotspotType.PF_KEY,
r, startCol, r, endCol - 1,
mPf.group(1), action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
// 3. Scan Menu choices ("1.", "2.", "===> 3", "=> 4")
if (menuChoiceEnabled) {
// Pattern 3a: Number followed by dot (e.g. " 1. ")
Matcher mNum = MENU_NUMBER_PATTERN.matcher(line);
while (mNum.find()) {
int startCol = mNum.start(1);
int endCol = mNum.end();
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String choiceNum = mNum.group(1);
String action = choiceNum + "[enter]";
String tooltip = "Select option " + choiceNum;
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
choiceNum + ".", action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
// Pattern 3b: Prompt followed by number (e.g. "===> 3")
Matcher mPrompt = MENU_PROMPT_PATTERN.matcher(line);
while (mPrompt.find()) {
int startCol = mPrompt.start();
int endCol = mPrompt.end();
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String choiceNum = mPrompt.group(1);
String action = choiceNum + "[enter]";
String tooltip = "Execute choice " + choiceNum;
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
mPrompt.group(), action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
}
this.cachedScreenText = screenText;
this.cachedRows = rows;
this.cachedCols = cols;
this.cachedHotspots = Collections.unmodifiableList(hotspots);
return cachedHotspots;
}
/**
* Hit-test for a hotspot at the specified terminal row and column.
*/
public Hotspot findHotspot(int row, int col) {
for (Hotspot h : cachedHotspots) {
if (h.contains(row, col)) {
return h;
}
}
return null;
}
/**
* Query currently active cached hotspots.
*/
public List<Hotspot> getHotspots() {
return cachedHotspots;
}
private static void markOccupied(boolean[] arr, int start, int end) {
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
arr[i] = true;
}
}
private static boolean isRangeOccupied(boolean[] arr, int start, int end) {
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
if (arr[i]) return true;
}
return false;
}
}
@@ -206,7 +206,8 @@ public class KeyBindings {
/** /**
* Parse a .kmp file from a Reader. * Parse a .kmp file from a Reader.
* Supports both INI-style KEY<n>=action formats and bind statements. * 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 * @param reader the reader to parse from
* @return Map of j3270 action names to key stroke bindings * @return Map of j3270 action names to key stroke bindings
@@ -214,11 +215,38 @@ public class KeyBindings {
*/ */
public static Map<String, String> parseKmp(Reader reader) throws IOException { public static Map<String, String> parseKmp(Reader reader) throws IOException {
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader); BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
Map<String, List<String>> actionToBindings = new LinkedHashMap<>(); List<String> rawLines = new ArrayList<>();
boolean hasHodKeyRemap = false;
boolean hasHodHashKey = false;
String line; String line;
while ((line = br.readLine()) != null) { while ((line = br.readLine()) != null) {
line = line.trim(); 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 // Strip comments
int commentIdx = line.indexOf(';'); int commentIdx = line.indexOf(';');
@@ -262,6 +290,22 @@ public class KeyBindings {
return result; 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) { private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
String[] parts = stmt.split("\\s+", 2); String[] parts = stmt.split("\\s+", 2);
if (parts.length < 2) return; if (parts.length < 2) return;
@@ -288,6 +332,19 @@ public class KeyBindings {
} }
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) { 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 // Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
String action = resolveMnemonic(right); String action = resolveMnemonic(right);
String keyStroke = null; String keyStroke = null;
@@ -0,0 +1,392 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLSession;
import haus.nightmare.lib3270j.macro.Macro;
import haus.nightmare.lib3270j.macro.MacroScreen;
import haus.nightmare.lib3270j.macro.MacroScreens;
import haus.nightmare.lib3270j.macro.MacroXmlParser;
import haus.nightmare.lib3270j.macro.action.MacroAction;
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/**
* Interactive Swing dialog for loading, editing, running, and debugging
* IBM Host On-Demand XML Macros (<HASCRIPT> / .mac files).
*/
public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextArea xmlArea;
private JTextArea logArea;
private JLabel statusLabel;
private JButton playBtn;
private JButton pauseBtn;
private JButton stopBtn;
private Macro macro;
public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "IBM HoD XML Macro Runtime (<HASCRIPT>)", false);
this.client = client;
this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
this.macro = new Macro(client);
this.macro.addMacroListener(this);
this.macro.setPromptHandler(this);
buildUI();
setSize(780, 580);
setLocationRelativeTo(parent);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
Font labelFont = ThemeManager.getUiFont();
// Top info label
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
topPanel.setOpaque(false);
JLabel descLabel = new JLabel("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code>&lt;HASCRIPT&gt;</code>).<br>"
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Center split: XML editor and Execution Log
xmlArea = new JTextArea();
xmlArea.setFont(ThemeManager.getMonospacedUiFont());
xmlArea.setLineWrap(false);
xmlArea.setText(getDefaultMacroXml());
ThemeManager.styleTextArea(xmlArea);
JScrollPane xmlScroll = new JScrollPane(xmlArea);
ThemeManager.styleScrollPane(xmlScroll);
xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML (<HASCRIPT>)"));
logArea = new JTextArea();
logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1));
logArea.setEditable(false);
logArea.setLineWrap(true);
ThemeManager.styleTextArea(logArea);
JScrollPane logScroll = new JScrollPane(logArea);
ThemeManager.styleScrollPane(logScroll);
logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log"));
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll);
splitPane.setResizeWeight(0.65);
splitPane.setOpaque(false);
mainPanel.add(splitPane, BorderLayout.CENTER);
// Bottom control panel
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
bottomPanel.setOpaque(false);
statusLabel = new JLabel("Status: Stopped");
statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load .mac...");
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadMacroFile());
JButton saveBtn = new JButton("Save .mac...");
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveMacroFile());
playBtn = new JButton("Play");
ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY);
playBtn.setFont(labelFont);
playBtn.addActionListener(e -> playMacro());
pauseBtn = new JButton("Pause");
ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT);
pauseBtn.setFont(labelFont);
pauseBtn.setEnabled(false);
pauseBtn.addActionListener(e -> togglePause());
stopBtn = new JButton("Stop");
ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT);
stopBtn.setFont(labelFont);
stopBtn.setEnabled(false);
stopBtn.addActionListener(e -> stopMacro());
JButton clearLogBtn = new JButton("Clear Log");
ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT);
clearLogBtn.setFont(labelFont);
clearLogBtn.addActionListener(e -> logArea.setText(""));
JButton closeBtn = new JButton("Close");
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
buttonPanel.add(loadBtn);
buttonPanel.add(saveBtn);
buttonPanel.add(Box.createHorizontalStrut(8));
buttonPanel.add(playBtn);
buttonPanel.add(pauseBtn);
buttonPanel.add(stopBtn);
buttonPanel.add(clearLogBtn);
buttonPanel.add(Box.createHorizontalStrut(8));
buttonPanel.add(closeBtn);
bottomPanel.add(buttonPanel, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
}
private void loadMacroFile() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Open IBM HoD Macro");
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
sb.append(line).append("\n");
}
xmlArea.setText(sb.toString());
log("Loaded macro file: " + f.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void saveMacroFile() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Save IBM HoD Macro");
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
if (!f.getName().contains(".")) {
f = new File(f.getParentFile(), f.getName() + ".mac");
}
try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) {
w.write(xmlArea.getText());
log("Saved macro file: " + f.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
public void playMacro() {
String xml = xmlArea.getText();
if (xml == null || xml.trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE);
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
int opt = JOptionPane.showConfirmDialog(this,
"Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?",
"Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (opt != JOptionPane.YES_OPTION) {
return;
}
}
try {
MacroScreens screens = MacroXmlParser.parse(xml);
macro.setScreens(screens);
macro.setClient(client);
log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName()));
macro.play();
} catch (Exception ex) {
log("Parse / Launch Error: " + ex.getMessage());
JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE);
}
}
public void togglePause() {
if (macro.getState() == Macro.STATE_PLAYING) {
macro.pause();
pauseBtn.setText("Resume");
log("Macro paused.");
} else if (macro.getState() == Macro.STATE_PAUSED) {
macro.resume();
pauseBtn.setText("Pause");
log("Macro resumed.");
}
}
public void stopMacro() {
macro.stop();
log("Macro stopped by user.");
}
private void log(String msg) {
SwingUtilities.invokeLater(() -> {
logArea.append(msg + "\n");
logArea.setCaretPosition(logArea.getDocument().getLength());
});
}
// ========== MacroListener Callbacks ==========
@Override
public void macroStateChanged(Macro macro, int oldState, int newState) {
SwingUtilities.invokeLater(() -> {
switch (newState) {
case Macro.STATE_PLAYING:
statusLabel.setText("Status: Playing");
statusLabel.setForeground(ThemeManager.getFgMain());
playBtn.setEnabled(false);
pauseBtn.setEnabled(true);
pauseBtn.setText("Pause");
stopBtn.setEnabled(true);
break;
case Macro.STATE_PAUSED:
statusLabel.setText("Status: Paused");
statusLabel.setForeground(ThemeManager.getFgMuted());
pauseBtn.setText("Resume");
break;
case Macro.STATE_ERROR:
statusLabel.setText("Status: Error");
statusLabel.setForeground(Color.RED);
playBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
break;
case Macro.STATE_STOPPED:
default:
statusLabel.setText("Status: Stopped");
statusLabel.setForeground(ThemeManager.getFgMuted());
playBtn.setEnabled(true);
pauseBtn.setEnabled(false);
pauseBtn.setText("Pause");
stopBtn.setEnabled(false);
break;
}
});
}
@Override
public void macroScreenMatching(MacroScreen screen) {
log("Matched screen: [" + screen.getName() + "]");
if (terminalPanel != null) {
terminalPanel.repaint();
}
}
@Override
public void macroActionExecuting(MacroAction action) {
log(" -> Executing: " + action.getClass().getSimpleName());
if (terminalPanel != null) {
terminalPanel.repaint();
}
}
@Override
public void macroMessage(String title, String message) {
log("Message [" + title + "]: " + message);
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE);
});
}
@Override
public void macroError(String error) {
log("ERROR: " + error);
}
// ========== MacroPromptHandler Callback ==========
@Override
public String prompt(MacroActionPrompt prompt) {
AtomicReference<String> result = new AtomicReference<>(null);
if (SwingUtilities.isEventDispatchThread()) {
result.set(showPromptModal(prompt));
} else {
try {
SwingUtilities.invokeAndWait(() -> {
result.set(showPromptModal(prompt));
});
} catch (Exception e) {
return null;
}
}
return result.get();
}
private String showPromptModal(MacroActionPrompt prompt) {
JPanel panel = new JPanel(new BorderLayout(5, 5));
JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription());
panel.add(msgLabel, BorderLayout.NORTH);
JTextField textField;
if (prompt.isEncrypted()) {
textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
} else {
textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
}
panel.add(textField, BorderLayout.CENTER);
int opt = JOptionPane.showConfirmDialog(this, panel,
prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(),
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
if (opt == JOptionPane.OK_OPTION) {
if (textField instanceof JPasswordField) {
return new String(((JPasswordField) textField).getPassword());
}
return textField.getText();
}
return null;
}
private String getDefaultMacroXml() {
return "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
+ " <vars>\n"
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
+ " </vars>\n\n"
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
+ " </actions>\n"
+ " <nextscreens timeout=\"10000\">\n"
+ " <nextscreen name=\"ReadyScreen\" />\n"
+ " </nextscreens>\n"
+ " </screen>\n\n"
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
+ " </actions>\n"
+ " </screen>\n"
+ "</HASCRIPT>";
}
}
@@ -48,6 +48,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) { public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
super(parent, "IBM 3287 Printer Session Manager", false); super(parent, "IBM 3287 Printer Session Manager", false);
this.parent = parent; this.parent = parent;
AppIcon.applyTo(this);
config = new PrinterConfig(defaultHost, defaultPort); config = new PrinterConfig(defaultHost, defaultPort);
config.setUseTls(defaultTls); config.setUseTls(defaultTls);
@@ -71,7 +72,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
gbc.insets = new Insets(3, 4, 3, 4); gbc.insets = new Insets(3, 4, 3, 4);
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); Font labelFont = ThemeManager.getUiFont();
// Host / Port // Host / Port
gbc.gridx = 0; gbc.gridy = 0; gbc.gridx = 0; gbc.gridy = 0;
@@ -205,7 +206,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
// Spool text area // Spool text area
spoolArea = new JTextArea(); spoolArea = new JTextArea();
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); spoolArea.setFont(ThemeManager.getMonospacedUiFont());
spoolArea.setEditable(false); spoolArea.setEditable(false);
ThemeManager.styleTextArea(spoolArea); ThemeManager.styleTextArea(spoolArea);
@@ -256,7 +257,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
private JTextField createField(String text, int cols) { private JTextField createField(String text, int cols) {
JTextField tf = new JTextField(text, cols); JTextField tf = new JTextField(text, cols);
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); tf.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(tf); ThemeManager.styleTextField(tf);
return tf; return tf;
} }
@@ -0,0 +1,323 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenHistoryListener;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenSnapshot;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Screen History viewer dialog matching IBM Host On-Demand ScreenHistory UI parity.
*
* Provides:
* - Interactive JSlider scrubber to review past 64 screen buffer frames
* - Previous / Next navigation buttons
* - Exact monospace terminal frame preview with selection capability
* - Copy Screen, Copy Selection, and Copy as Table actions
* - Live update synchronization as new host screens arrive
*/
public class ScreenHistoryDialog extends JDialog implements ScreenHistoryListener {
private final J3270App parentApp;
private final Telnet3270Client client;
private final ScreenHistoryBuffer historyBuffer;
private JSlider scrubSlider;
private JButton btnPrev;
private JButton btnNext;
private JLabel infoLabel;
private JTextArea previewArea;
private JButton btnCopyScreen;
private JButton btnCopySelection;
private JButton btnCopyTable;
private JButton btnClose;
private int currentIndex = -1; // 0-based index into historyBuffer
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public ScreenHistoryDialog(J3270App parent, Telnet3270Client client) {
super(parent, "Screen History", false);
this.parentApp = parent;
this.client = client;
this.historyBuffer = client != null ? client.getScreenHistoryBuffer() : null;
AppIcon.applyTo(this);
initComponents();
setupKeyNavigation();
if (historyBuffer != null) {
historyBuffer.addListener(this);
refreshHistoryView(historyBuffer.size() - 1);
} else {
updateEmptyState();
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
if (historyBuffer != null) {
historyBuffer.removeListener(ScreenHistoryDialog.this);
}
}
});
setSize(780, 560);
setLocationRelativeTo(parent);
}
private void initComponents() {
setLayout(new BorderLayout(6, 6));
JPanel contentPane = new JPanel(new BorderLayout(8, 8));
contentPane.setBorder(new EmptyBorder(10, 10, 10, 10));
setContentPane(contentPane);
// Top Navigation Bar
JPanel topPanel = new JPanel(new BorderLayout(8, 8));
topPanel.setBorder(BorderFactory.createTitledBorder("History Scrub Navigation"));
JPanel sliderPanel = new JPanel(new BorderLayout(6, 6));
btnPrev = new JButton("◄ Prev");
ThemeManager.styleButton(btnPrev, ThemeManager.ButtonVariant.DEFAULT);
btnPrev.addActionListener(e -> stepHistory(-1));
btnNext = new JButton("Next ►");
ThemeManager.styleButton(btnNext, ThemeManager.ButtonVariant.DEFAULT);
btnNext.addActionListener(e -> stepHistory(1));
scrubSlider = new JSlider(1, 1, 1);
scrubSlider.setMajorTickSpacing(10);
scrubSlider.setMinorTickSpacing(1);
scrubSlider.setPaintTicks(true);
scrubSlider.setSnapToTicks(true);
scrubSlider.addChangeListener(e -> {
if (!scrubSlider.getValueIsAdjusting()) {
int selected = scrubSlider.getValue() - 1;
displayScreen(selected);
}
});
sliderPanel.add(btnPrev, BorderLayout.WEST);
sliderPanel.add(scrubSlider, BorderLayout.CENTER);
sliderPanel.add(btnNext, BorderLayout.EAST);
infoLabel = new JLabel("Screen 0 of 0", SwingConstants.CENTER);
infoLabel.setFont(infoLabel.getFont().deriveFont(Font.BOLD, 13f));
topPanel.add(sliderPanel, BorderLayout.CENTER);
topPanel.add(infoLabel, BorderLayout.SOUTH);
contentPane.add(topPanel, BorderLayout.NORTH);
// Center Terminal Preview Pane
previewArea = new JTextArea();
previewArea.setEditable(false);
previewArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
previewArea.setBackground(Color.BLACK);
previewArea.setForeground(new Color(0, 255, 65)); // 3270 Green Phosphor
previewArea.setCaretColor(Color.WHITE);
previewArea.setLineWrap(false);
previewArea.setMargin(new Insets(6, 6, 6, 6));
JScrollPane scrollPane = new JScrollPane(previewArea);
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
contentPane.add(scrollPane, BorderLayout.CENTER);
// Bottom Action Bar
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
btnCopyScreen = new JButton("Copy Screen");
ThemeManager.styleButton(btnCopyScreen, ThemeManager.ButtonVariant.DEFAULT);
btnCopyScreen.setToolTipText("Copy entire historical screen to clipboard");
btnCopyScreen.addActionListener(e -> copyCurrentScreen());
btnCopySelection = new JButton("Copy Selection");
ThemeManager.styleButton(btnCopySelection, ThemeManager.ButtonVariant.DEFAULT);
btnCopySelection.setToolTipText("Copy selected text to clipboard");
btnCopySelection.addActionListener(e -> copySelectedText());
btnCopyTable = new JButton("Copy as Table");
ThemeManager.styleButton(btnCopyTable, ThemeManager.ButtonVariant.DEFAULT);
btnCopyTable.setToolTipText("Copy screen text with trimmed trailing spaces per row");
btnCopyTable.addActionListener(e -> copyAsTable());
btnClose = new JButton("Close");
ThemeManager.styleButton(btnClose, ThemeManager.ButtonVariant.DEFAULT);
btnClose.addActionListener(e -> setVisible(false));
bottomPanel.add(btnCopyScreen);
bottomPanel.add(btnCopySelection);
bottomPanel.add(btnCopyTable);
bottomPanel.add(Box.createHorizontalStrut(12));
bottomPanel.add(btnClose);
contentPane.add(bottomPanel, BorderLayout.SOUTH);
}
private void setupKeyNavigation() {
// Keyboard shortcuts for rapid scrub navigation
Action prevAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { stepHistory(-1); }
};
Action nextAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { stepHistory(1); }
};
Action homeAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { displayScreen(0); }
};
Action endAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) {
if (historyBuffer != null) displayScreen(historyBuffer.size() - 1);
}
};
Action closeAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { setVisible(false); }
};
JRootPane root = getRootPane();
InputMap im = root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap am = root.getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK), "prevScreen");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "prevScreen");
am.put("prevScreen", prevAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK), "nextScreen");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "nextScreen");
am.put("nextScreen", nextAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.ALT_DOWN_MASK), "homeScreen");
am.put("homeScreen", homeAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.ALT_DOWN_MASK), "endScreen");
am.put("endScreen", endAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeDialog");
am.put("closeDialog", closeAction);
}
private void stepHistory(int delta) {
if (historyBuffer == null || historyBuffer.isEmpty()) return;
int target = currentIndex + delta;
target = Math.max(0, Math.min(target, historyBuffer.size() - 1));
displayScreen(target);
}
private void displayScreen(int index) {
if (historyBuffer == null || historyBuffer.isEmpty()) {
updateEmptyState();
return;
}
int total = historyBuffer.size();
index = Math.max(0, Math.min(index, total - 1));
currentIndex = index;
ScreenSnapshot snap = historyBuffer.get(index);
if (snap != null) {
previewArea.setText(snap.getText());
previewArea.setCaretPosition(0);
String timeStr = dateFormat.format(new Date(snap.getTimestamp()));
infoLabel.setText("Screen " + (index + 1) + " of " + total + " (" + timeStr + ") — "
+ snap.getRows() + "x" + snap.getCols() + ", Cursor: ["
+ (snap.getCursorRow() + 1) + "," + (snap.getCursorCol() + 1) + "]");
scrubSlider.setValue(index + 1);
}
btnPrev.setEnabled(index > 0);
btnNext.setEnabled(index < total - 1);
btnCopyScreen.setEnabled(true);
btnCopySelection.setEnabled(true);
btnCopyTable.setEnabled(true);
}
private void updateEmptyState() {
currentIndex = -1;
previewArea.setText("No screen history captured yet.\n\nScreens are automatically recorded as the host session receives updates or unlocks the keyboard.");
infoLabel.setText("Screen 0 of 0 — Empty History");
scrubSlider.setEnabled(false);
btnPrev.setEnabled(false);
btnNext.setEnabled(false);
btnCopyScreen.setEnabled(false);
btnCopySelection.setEnabled(false);
btnCopyTable.setEnabled(false);
}
/**
* Refresh slider bounds and select target index.
*/
public synchronized void refreshHistoryView(int targetIndex) {
if (historyBuffer == null || historyBuffer.isEmpty()) {
updateEmptyState();
return;
}
int total = historyBuffer.size();
scrubSlider.setEnabled(true);
scrubSlider.setMinimum(1);
scrubSlider.setMaximum(total);
int select = targetIndex >= 0 ? Math.min(targetIndex, total - 1) : total - 1;
displayScreen(select);
}
@Override
public void onHistoryUpdated(ScreenHistoryBuffer buffer, ScreenSnapshot newSnapshot) {
SwingUtilities.invokeLater(() -> {
boolean wasAtLatest = (currentIndex >= 0 && currentIndex == buffer.size() - 2);
int newTotal = buffer.size();
scrubSlider.setMaximum(newTotal);
if (wasAtLatest || currentIndex < 0) {
// Keep tracked with latest frame
displayScreen(newTotal - 1);
} else {
// Keep operator's active scrub position, update label
displayScreen(currentIndex);
}
});
}
private void copyCurrentScreen() {
if (historyBuffer == null || currentIndex < 0) return;
ScreenSnapshot snap = historyBuffer.get(currentIndex);
if (snap != null) {
setClipboardText(snap.getText());
}
}
private void copySelectedText() {
String selected = previewArea.getSelectedText();
if (selected != null && !selected.isEmpty()) {
setClipboardText(selected);
} else {
copyCurrentScreen();
}
}
private void copyAsTable() {
if (historyBuffer == null || currentIndex < 0) return;
ScreenSnapshot snap = historyBuffer.get(currentIndex);
if (snap != null) {
setClipboardText(snap.getFormattedTable());
}
}
private void setClipboardText(String text) {
try {
Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
cb.setContents(new StringSelection(text), null);
} catch (Exception e) {
Toolkit.getDefaultToolkit().beep();
}
}
}
@@ -25,6 +25,7 @@ public class ScriptDialog extends JDialog {
super(parent, "Run Keystrokes / ECL Script", false); super(parent, "Run Keystrokes / ECL Script", false);
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
buildUI(); buildUI();
setSize(600, 480); setSize(600, 480);
@@ -35,7 +36,7 @@ public class ScriptDialog extends JDialog {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); Font labelFont = ThemeManager.getUiFont();
// Header // Header
JPanel topPanel = new JPanel(new BorderLayout(5, 5)); JPanel topPanel = new JPanel(new BorderLayout(5, 5));
@@ -48,7 +49,7 @@ public class ScriptDialog extends JDialog {
// Script area // Script area
scriptArea = new JTextArea(); scriptArea = new JTextArea();
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); scriptArea.setFont(ThemeManager.getMonospacedUiFont());
scriptArea.setLineWrap(true); scriptArea.setLineWrap(true);
scriptArea.setWrapStyleWord(false); scriptArea.setWrapStyleWord(false);
ThemeManager.styleTextArea(scriptArea); ThemeManager.styleTextArea(scriptArea);
@@ -74,7 +75,7 @@ public class ScriptDialog extends JDialog {
for (String token : tokens) { for (String token : tokens) {
JButton btn = new JButton(token); JButton btn = new JButton(token);
btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11)); btn.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -2));
ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT);
btn.setFocusable(false); btn.setFocusable(false);
btn.setMargin(new Insets(2, 4, 2, 4)); btn.setMargin(new Insets(2, 4, 2, 4));
@@ -94,7 +95,7 @@ public class ScriptDialog extends JDialog {
statusLabel = new JLabel("Ready"); statusLabel = new JLabel("Ready");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
File diff suppressed because it is too large Load Diff
@@ -29,14 +29,15 @@ public class StatusBar extends JPanel {
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel; private TerminalPanel terminalPanel;
private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener;
public StatusBar() { public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(ThemeManager.getStatusBarBg()); setBackground(ThemeManager.getStatusBarBg());
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder())); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim()); connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
@@ -131,8 +132,15 @@ public class StatusBar extends JPanel {
} }
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) { public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
if (this.client != null && this.client.getOIA() != null && this.oiaNotifyListener != null) {
this.client.getOIA().unregisterOIAEvent(this.oiaNotifyListener);
}
this.client = client; this.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
if (this.client != null && this.client.getOIA() != null) {
this.oiaNotifyListener = event -> updateStatus();
this.client.getOIA().registerOIAEvent(this.oiaNotifyListener);
}
updateStatus(); updateStatus();
} }
@@ -143,9 +151,36 @@ public class StatusBar extends JPanel {
} }
setBackground(ThemeManager.getStatusBarBg(theme)); setBackground(ThemeManager.getStatusBarBg(theme));
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme))); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
Font oiaFont = ThemeManager.getMonospacedUiFont();
updateBarHeight(oiaFont);
applyFontRecursively(this, oiaFont);
revalidate();
repaint();
updateStatus(); updateStatus();
} }
private void updateBarHeight(Font font) {
int fontHeight = 16;
try {
FontMetrics fm = getFontMetrics(font);
if (fm != null) fontHeight = fm.getHeight();
} catch (Exception ignored) {
fontHeight = font.getSize() + 4;
}
int barHeight = Math.max(22, fontHeight + 6);
setPreferredSize(new Dimension(800, barHeight));
}
private void applyFontRecursively(Component comp, Font font) {
if (comp instanceof JLabel) {
comp.setFont(font);
} else if (comp instanceof Container) {
for (Component child : ((Container) comp).getComponents()) {
applyFontRecursively(child, font);
}
}
}
public void updateStatus() { public void updateStatus() {
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) { if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(this::updateStatus); SwingUtilities.invokeLater(this::updateStatus);
@@ -361,4 +396,8 @@ public class StatusBar extends JPanel {
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr)); cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme)); cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
} }
public String getLockStatusText() {
return lockStatus != null ? lockStatus.getText() : "";
}
} }
@@ -25,8 +25,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
// Font and cell dimensions // Font and cell dimensions
private Font terminalFont; private Font terminalFont;
private Font boldTerminalFont; private Font boldTerminalFont;
private int cellWidth; int cellWidth;
private int cellHeight; int cellHeight;
private int fontAscent; private int fontAscent;
private int fontDescent; private int fontDescent;
private int padding = 4; private int padding = 4;
@@ -65,6 +65,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180); private static final Color CURSOR_COLOR = new Color(255, 255, 255, 180);
private boolean crosshairRulerEnabled = false; private boolean crosshairRulerEnabled = false;
private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC) private static final Color CROSSHAIR_RULER_COLOR = new Color(0, 255, 0, 102); // 40% alpha (cRC)
private boolean inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
private String inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
private boolean textBlinkVisible = true; private boolean textBlinkVisible = true;
private Image wallpaperImage = null; private Image wallpaperImage = null;
private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null; private haus.nightmare.lib3270j.graphics.HODWallpaper hodWallpaper = null;
@@ -72,8 +74,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private int selectionStartRow = -1, selectionStartCol = -1; private int selectionStartRow = -1, selectionStartCol = -1;
private int selectionEndRow = -1, selectionEndCol = -1; private int selectionEndRow = -1, selectionEndCol = -1;
private boolean isDragging = false; private boolean isDragging = false;
private boolean isActivationClick = false;
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend
public enum DragHandle {
NONE, NW, N, NE, E, SE, S, SW, W, MOVE
}
private boolean trimRectSizingHandles = false;
private DragHandle activeDragHandle = DragHandle.NONE;
private int dragAnchorRow = -1;
private int dragAnchorCol = -1;
private int initialBoxR1 = -1, initialBoxR2 = -1, initialBoxC1 = -1, initialBoxC2 = -1;
// ========== Search Highlight state ========== // ========== Search Highlight state ==========
private int searchHighlightAddr = -1; private int searchHighlightAddr = -1;
private int searchHighlightLen = 0; private int searchHighlightLen = 0;
@@ -86,6 +98,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private boolean resizeGuard = false; private boolean resizeGuard = false;
private StatusBar statusBar = null; private StatusBar statusBar = null;
// ========== Phase 25: Hotspots & Mouse Wheel ==========
private final HotspotEngine hotspotEngine = new HotspotEngine();
private HotspotEngine.Hotspot hoveredHotspot = null;
private boolean hotspotsEnabled = haus.nightmare.j3270.config.Settings.getHotspotsEnabled();
private boolean mouseWheelEnabled = haus.nightmare.j3270.config.Settings.getMouseWheelEnabled();
private String mouseWheelUpAction = haus.nightmare.j3270.config.Settings.getMouseWheelUpKey();
private String mouseWheelDownAction = haus.nightmare.j3270.config.Settings.getMouseWheelDownKey();
// ========== Mode and Listener State ========== // ========== Mode and Listener State ==========
private boolean insertMode = false; private boolean insertMode = false;
@@ -115,7 +135,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
/** /**
* Compute the horizontal render offset to center the grid within the panel. * Compute the horizontal render offset to center the grid within the panel.
*/ */
private int getRenderOffsetX() { int getRenderOffsetX() {
int termCols = 80; int termCols = 80;
if (client != null) termCols = client.getScreenBuffer().getDisplayCols(); if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
int gridW = termCols * cellWidth; int gridW = termCols * cellWidth;
@@ -126,7 +146,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
/** /**
* Compute the vertical render offset to center the grid within the panel. * Compute the vertical render offset to center the grid within the panel.
*/ */
private int getRenderOffsetY() { int getRenderOffsetY() {
int termRows = 24; int termRows = 24;
if (client != null) termRows = client.getScreenBuffer().getDisplayRows(); if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
int gridH = termRows * cellHeight; int gridH = termRows * cellHeight;
@@ -176,6 +196,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
setupCursorBlink(); setupCursorBlink();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode(); blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
trimRectSizingHandles = haus.nightmare.j3270.config.Settings.getTrimRectSizingHandles();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler(); crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle(); String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK; cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
@@ -184,7 +205,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
MouseAdapter mouseHandler = new MouseAdapter() { MouseAdapter mouseHandler = new MouseAdapter() {
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
requestFocusInWindow(); Window win = SwingUtilities.getWindowAncestor(TerminalPanel.this);
boolean wasWindowInactive = (win != null && (!win.isActive() || !win.isFocused()));
boolean wasPanelUnfocused = !hasFocus();
isActivationClick = wasWindowInactive || wasPanelUnfocused;
if (wasWindowInactive) {
win.toFront();
win.requestFocus();
}
requestFocus();
// Right-click context menu // Right-click context menu
if (SwingUtilities.isRightMouseButton(e)) { if (SwingUtilities.isRightMouseButton(e)) {
@@ -204,18 +234,30 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
row = Math.max(0, Math.min(row, displayRows - 1)); row = Math.max(0, Math.min(row, displayRows - 1));
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive(); boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
boolean isSelectableField = false; if (isGraphic || lightPenMode) {
if (sb.isFormatted()) {
int faPos = sb.findFieldAttribute(row * displayCols + col);
if (faPos >= 0) {
int fa = sb.getCell(faPos).fa & 0xFF;
isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa);
}
}
if (isGraphic || lightPenMode || isSelectableField) {
clearSelection(); clearSelection();
} else if (SwingUtilities.isLeftMouseButton(e) && trimRectSizingHandles && hasSelection()) {
DragHandle handle = getDragHandleAt(e.getX(), e.getY());
if (handle != DragHandle.NONE) {
activeDragHandle = handle;
isDragging = true;
dragAnchorRow = row;
dragAnchorCol = col;
initialBoxR1 = Math.min(selectionStartRow, selectionEndRow);
initialBoxR2 = Math.max(selectionStartRow, selectionEndRow);
initialBoxC1 = Math.min(selectionStartCol, selectionEndCol);
initialBoxC2 = Math.max(selectionStartCol, selectionEndCol);
repaint();
return;
} else { } else {
activeDragHandle = DragHandle.NONE;
selectionStartRow = row;
selectionStartCol = col;
selectionEndRow = row;
selectionEndCol = col;
}
} else {
activeDragHandle = DragHandle.NONE;
selectionStartRow = row; selectionStartRow = row;
selectionStartCol = col; selectionStartCol = col;
selectionEndRow = row; selectionEndRow = row;
@@ -249,9 +291,44 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
col = Math.max(0, Math.min(col, sb.getDisplayCols() - 1)); col = Math.max(0, Math.min(col, sb.getDisplayCols() - 1));
row = Math.max(0, Math.min(row, sb.getDisplayRows() - 1)); row = Math.max(0, Math.min(row, sb.getDisplayRows() - 1));
if (activeDragHandle != DragHandle.NONE) {
if (activeDragHandle == DragHandle.MOVE) {
int deltaR = row - dragAnchorRow;
int deltaC = col - dragAnchorCol;
int h = initialBoxR2 - initialBoxR1;
int w = initialBoxC2 - initialBoxC1;
int newR1 = Math.max(0, Math.min(initialBoxR1 + deltaR, sb.getDisplayRows() - 1 - h));
int newC1 = Math.max(0, Math.min(initialBoxC1 + deltaC, sb.getDisplayCols() - 1 - w));
selectionStartRow = newR1;
selectionStartCol = newC1;
selectionEndRow = newR1 + h;
selectionEndCol = newC1 + w;
} else {
int newR1 = initialBoxR1;
int newR2 = initialBoxR2;
int newC1 = initialBoxC1;
int newC2 = initialBoxC2;
switch (activeDragHandle) {
case NW: newR1 = row; newC1 = col; break;
case N: newR1 = row; break;
case NE: newR1 = row; newC2 = col; break;
case E: newC2 = col; break;
case SE: newR2 = row; newC2 = col; break;
case S: newR2 = row; break;
case SW: newR2 = row; newC1 = col; break;
case W: newC1 = col; break;
default: break;
}
selectionStartRow = Math.min(newR1, newR2);
selectionEndRow = Math.max(newR1, newR2);
selectionStartCol = Math.min(newC1, newC2);
selectionEndCol = Math.max(newC1, newC2);
}
} else {
selectionEndRow = row; selectionEndRow = row;
selectionEndCol = col; selectionEndCol = col;
} }
}
repaint(); repaint();
} }
} }
@@ -272,12 +349,72 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
py = Math.max(0, Math.min(py, gHeight - 1)); py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py); client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
} }
// Phase 26.1: Drag handle cursor hover detection
if (trimRectSizingHandles && hasSelection() && client != null && client.getConnectionState().isFullSession()) {
DragHandle handle = getDragHandleAt(e.getX(), e.getY());
if (handle != DragHandle.NONE) {
setCursor(getCursorForDragHandle(handle));
setToolTipText(null);
return;
}
}
// Phase 25.2: Real-time hotspot hover detection
if (hotspotsEnabled && client != null && client.getScreenBuffer() != null) {
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
int col = (e.getX() - ox) / cellWidth;
int row = (e.getY() - oy) / cellHeight;
if (row >= 0 && row < rows && col >= 0 && col < cols) {
hotspotEngine.scan(sb);
HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col);
if (h != null) {
if (hoveredHotspot != h) {
hoveredHotspot = h;
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
setToolTipText(h.getTooltip());
repaint();
}
return;
}
}
}
if (hoveredHotspot != null) {
hoveredHotspot = null;
setCursor(Cursor.getDefaultCursor());
setToolTipText(null);
repaint();
} else if (activeDragHandle == DragHandle.NONE) {
setCursor(Cursor.getDefaultCursor());
}
}
@Override
public void mouseExited(MouseEvent e) {
if (hoveredHotspot != null) {
hoveredHotspot = null;
setCursor(Cursor.getDefaultCursor());
setToolTipText(null);
repaint();
} else if (activeDragHandle == DragHandle.NONE) {
setCursor(Cursor.getDefaultCursor());
}
} }
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (isDragging) { if (isDragging) {
isDragging = false; isDragging = false;
if (activeDragHandle != DragHandle.NONE) {
activeDragHandle = DragHandle.NONE;
isActivationClick = false;
repaint();
return;
}
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer(); ScreenBuffer sb = client.getScreenBuffer();
int ox = getRenderOffsetX(); int ox = getRenderOffsetX();
@@ -290,7 +427,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
row = Math.max(0, Math.min(row, displayRows - 1)); row = Math.max(0, Math.min(row, displayRows - 1));
int clickAddr = row * displayCols + col; int clickAddr = row * displayCols + col;
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null; boolean isGraphic = client.getGocaDecoder() != null &&
(client.getGocaDecoder().isGraphicsCursorActive() ||
(client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()));
if (isGraphic) { if (isGraphic) {
clearSelection(); clearSelection();
@@ -305,8 +444,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
py = Math.max(0, Math.min(py, gHeight - 1)); py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py); client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
int gx = client.getGocaDecoder().getGraphicCursorX();
int gy = client.getGocaDecoder().getGraphicCursorY();
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1; int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
client.getInputProcessor().sendGraphicMouseAid( client.getInputProcessor().sendGraphicMouseAid(
gx, gy,
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER, haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
button, button,
e.isShiftDown(), e.isShiftDown(),
@@ -316,14 +458,65 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
return; return;
} }
// Automatic Light-Pen / Selectable Field detection on mouse click: boolean wasDrag = (selectionStartRow != -1 && selectionEndRow != -1 &&
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol));
boolean activation = isActivationClick;
isActivationClick = false;
if (wasDrag) {
// Preserve drag/block selection so user can interact with menus or press Ctrl+C
repaint();
return;
}
// Single click (not a drag):
clearSelection();
sb.setCursorAddress(clickAddr);
// If this was an activation click (e.g. clicking to resume focus),
// do not fire field actions or send AIDs
if (activation) {
refreshScreen();
return;
}
// Phase 25.2: Interactive Hotspot click execution (URLs, PF keys, choices)
if (hotspotsEnabled && client.getConnectionState().isFullSession()) {
HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col);
if (h != null) {
if (h.getType() == HotspotEngine.HotspotType.URL) {
openUrl(h.getAction());
return;
} else {
client.getPS().SendKeys(h.getAction());
refreshScreen();
return;
}
}
}
// Automatic Light-Pen / Selectable Field detection on mouse click (HoD parity):
if (sb.isFormatted()) { if (sb.isFormatted()) {
int faPos = sb.findFieldAttribute(clickAddr); int faPos = sb.findFieldAttribute(clickAddr);
if (faPos >= 0) { if (faPos >= 0) {
int fa = sb.getCell(faPos).fa & 0xFF; int fa = sb.getCell(faPos).fa & 0xFF;
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) && if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) { !haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
clearSelection(); int size = displayRows * displayCols;
int desPos = (faPos + 1) % size;
char desAscii = (char) sb.getCell(desPos).ucs4;
int desEbcdic = sb.getCell(desPos).ec & 0xFF;
boolean isToggleField = (desEbcdic == 0x6F || desAscii == '?' || desAscii == '\uFF1F' ||
desEbcdic == 0x6E || desAscii == '>' || desAscii == '\uFF1E');
boolean isEnterField = (desEbcdic == 0x50 || desAscii == '&' || desAscii == '\uFF06');
boolean isProtected = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa);
boolean isDoubleClick = (e.getClickCount() >= 2);
// For detectable fields (FA bits 4-5 = 01 or 11):
// Double-clicking or invoking mnemonic [cursel] toggles the designator character (? -> >).
// If field has immediate select attribute, immediately transmit Cursor Select AID (0x7E).
if (lightPenMode || isDoubleClick || isToggleField || isEnterField || !isProtected) {
if (client.lightPenSelect(clickAddr)) { if (client.lightPenSelect(clickAddr)) {
refreshScreen(); refreshScreen();
return; return;
@@ -331,10 +524,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
} }
}
if (lightPenMode) { if (lightPenMode) {
clearSelection();
sb.setCursorAddress(clickAddr);
boolean result = client.lightPenSelect(clickAddr); boolean result = client.lightPenSelect(clickAddr);
if (result) { if (result) {
refreshScreen(); refreshScreen();
@@ -344,13 +536,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
sb.setCursorAddress(clickAddr);
clearSelection();
refreshScreen(); refreshScreen();
return; return;
} }
}
repaint(); repaint();
} }
} }
@@ -358,6 +546,21 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
addMouseListener(mouseHandler); addMouseListener(mouseHandler);
addMouseMotionListener(mouseHandler); addMouseMotionListener(mouseHandler);
// Phase 25.2: Mouse wheel navigation (Wheel Up -> [pf7], Wheel Down -> [pf8])
addMouseWheelListener(e -> {
if (!mouseWheelEnabled || client == null || !client.getConnectionState().isFullSession()) {
return;
}
int rotation = e.getWheelRotation();
if (rotation < 0) {
client.getPS().SendKeys(mouseWheelUpAction);
refreshScreen();
} else if (rotation > 0) {
client.getPS().SendKeys(mouseWheelDownAction);
refreshScreen();
}
});
// Handle resize: keep user configured font persistent without auto-fit reset // Handle resize: keep user configured font persistent without auto-fit reset
addComponentListener(new ComponentAdapter() { addComponentListener(new ComponentAdapter() {
@Override @Override
@@ -368,8 +571,104 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}); });
} }
// ========== Phase 25: Hotspot URL Opener ==========
private void openUrl(String url) {
if (url == null || url.trim().isEmpty()) return;
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(new java.net.URI(url));
return;
}
} catch (Exception ignored) {
}
// Fallback: command line launcher
try {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
Runtime.getRuntime().exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", url});
} else if (os.contains("mac")) {
Runtime.getRuntime().exec(new String[]{"open", url});
} else {
Runtime.getRuntime().exec(new String[]{"xdg-open", url});
}
} catch (Exception ex) {
Toolkit.getDefaultToolkit().beep();
}
}
public HotspotEngine getHotspotEngine() {
return hotspotEngine;
}
// ========== Selection / Copy-Paste ========== // ========== Selection / Copy-Paste ==========
public boolean isBlockSelectMode() {
return blockSelectMode;
}
public void setBlockSelectMode(boolean val) {
this.blockSelectMode = val;
repaint();
}
public boolean isTrimRectSizingHandles() {
return trimRectSizingHandles;
}
public void setTrimRectSizingHandles(boolean val) {
this.trimRectSizingHandles = val;
repaint();
}
public DragHandle getDragHandleAt(int mx, int my) {
if (!hasSelection() || client == null) return DragHandle.NONE;
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol);
int x1 = ox + c1 * cellWidth;
int y1 = oy + r1 * cellHeight;
int x2 = ox + (c2 + 1) * cellWidth;
int y2 = oy + (r2 + 1) * cellHeight;
int xm = (x1 + x2) / 2;
int ym = (y1 + y2) / 2;
int tol = 6;
if (Math.abs(mx - x1) <= tol && Math.abs(my - y1) <= tol) return DragHandle.NW;
if (Math.abs(mx - xm) <= tol && Math.abs(my - y1) <= tol) return DragHandle.N;
if (Math.abs(mx - x2) <= tol && Math.abs(my - y1) <= tol) return DragHandle.NE;
if (Math.abs(mx - x2) <= tol && Math.abs(my - ym) <= tol) return DragHandle.E;
if (Math.abs(mx - x2) <= tol && Math.abs(my - y2) <= tol) return DragHandle.SE;
if (Math.abs(mx - xm) <= tol && Math.abs(my - y2) <= tol) return DragHandle.S;
if (Math.abs(mx - x1) <= tol && Math.abs(my - y2) <= tol) return DragHandle.SW;
if (Math.abs(mx - x1) <= tol && Math.abs(my - ym) <= tol) return DragHandle.W;
if (mx >= x1 && mx <= x2 && my >= y1 && my <= y2) {
return DragHandle.MOVE;
}
return DragHandle.NONE;
}
public Cursor getCursorForDragHandle(DragHandle handle) {
switch (handle) {
case NW: return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
case N: return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
case NE: return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
case E: return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
case SE: return Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR);
case S: return Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
case SW: return Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR);
case W: return Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR);
case MOVE: return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
default: return Cursor.getDefaultCursor();
}
}
public void clearSelection() { public void clearSelection() {
selectionStartRow = -1; selectionStartRow = -1;
selectionStartCol = -1; selectionStartCol = -1;
@@ -415,23 +714,30 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int r2 = Math.max(selectionStartRow, selectionEndRow); int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1, c2; int c1, c2;
if (blockSelectMode) { if (blockSelectMode || trimRectSizingHandles) {
c1 = Math.min(selectionStartCol, selectionEndCol); c1 = Math.min(selectionStartCol, selectionEndCol);
c2 = Math.max(selectionStartCol, selectionEndCol); c2 = Math.max(selectionStartCol, selectionEndCol);
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
for (int r = r1; r <= r2; r++) { for (int r = r1; r <= r2; r++) {
if (r > r1) result.append('\n'); if (r > r1) result.append('\n');
StringBuilder line = new StringBuilder();
for (int c = c1; c <= c2; c++) { for (int c = c1; c <= c2; c++) {
int baddr = r * cols + c; int baddr = r * cols + c;
ExtendedAttribute ea = sb.getCell(baddr); ExtendedAttribute ea = sb.getCell(baddr);
char ch = ea.ucs4; char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) { if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
result.append(' '); line.append(' ');
} else { } else {
result.append(ch); line.append(ch);
} }
} }
// Trim trailing whitespace on copied lines per HoD CutPasteOptions
int end = line.length();
while (end > 0 && line.charAt(end - 1) == ' ') {
end--;
}
result.append(line.substring(0, end));
} }
return result.toString(); return result.toString();
} else { } else {
@@ -446,21 +752,34 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
StringBuilder line = new StringBuilder();
int prevRow = startAddr / cols; int prevRow = startAddr / cols;
for (int addr = startAddr; addr <= endAddr; addr++) { for (int addr = startAddr; addr <= endAddr; addr++) {
int curRow = addr / cols; int curRow = addr / cols;
if (curRow != prevRow) { if (curRow != prevRow) {
result.append('\n'); int end = line.length();
while (end > 0 && line.charAt(end - 1) == ' ') {
end--;
}
if (result.length() > 0) result.append('\n');
result.append(line.substring(0, end));
line.setLength(0);
prevRow = curRow; prevRow = curRow;
} }
ExtendedAttribute ea = sb.getCell(addr); ExtendedAttribute ea = sb.getCell(addr);
char ch = ea.ucs4; char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) { if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
result.append(' '); line.append(' ');
} else { } else {
result.append(ch); line.append(ch);
} }
} }
int end = line.length();
while (end > 0 && line.charAt(end - 1) == ' ') {
end--;
}
if (result.length() > 0) result.append('\n');
result.append(line.substring(0, end));
return result.toString(); return result.toString();
} }
} }
@@ -471,6 +790,65 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
StringSelection ss = new StringSelection(text); StringSelection ss = new StringSelection(text);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
} }
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit()) {
clearSelection();
repaint();
}
}
public void cutSelection() {
if (!hasSelection() || client == null || !client.getConnectionState().isFullSession()) return;
String text = getSelectedText();
if (!text.isEmpty()) {
StringSelection ss = new StringSelection(text);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
}
ScreenBuffer sb = client.getScreenBuffer();
int cols = sb.getCols();
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol);
if (blockSelectMode || trimRectSizingHandles) {
for (int r = r1; r <= r2; r++) {
for (int c = c1; c <= c2; c++) {
int baddr = r * cols + c;
if (!sb.isProtected(baddr) && !sb.getCell(baddr).isFieldAttribute()) {
ExtendedAttribute ea = sb.getCell(baddr);
ea.ucs4 = ' ';
ea.ec = (byte) 0x40;
if (sb.isFormatted()) {
int faAddr = sb.findFieldAttribute(baddr);
if (faAddr >= 0) {
sb.getCell(faAddr).fa = (byte) (sb.getCell(faAddr).fa | haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY);
}
}
}
}
}
} else {
int startAddr = Math.min(selectionStartRow * cols + selectionStartCol, selectionEndRow * cols + selectionEndCol);
int endAddr = Math.max(selectionStartRow * cols + selectionStartCol, selectionEndRow * cols + selectionEndCol);
for (int baddr = startAddr; baddr <= endAddr; baddr++) {
if (!sb.isProtected(baddr) && !sb.getCell(baddr).isFieldAttribute()) {
ExtendedAttribute ea = sb.getCell(baddr);
ea.ucs4 = ' ';
ea.ec = (byte) 0x40;
if (sb.isFormatted()) {
int faAddr = sb.findFieldAttribute(baddr);
if (faAddr >= 0) {
sb.getCell(faAddr).fa = (byte) (sb.getCell(faAddr).fa | haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY);
}
}
}
}
}
sb.updateDisplaySnapshot();
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit()) {
clearSelection();
}
refreshScreen();
} }
public void pasteClipboard() { public void pasteClipboard() {
@@ -496,11 +874,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int curCol = cols > 0 ? curPos % cols : 0; int curCol = cols > 0 ? curPos % cols : 0;
boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel(); boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel();
boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine(); boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine();
boolean blockPaste = haus.nightmare.j3270.config.Settings.getBlockPaste();
boolean wordBreak = haus.nightmare.j3270.config.Settings.getPasteWordBreak();
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || text.contains("\t") || text.contains("\n") || text.contains("\r"))) { if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || blockPaste || wordBreak || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected); client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected, blockPaste, wordBreak);
} else if (client.getPS() != null) { } else if (client.getPS() != null) {
client.getPS().setPasteWordBreak(wordBreak);
if (blockPaste) {
client.getPS().pasteRectangular(text, curRow, curCol);
} else {
client.getPS().pasteString(text, curRow, curCol); client.getPS().pasteString(text, curRow, curCol);
}
} else { } else {
for (char ch : text.toCharArray()) { for (char ch : text.toCharArray()) {
if (ch == '\n' || ch == '\r') { if (ch == '\n' || ch == '\r') {
@@ -511,6 +896,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
} }
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit() && hasSelection()) {
clearSelection();
}
refreshScreen(); refreshScreen();
} }
} catch (Exception ignored) {} } catch (Exception ignored) {}
@@ -548,6 +936,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
JPopupMenu popup = new JPopupMenu(); JPopupMenu popup = new JPopupMenu();
ThemeManager.stylePopupMenu(popup); ThemeManager.stylePopupMenu(popup);
JMenuItem cutItem = new JMenuItem("Cut");
ThemeManager.styleMenuItem(cutItem);
cutItem.setEnabled(hasSelection() && client != null && client.getConnectionState().isFullSession());
cutItem.addActionListener(ev -> cutSelection());
popup.add(cutItem);
JMenuItem copyItem = new JMenuItem("Copy"); JMenuItem copyItem = new JMenuItem("Copy");
ThemeManager.styleMenuItem(copyItem); ThemeManager.styleMenuItem(copyItem);
copyItem.setEnabled(hasSelection()); copyItem.setEnabled(hasSelection());
@@ -575,6 +969,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}); });
popup.add(blockModeItem); popup.add(blockModeItem);
JCheckBoxMenuItem blockPasteItem = new JCheckBoxMenuItem("Block Paste", haus.nightmare.j3270.config.Settings.getBlockPaste());
ThemeManager.styleMenuItem(blockPasteItem);
blockPasteItem.addActionListener(ev -> {
haus.nightmare.j3270.config.Settings.setBlockPaste(blockPasteItem.isSelected());
applyModeSettings();
});
popup.add(blockPasteItem);
JCheckBoxMenuItem handlesItem = new JCheckBoxMenuItem("Trim Sizing Handles", trimRectSizingHandles);
ThemeManager.styleMenuItem(handlesItem);
handlesItem.addActionListener(ev -> {
trimRectSizingHandles = handlesItem.isSelected();
haus.nightmare.j3270.config.Settings.setTrimRectSizingHandles(trimRectSizingHandles);
repaint();
});
popup.add(handlesItem);
popup.show(this, e.getX(), e.getY()); popup.show(this, e.getX(), e.getY());
} }
@@ -584,7 +995,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int r1 = Math.min(selectionStartRow, selectionEndRow); int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow); int r2 = Math.max(selectionStartRow, selectionEndRow);
if (blockSelectMode) { if (blockSelectMode || trimRectSizingHandles) {
int c1 = Math.min(selectionStartCol, selectionEndCol); int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol); int c2 = Math.max(selectionStartCol, selectionEndCol);
return row >= r1 && row <= r2 && col >= c1 && col <= c2; return row >= r1 && row <= r2 && col >= c1 && col <= c2;
@@ -686,9 +1097,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
bindKeyToMap(im, "APL", haus.nightmare.j3270.config.Settings.getKeyBinding("APL", "alt F3")); bindKeyToMap(im, "APL", haus.nightmare.j3270.config.Settings.getKeyBinding("APL", "alt F3"));
// Copy/Paste/Lightpen bindings // Copy/Paste/Lightpen bindings
String defCut = haus.nightmare.j3270.config.Settings.isMac() ? "meta X, ctrl X" : "ctrl X, shift DELETE";
String defCopy = haus.nightmare.j3270.config.Settings.getDefaultBinding("COPY"); String defCopy = haus.nightmare.j3270.config.Settings.getDefaultBinding("COPY");
String defPaste = haus.nightmare.j3270.config.Settings.getDefaultBinding("PASTE"); String defPaste = haus.nightmare.j3270.config.Settings.getDefaultBinding("PASTE");
String defSelectAll = haus.nightmare.j3270.config.Settings.getDefaultBinding("SELECTALL"); String defSelectAll = haus.nightmare.j3270.config.Settings.getDefaultBinding("SELECTALL");
bindKeyToMap(im, "CUT", haus.nightmare.j3270.config.Settings.getKeyBinding("CUT", defCut));
bindKeyToMap(im, "COPY", haus.nightmare.j3270.config.Settings.getKeyBinding("COPY", defCopy)); bindKeyToMap(im, "COPY", haus.nightmare.j3270.config.Settings.getKeyBinding("COPY", defCopy));
bindKeyToMap(im, "PASTE", haus.nightmare.j3270.config.Settings.getKeyBinding("PASTE", defPaste)); bindKeyToMap(im, "PASTE", haus.nightmare.j3270.config.Settings.getKeyBinding("PASTE", defPaste));
bindKeyToMap(im, "SELECTALL", haus.nightmare.j3270.config.Settings.getKeyBinding("SELECTALL", defSelectAll)); bindKeyToMap(im, "SELECTALL", haus.nightmare.j3270.config.Settings.getKeyBinding("SELECTALL", defSelectAll));
@@ -725,6 +1138,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
am.put("j3270-SYSREQ", createAction(this::handleSysReq)); am.put("j3270-SYSREQ", createAction(this::handleSysReq));
am.put("j3270-CURSEL", createAction(this::handleCursorSelect)); am.put("j3270-CURSEL", createAction(this::handleCursorSelect));
am.put("j3270-CUT", createAction(this::cutSelection));
am.put("j3270-COPY", createAction(this::copySelection)); am.put("j3270-COPY", createAction(this::copySelection));
am.put("j3270-PASTE", createAction(this::pasteClipboard)); am.put("j3270-PASTE", createAction(this::pasteClipboard));
am.put("j3270-SELECTALL", createAction(this::selectAll)); am.put("j3270-SELECTALL", createAction(this::selectAll));
@@ -1179,10 +1593,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock()); ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
ip.setBlockPaste(haus.nightmare.j3270.config.Settings.getBlockPaste());
} }
if (client.getPS() != null) { if (client.getPS() != null) {
client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel()); client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel());
client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine()); client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine());
client.getPS().setBlockPaste(haus.nightmare.j3270.config.Settings.getBlockPaste());
} }
} }
if (statusBar != null) { if (statusBar != null) {
@@ -1238,13 +1654,45 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
setupFont(); setupFont();
applyModeSettings(); applyModeSettings();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode(); blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
trimRectSizingHandles = haus.nightmare.j3270.config.Settings.getTrimRectSizingHandles();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler(); crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle(); String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK; cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
// Phase 25: Reload Hotspots and Mouse Wheel preferences
hotspotsEnabled = haus.nightmare.j3270.config.Settings.getHotspotsEnabled();
hotspotEngine.setUrlEnabled(haus.nightmare.j3270.config.Settings.getHotspotsUrlEnabled());
hotspotEngine.setPfKeyEnabled(haus.nightmare.j3270.config.Settings.getHotspotsPfEnabled());
hotspotEngine.setMenuChoiceEnabled(haus.nightmare.j3270.config.Settings.getHotspotsMenuChoicesEnabled());
hotspotEngine.invalidateCache();
mouseWheelEnabled = haus.nightmare.j3270.config.Settings.getMouseWheelEnabled();
mouseWheelUpAction = haus.nightmare.j3270.config.Settings.getMouseWheelUpKey();
mouseWheelDownAction = haus.nightmare.j3270.config.Settings.getMouseWheelDownKey();
revalidate(); revalidate();
repaint(); repaint();
} }
public boolean isInputMaskEnabled() {
return inputMaskEnabled;
}
public void setInputMaskEnabled(boolean enabled) {
this.inputMaskEnabled = enabled;
repaint();
}
public String getInputMaskChar() {
return inputMaskChar;
}
public void setInputMaskChar(String maskChar) {
this.inputMaskChar = maskChar;
repaint();
}
public void setFontSize(int size) { public void setFontSize(int size) {
currentFontSize = size; currentFontSize = size;
haus.nightmare.j3270.config.Settings.setFontSize(size); haus.nightmare.j3270.config.Settings.setFontSize(size);
@@ -1272,7 +1720,41 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
blinkTimer = new Timer(500, e -> { blinkTimer = new Timer(500, e -> {
cursorVisible = !cursorVisible; cursorVisible = !cursorVisible;
textBlinkVisible = !textBlinkVisible; textBlinkVisible = !textBlinkVisible;
// Check if there are any blinking cells or fields on the screen
boolean hasBlinkingCells = false;
ScreenBuffer sb = (client != null) ? client.getScreenBuffer() : null;
if (sb != null) {
int total = sb.getDisplayRows() * sb.getDisplayCols();
byte curFaGr = 0;
for (int i = 0; i < total; i++) {
ExtendedAttribute c = sb.getDisplayCell(i);
if (c.isFieldAttribute()) {
curFaGr = c.gr;
}
if ((c.gr & ExtendedAttribute.GR_BLINK) != 0 || (curFaGr & ExtendedAttribute.GR_BLINK) != 0) {
hasBlinkingCells = true;
break;
}
}
}
if (hasBlinkingCells) {
repaint(); repaint();
} else if (sb != null && client != null && client.getConnectionState() != null && client.getConnectionState().isFullSession()) {
// Only repaint the cursor cell bounding rect
int cols = sb.getDisplayCols();
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
int curAddr = sb.getDisplayCursorAddress();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int cx = ox + curCol * cellWidth;
int cy = oy + curRow * cellHeight;
repaint(cx, cy, cellWidth, cellHeight);
} else {
repaint();
}
}); });
blinkTimer.start(); blinkTimer.start();
} }
@@ -1527,13 +2009,19 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
// Password fields // Password fields
if (faIsZero(currentFA & 0xFF)) { if (faIsZero(currentFA & 0xFF)) {
if (inputMaskEnabled) {
char ch = ea.ucs4; char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) { if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? boldTerminalFont : terminalFont; Font f = bold ? boldTerminalFont : terminalFont;
g2.setFont(f); g2.setFont(f);
g2.setColor(fgColor); g2.setColor(fgColor);
g2.drawString("*", x, y + fontAscent); String mask = (inputMaskChar != null && !inputMaskChar.isEmpty()) ? inputMaskChar : "*";
g2.drawString(mask, x, y + fontAscent);
} }
}
// Field Outlining (0xC2 / TY_FOUTLINE)
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
if (isCellSelected(row, col)) { if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR); g2.setColor(SELECTION_COLOR);
@@ -1554,7 +2042,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
// Check text blink (500ms cycle) // Check text blink (500ms cycle)
boolean blink = (gr & GR_BLINK) != 0; boolean blink = (gr & GR_BLINK) != 0 || (currentFieldEa != null && (currentFieldEa.gr & GR_BLINK) != 0);
boolean suppressGlyph = blink && !textBlinkVisible; boolean suppressGlyph = blink && !textBlinkVisible;
// Draw character or Programmed Symbol // Draw character or Programmed Symbol
@@ -1588,12 +2076,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
if (underline) { if (underline && !suppressGlyph) {
g2.setColor(fgColor); g2.setColor(fgColor);
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2); int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
g2.drawLine(x, ulY, x + cellWidth - 1, ulY); g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
} }
// Field Outlining (0xC2 / TY_FOUTLINE)
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
// Draw search highlight // Draw search highlight
if (isCellSearchHighlighted(row, col)) { if (isCellSearchHighlighted(row, col)) {
g2.setColor(SEARCH_HIGHLIGHT_COLOR); g2.setColor(SEARCH_HIGHLIGHT_COLOR);
@@ -1608,6 +2099,63 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
} }
} }
// Phase 26.1: Rectangular Block Selection Sizing Handles & Box Outline
if (trimRectSizingHandles && hasSelection() && client != null && client.getConnectionState().isFullSession()) {
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol);
int x1 = ox + c1 * cellWidth;
int y1 = oy + r1 * cellHeight;
int x2 = ox + (c2 + 1) * cellWidth;
int y2 = oy + (r2 + 1) * cellHeight;
int w = x2 - x1;
int h = y2 - y1;
int xm = (x1 + x2) / 2;
int ym = (y1 + y2) / 2;
Color oldColor = g2.getColor();
Stroke oldStroke = g2.getStroke();
g2.setColor(Color.YELLOW);
g2.drawRect(x1, y1, w, h);
drawSizingHandle(g2, x1, y1);
drawSizingHandle(g2, xm, y1);
drawSizingHandle(g2, x2, y1);
drawSizingHandle(g2, x2, ym);
drawSizingHandle(g2, x2, y2);
drawSizingHandle(g2, xm, y2);
drawSizingHandle(g2, x1, y2);
drawSizingHandle(g2, x1, ym);
g2.setStroke(oldStroke);
g2.setColor(oldColor);
}
// Phase 25.2: Draw visual underline on hovered hotspot
if (hotspotsEnabled && hoveredHotspot != null) {
Color oldColor = g2.getColor();
Stroke oldStroke = g2.getStroke();
g2.setColor(new Color(0, 220, 255)); // Bright cyan hotspot underline
g2.setStroke(new BasicStroke(2.0f));
int hr0 = hoveredHotspot.getStartRow();
int hr1 = hoveredHotspot.getEndRow();
for (int r = hr0; r <= hr1; r++) {
int c0 = (r == hr0) ? hoveredHotspot.getStartCol() : 0;
int c1 = (r == hr1) ? hoveredHotspot.getEndCol() : cols - 1;
int x1 = ox + c0 * cellWidth;
int x2 = ox + (c1 + 1) * cellWidth;
int ulY = oy + r * cellHeight + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2;
ulY = Math.min(oy + (r + 1) * cellHeight - 1, ulY);
g2.drawLine(x1, ulY, x2 - 1, ulY);
}
g2.setStroke(oldStroke);
g2.setColor(oldColor);
}
// Draw Graphic Cursor if active // Draw Graphic Cursor if active
boolean isGraphicCursor = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive()) boolean isGraphicCursor = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
|| (client.getGraphicsPlane() != null && client.getGraphicsPlane().isGraphicCursorAttached()); || (client.getGraphicsPlane() != null && client.getGraphicsPlane().isGraphicCursorAttached());
@@ -1761,6 +2309,64 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal; return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
} }
/**
* Render field outline lines around cells based on ea.ol or field attribute outline.
* Conforms to IBM 3270 Extended Data Stream / Host On-Demand DS3270 TY_FOUTLINE (0xC2).
*
* Bit 0 (0x01): Underline -> draw line at y + cellHeight - 1
* Bit 1 (0x02): Right vertical line -> draw line at x + cellWidth - 1
* Bit 2 (0x04): Overline -> draw line at y
* Bit 3 (0x08): Left vertical line -> draw line at x
*/
private void drawOutlining(Graphics2D g2, ExtendedAttribute ea, ExtendedAttribute currentFieldEa,
int baddr, int row, int col, int x, int y,
int cellWidth, int cellHeight, int rows, int cols,
ScreenBuffer sb, Color fgColor) {
int outline = ea.ol & 0xFF;
if (outline == 0 && currentFieldEa != null && currentFieldEa.ol != 0) {
int fol = currentFieldEa.ol & 0xFF;
if ((fol & ExtendedAttribute.OL_UNDERLINE) != 0) outline |= ExtendedAttribute.OL_UNDERLINE;
if ((fol & ExtendedAttribute.OL_OVERLINE) != 0) outline |= ExtendedAttribute.OL_OVERLINE;
int size = rows * cols;
int prevAddr = (baddr - 1 + size) % size;
if (sb.getDisplayCell(prevAddr).isFieldAttribute() || col == 0) {
if ((fol & ExtendedAttribute.OL_LEFT) != 0) outline |= ExtendedAttribute.OL_LEFT;
}
int nextAddr = (baddr + 1) % size;
if (sb.getDisplayCell(nextAddr).isFieldAttribute() || col == cols - 1) {
if ((fol & ExtendedAttribute.OL_RIGHT) != 0) outline |= ExtendedAttribute.OL_RIGHT;
}
}
if (outline != 0) {
g2.setColor(fgColor);
// Bit 0 (0x01): Underline -> draw line at y + cellHeight - 1
if ((outline & ExtendedAttribute.OL_UNDERLINE) != 0) {
g2.drawLine(x, y + cellHeight - 1, x + cellWidth - 1, y + cellHeight - 1);
}
// Bit 1 (0x02): Right vertical line -> draw line at x + cellWidth - 1
if ((outline & ExtendedAttribute.OL_RIGHT) != 0) {
g2.drawLine(x + cellWidth - 1, y, x + cellWidth - 1, y + cellHeight - 1);
}
// Bit 2 (0x04): Overline -> draw line at y
if ((outline & ExtendedAttribute.OL_OVERLINE) != 0) {
g2.drawLine(x, y, x + cellWidth - 1, y);
}
// Bit 3 (0x08): Left vertical line -> draw line at x
if ((outline & ExtendedAttribute.OL_LEFT) != 0) {
g2.drawLine(x, y, x, y + cellHeight - 1);
}
}
}
private void drawSizingHandle(Graphics2D g2, int cx, int cy) {
g2.setColor(Color.YELLOW);
g2.fillRect(cx - 2, cy - 2, 5, 5);
g2.setColor(Color.BLACK);
g2.drawRect(cx - 2, cy - 2, 5, 5);
}
public void dispose() { public void dispose() {
if (blinkTimer != null) if (blinkTimer != null)
blinkTimer.stop(); blinkTimer.stop();
@@ -20,6 +20,7 @@ import java.awt.event.MouseEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import haus.nightmare.j3270.config.Settings;
/** /**
* Centralized theme manager for j3270 Java desktop UI. * Centralized theme manager for j3270 Java desktop UI.
@@ -33,6 +34,25 @@ public final class ThemeManager {
private ThemeManager() {} private ThemeManager() {}
public static Font getUiFont() {
return getUiFont(Font.PLAIN, 0);
}
public static Font getUiFont(int style, int sizeDelta) {
String family = Settings.getUiFontFamily();
int size = Math.max(8, Settings.getUiFontSize() + sizeDelta);
return new Font(family, style, size);
}
public static Font getMonospacedUiFont() {
return getMonospacedUiFont(Font.PLAIN, 0);
}
public static Font getMonospacedUiFont(int style, int sizeDelta) {
int size = Math.max(8, Settings.getUiFontSize() + sizeDelta);
return new Font(Font.MONOSPACED, style, size);
}
public static UITheme getTheme() { public static UITheme getTheme() {
return currentTheme; return currentTheme;
} }
@@ -433,6 +453,47 @@ public final class ThemeManager {
UIManager.put("TitledBorder.titleColor", fgMain); UIManager.put("TitledBorder.titleColor", fgMain);
UIManager.put("OptionPane.background", bgPanel); UIManager.put("OptionPane.background", bgPanel);
UIManager.put("OptionPane.messageForeground", fgMain); UIManager.put("OptionPane.messageForeground", fgMain);
// UI Font Defaults
Font uiFont = getUiFont();
Font uiFontBold = getUiFont(Font.BOLD, 0);
Font monoUiFont = getMonospacedUiFont();
UIManager.put("Button.font", uiFont);
UIManager.put("ToggleButton.font", uiFont);
UIManager.put("RadioButton.font", uiFont);
UIManager.put("CheckBox.font", uiFont);
UIManager.put("ColorChooser.font", uiFont);
UIManager.put("ComboBox.font", uiFont);
UIManager.put("Label.font", uiFont);
UIManager.put("List.font", uiFont);
UIManager.put("MenuBar.font", uiFont);
UIManager.put("MenuItem.font", uiFont);
UIManager.put("RadioButtonMenuItem.font", uiFont);
UIManager.put("CheckBoxMenuItem.font", uiFont);
UIManager.put("Menu.font", uiFont);
UIManager.put("PopupMenu.font", uiFont);
UIManager.put("OptionPane.font", uiFont);
UIManager.put("OptionPane.messageFont", uiFont);
UIManager.put("OptionPane.buttonFont", uiFont);
UIManager.put("Panel.font", uiFont);
UIManager.put("ProgressBar.font", uiFont);
UIManager.put("ScrollPane.font", uiFont);
UIManager.put("Viewport.font", uiFont);
UIManager.put("TabbedPane.font", uiFont);
UIManager.put("Table.font", uiFont);
UIManager.put("TableHeader.font", uiFontBold);
UIManager.put("TextField.font", uiFont);
UIManager.put("PasswordField.font", uiFont);
UIManager.put("TextArea.font", monoUiFont);
UIManager.put("TextPane.font", uiFont);
UIManager.put("EditorPane.font", uiFont);
UIManager.put("TitledBorder.font", uiFontBold);
UIManager.put("ToolBar.font", uiFont);
UIManager.put("ToolTip.font", uiFont);
UIManager.put("Tree.font", uiFont);
UIManager.put("Spinner.font", uiFont);
UIManager.put("FormattedTextField.font", uiFont);
} }
// ========================================================================= // =========================================================================
@@ -446,6 +507,7 @@ public final class ThemeManager {
public static JButton styleButton(JButton button, ButtonVariant variant) { public static JButton styleButton(JButton button, ButtonVariant variant) {
if (button == null) return null; if (button == null) return null;
button.setUI(new StyledButtonUI(variant)); button.setUI(new StyledButtonUI(variant));
button.setFont(getUiFont());
button.setFocusPainted(false); button.setFocusPainted(false);
button.setOpaque(false); button.setOpaque(false);
button.setContentAreaFilled(false); button.setContentAreaFilled(false);
@@ -465,6 +527,7 @@ public final class ThemeManager {
public static JTextField styleTextField(JTextField field) { public static JTextField styleTextField(JTextField field) {
if (field == null) return null; if (field == null) return null;
field.setFont(getUiFont());
field.setBackground(getBgComponent()); field.setBackground(getBgComponent());
field.setForeground(getFgMain()); field.setForeground(getFgMain());
field.setCaretColor(getFgMain()); field.setCaretColor(getFgMain());
@@ -478,6 +541,7 @@ public final class ThemeManager {
public static JTextArea styleTextArea(JTextArea area) { public static JTextArea styleTextArea(JTextArea area) {
if (area == null) return null; if (area == null) return null;
area.setFont(getMonospacedUiFont());
area.setBackground(getCodeAreaBg()); area.setBackground(getCodeAreaBg());
area.setForeground(getCodeAreaFg()); area.setForeground(getCodeAreaFg());
area.setCaretColor(getFgMain()); area.setCaretColor(getFgMain());
@@ -488,6 +552,7 @@ public final class ThemeManager {
public static JComboBox<?> styleComboBox(JComboBox<?> box) { public static JComboBox<?> styleComboBox(JComboBox<?> box) {
if (box == null) return null; if (box == null) return null;
box.setFont(getUiFont());
box.setBackground(getBgComponent()); box.setBackground(getBgComponent());
box.setForeground(getFgMain()); box.setForeground(getFgMain());
box.setRenderer(new DefaultListCellRenderer() { box.setRenderer(new DefaultListCellRenderer() {
@@ -510,12 +575,14 @@ public final class ThemeManager {
public static JTable styleTable(JTable table) { public static JTable styleTable(JTable table) {
if (table == null) return null; if (table == null) return null;
table.setFont(getUiFont());
table.setBackground(getBgComponent()); table.setBackground(getBgComponent());
table.setForeground(getFgMain()); table.setForeground(getFgMain());
table.setSelectionBackground(getSelectionBg()); table.setSelectionBackground(getSelectionBg());
table.setSelectionForeground(getSelectionFg()); table.setSelectionForeground(getSelectionFg());
table.setGridColor(getTableGrid()); table.setGridColor(getTableGrid());
table.setRowHeight(24); int rowHeight = Math.max(24, getUiFont().getSize() + 10);
table.setRowHeight(rowHeight);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() { table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
@Override @Override
@@ -537,14 +604,14 @@ public final class ThemeManager {
if (header != null) { if (header != null) {
header.setBackground(getTableHeaderBg()); header.setBackground(getTableHeaderBg());
header.setForeground(getTableHeaderFg()); header.setForeground(getTableHeaderFg());
header.setFont(header.getFont().deriveFont(Font.BOLD)); header.setFont(getUiFont(Font.BOLD, 0));
header.setDefaultRenderer(new DefaultTableCellRenderer() { header.setDefaultRenderer(new DefaultTableCellRenderer() {
@Override @Override
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) { public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col); super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
setBackground(getTableHeaderBg()); setBackground(getTableHeaderBg());
setForeground(getTableHeaderFg()); setForeground(getTableHeaderFg());
setFont(getFont().deriveFont(Font.BOLD)); setFont(getUiFont(Font.BOLD, 0));
setBorder(BorderFactory.createCompoundBorder( setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()), BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
new EmptyBorder(4, 6, 4, 6))); new EmptyBorder(4, 6, 4, 6)));
@@ -558,6 +625,7 @@ public final class ThemeManager {
public static JTabbedPane styleTabbedPane(JTabbedPane tp) { public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
if (tp == null) return null; if (tp == null) return null;
tp.setUI(new StyledTabbedPaneUI()); tp.setUI(new StyledTabbedPaneUI());
tp.setFont(getUiFont());
tp.setBackground(getBgMain()); tp.setBackground(getBgMain());
tp.setForeground(getFgMain()); tp.setForeground(getFgMain());
return tp; return tp;
@@ -566,6 +634,7 @@ public final class ThemeManager {
public static JMenuBar styleMenuBar(JMenuBar bar) { public static JMenuBar styleMenuBar(JMenuBar bar) {
if (bar == null) return null; if (bar == null) return null;
bar.setUI(new StyledMenuBarUI()); bar.setUI(new StyledMenuBarUI());
bar.setFont(getUiFont());
bar.setBackground(getMenuBarBg()); bar.setBackground(getMenuBarBg());
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder())); bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
return bar; return bar;
@@ -574,6 +643,7 @@ public final class ThemeManager {
public static JMenu styleMenu(JMenu menu) { public static JMenu styleMenu(JMenu menu) {
if (menu == null) return null; if (menu == null) return null;
menu.setUI(new StyledMenuUI()); menu.setUI(new StyledMenuUI());
menu.setFont(getUiFont());
menu.setForeground(getMenuBarFg()); menu.setForeground(getMenuBarFg());
menu.setBackground(getMenuBarBg()); menu.setBackground(getMenuBarBg());
menu.setOpaque(false); menu.setOpaque(false);
@@ -583,6 +653,7 @@ public final class ThemeManager {
public static JMenuItem styleMenuItem(JMenuItem item) { public static JMenuItem styleMenuItem(JMenuItem item) {
if (item == null) return null; if (item == null) return null;
item.setUI(new StyledMenuItemUI()); item.setUI(new StyledMenuItemUI());
item.setFont(getUiFont());
item.setBackground(getMenuPopupBg()); item.setBackground(getMenuPopupBg());
item.setForeground(getMenuItemFg()); item.setForeground(getMenuItemFg());
return item; return item;
@@ -591,6 +662,7 @@ public final class ThemeManager {
public static JPopupMenu stylePopupMenu(JPopupMenu popup) { public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
if (popup == null) return null; if (popup == null) return null;
popup.setUI(new StyledPopupMenuUI()); popup.setUI(new StyledPopupMenuUI());
popup.setFont(getUiFont());
popup.setBackground(getMenuPopupBg()); popup.setBackground(getMenuPopupBg());
popup.setBorder(BorderFactory.createCompoundBorder( popup.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(getBorder(), 1), new LineBorder(getBorder(), 1),
@@ -610,12 +682,14 @@ public final class ThemeManager {
public static JSpinner styleSpinner(JSpinner sp) { public static JSpinner styleSpinner(JSpinner sp) {
if (sp == null) return null; if (sp == null) return null;
sp.setFont(getUiFont());
sp.setBackground(getBgComponent()); sp.setBackground(getBgComponent());
sp.setForeground(getFgMain()); sp.setForeground(getFgMain());
sp.setBorder(new LineBorder(getBorder(), 1)); sp.setBorder(new LineBorder(getBorder(), 1));
JComponent editor = sp.getEditor(); JComponent editor = sp.getEditor();
if (editor instanceof JSpinner.DefaultEditor) { if (editor instanceof JSpinner.DefaultEditor) {
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField(); JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
tf.setFont(getUiFont());
tf.setBackground(getBgComponent()); tf.setBackground(getBgComponent());
tf.setForeground(getFgMain()); tf.setForeground(getFgMain());
tf.setCaretColor(getFgMain()); tf.setCaretColor(getFgMain());
@@ -626,6 +700,7 @@ public final class ThemeManager {
public static JCheckBox styleCheckBox(JCheckBox cb) { public static JCheckBox styleCheckBox(JCheckBox cb) {
if (cb == null) return null; if (cb == null) return null;
cb.setFont(getUiFont());
cb.setOpaque(false); cb.setOpaque(false);
cb.setForeground(getFgMain()); cb.setForeground(getFgMain());
cb.setFocusPainted(false); cb.setFocusPainted(false);
@@ -634,6 +709,7 @@ public final class ThemeManager {
public static JRadioButton styleRadioButton(JRadioButton rb) { public static JRadioButton styleRadioButton(JRadioButton rb) {
if (rb == null) return null; if (rb == null) return null;
rb.setFont(getUiFont());
rb.setOpaque(false); rb.setOpaque(false);
rb.setForeground(getFgMain()); rb.setForeground(getFgMain());
rb.setFocusPainted(false); rb.setFocusPainted(false);
@@ -646,7 +722,7 @@ public final class ThemeManager {
title, title,
TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_JUSTIFICATION,
TitledBorder.DEFAULT_POSITION, TitledBorder.DEFAULT_POSITION,
new Font(Font.SANS_SERIF, Font.BOLD, 12), getUiFont(Font.BOLD, 0),
getFgMain()); getFgMain());
} }
@@ -732,6 +808,7 @@ public final class ThemeManager {
if (comp instanceof JButton) { if (comp instanceof JButton) {
JButton btn = (JButton) comp; JButton btn = (JButton) comp;
btn.setFont(getUiFont());
if (!(btn.getUI() instanceof StyledButtonUI)) { if (!(btn.getUI() instanceof StyledButtonUI)) {
styleButton(btn, ButtonVariant.DEFAULT); styleButton(btn, ButtonVariant.DEFAULT);
} }
@@ -770,6 +847,21 @@ public final class ThemeManager {
if (comp instanceof JLabel) { if (comp instanceof JLabel) {
comp.setForeground(getFgMain(theme)); comp.setForeground(getFgMain(theme));
JLabel label = (JLabel) comp;
Font cur = label.getFont();
int style = (cur != null && cur.isBold()) ? Font.BOLD : Font.PLAIN;
Object deltaProp = label.getClientProperty("uiFontSizeDelta");
int delta = 0;
if (deltaProp instanceof Integer) {
delta = (Integer) deltaProp;
} else {
int baseSize = Settings.getUiFontSize();
if (cur != null && cur.getSize() > baseSize) {
delta = cur.getSize() - baseSize;
}
label.putClientProperty("uiFontSizeDelta", delta);
}
label.setFont(getUiFont(style, delta));
return; return;
} }
@@ -783,6 +875,7 @@ public final class ThemeManager {
if (b instanceof TitledBorder) { if (b instanceof TitledBorder) {
TitledBorder tb = (TitledBorder) b; TitledBorder tb = (TitledBorder) b;
tb.setTitleColor(getFgMain(theme)); tb.setTitleColor(getFgMain(theme));
tb.setTitleFont(getUiFont(Font.BOLD, 0));
tb.setBorder(new LineBorder(getBorder(theme), 1)); tb.setBorder(new LineBorder(getBorder(theme), 1));
} }
} else if (comp instanceof Container) { } else if (comp instanceof Container) {
@@ -17,6 +17,7 @@ public class UntrustedCertificateDialog extends JDialog {
public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) { public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) {
super(parent, "Untrusted SSL/TLS Certificate", true); super(parent, "Untrusted SSL/TLS Certificate", true);
AppIcon.applyTo(this);
buildUI(host, port, chain, exception); buildUI(host, port, chain, exception);
pack(); pack();
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
@@ -31,18 +32,20 @@ public class UntrustedCertificateDialog extends JDialog {
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
headerPanel.setOpaque(false); headerPanel.setOpaque(false);
JLabel iconLabel = new JLabel("⚠️"); JLabel iconLabel = new JLabel("⚠️");
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28)); iconLabel.putClientProperty("uiFontSizeDelta", 15);
iconLabel.setFont(ThemeManager.getUiFont(Font.PLAIN, 15));
headerPanel.add(iconLabel); headerPanel.add(iconLabel);
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
titleBox.setOpaque(false); titleBox.setOpaque(false);
JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); JLabel titleLabel = new JLabel("Untrusted SSL Certificate");
titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); titleLabel.putClientProperty("uiFontSizeDelta", 3);
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
titleLabel.setForeground(ThemeManager.getOiaFgWarn()); titleLabel.setForeground(ThemeManager.getOiaFgWarn());
titleBox.add(titleLabel); titleBox.add(titleLabel);
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); subtitleLabel.setFont(ThemeManager.getUiFont());
subtitleLabel.setForeground(ThemeManager.getFgMuted()); subtitleLabel.setForeground(ThemeManager.getFgMuted());
titleBox.add(subtitleLabel); titleBox.add(subtitleLabel);
headerPanel.add(titleBox); headerPanel.add(titleBox);
@@ -69,7 +72,7 @@ public class UntrustedCertificateDialog extends JDialog {
JTextArea detailsArea = new JTextArea(sb.toString()); JTextArea detailsArea = new JTextArea(sb.toString());
detailsArea.setEditable(false); detailsArea.setEditable(false);
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); detailsArea.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextArea(detailsArea); ThemeManager.styleTextArea(detailsArea);
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@@ -91,7 +94,7 @@ public class UntrustedCertificateDialog extends JDialog {
JButton trustBtn = new JButton("Connect Anyway"); JButton trustBtn = new JButton("Connect Anyway");
ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER); ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER);
trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); trustBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
trustBtn.addActionListener(e -> { trustBtn.addActionListener(e -> {
accepted = true; accepted = true;
dispose(); dispose();
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="124 738 320 320"
width="320"
height="320"
xml:space="preserve"
id="svg_j3270_icon"
version="1.1">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
<rect x="124" y="738" width="320" height="320" fill="#000000" />
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
<g transform="scale(0.1)" id="g12">
<g id="g14">
<!-- Green Terminal Screen Frame -->
<path
id="path28"
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
<!-- Blue Wave / Exponential Curve -->
<path
id="path30"
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
<!-- Red Circular Arc -->
<path
id="path32"
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
<!-- Yellow Smiley Head -->
<path
id="path34"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
<!-- 'l' -->
<path
id="path36"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<path
id="path38"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
<!-- 'i' -->
<path
id="path40"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<path
id="path42"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
<!-- 'b' -->
<path
id="path44"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<path
id="path46"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
<!-- '3' -->
<path
id="path48"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<path
id="path50"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
<!-- '2' -->
<path
id="path52"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<path
id="path54"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
<!-- '7' -->
<path
id="path56"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<path
id="path58"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
<!-- '0' -->
<path
id="path60"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<path
id="path62"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
<!-- 'j' -->
<path
id="path64"
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<path
id="path66"
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
<!-- Smile -->
<path
id="path68"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
<!-- Left Eye (cyan) -->
<path
id="path70"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
<!-- Right Eye (cyan) -->
<path
id="path72"
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
<!-- Nose -->
<path
id="path74"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2892.03,1528.23 -29.21,-20.68" />
<path
id="path76"
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
d="m 2862.82,1507.55 h 32.86" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

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

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -0,0 +1,57 @@
package haus.nightmare.j3270;
import haus.nightmare.j3270.ui.AppIcon;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AppIconTest {
@Test
public void testIconsAreLoaded() {
List<Image> icons = AppIcon.getAppIcons();
assertNotNull(icons, "Icons list must not be null");
assertFalse(icons.isEmpty(), "At least one icon image must be loaded");
// Verify standard sizes are present
int[] expectedSizes = {16, 24, 32, 48, 64, 128, 256, 512};
for (int expectedSize : expectedSizes) {
boolean found = icons.stream().anyMatch(img -> img.getWidth(null) == expectedSize && img.getHeight(null) == expectedSize);
assertTrue(found, "Expected icon of size " + expectedSize + "x" + expectedSize + " was not found");
}
}
@Test
public void testDefaultIcon() {
Image defaultIcon = AppIcon.getDefaultIcon();
assertNotNull(defaultIcon, "Default icon must not be null");
assertEquals(256, defaultIcon.getWidth(null), "Default icon width should be 256");
assertEquals(256, defaultIcon.getHeight(null), "Default icon height should be 256");
}
@Test
public void testApplyToInHeadless() {
// Must execute without exception even in headless environment
assertDoesNotThrow(() -> AppIcon.applyTo(null));
if (!GraphicsEnvironment.isHeadless()) {
JFrame frame = new JFrame();
assertDoesNotThrow(() -> AppIcon.applyTo(frame));
frame.dispose();
}
}
@Test
public void testSvgResourcesExist() {
URL svgUrl = getClass().getResource("/icons/lib3270j.svg");
assertNotNull(svgUrl, "lib3270j.svg must exist in /icons/");
URL iconSvgUrl = getClass().getResource("/icons/j3270_icon.svg");
assertNotNull(iconSvgUrl, "j3270_icon.svg must exist in /icons/");
}
}
@@ -0,0 +1,93 @@
package haus.nightmare.j3270;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
public class LoggingConfigurationTest {
private PrintStream originalOut;
private PrintStream originalErr;
@BeforeEach
public void setUp() {
originalOut = System.out;
originalErr = System.err;
}
@AfterEach
public void tearDown() {
System.setOut(originalOut);
System.setErr(originalErr);
// Ensure all handlers are closed and logging reset
J3270App.configureLogging(false);
}
@Test
public void testLoggingDisabledWhenDebugFalse() {
File logFile = new File("j3270.log");
if (logFile.exists()) {
logFile.delete();
}
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
System.setErr(new PrintStream(errContent));
J3270App.configureLogging(false);
// Root logger should have no handlers attached
Logger rootLogger = Logger.getLogger("");
assertEquals(0, rootLogger.getHandlers().length, "Root logger should have no handlers when debug is disabled");
assertEquals(Level.OFF, rootLogger.getLevel(), "Root logger level should be OFF when debug is disabled");
// Application loggers should be OFF
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare").getLevel());
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.j3270").getLevel());
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
// Emit log records at all levels
Logger appLogger = Logger.getLogger("haus.nightmare.j3270.J3270App");
appLogger.severe("Test SEVERE message");
appLogger.warning("Test WARNING message");
appLogger.info("Test INFO message");
appLogger.fine("Test FINE message");
// Verify nothing was written to stdout or stderr
assertEquals(0, outContent.size(), "Standard output should be empty when debug is disabled");
assertEquals(0, errContent.size(), "Standard error should be empty when debug is disabled");
// Verify j3270.log was NOT created
assertFalse(logFile.exists(), "j3270.log should NOT be created when debug is disabled");
}
@Test
public void testLoggingEnabledWhenDebugTrue() {
J3270App.configureLogging(true);
Logger rootLogger = Logger.getLogger("");
Handler[] handlers = rootLogger.getHandlers();
assertTrue(handlers.length >= 2, "Root logger should have at least ConsoleHandler and FileHandler when debug is enabled");
assertEquals(Level.ALL, rootLogger.getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare").getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.j3270").getLevel());
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
File logFile = new File("j3270.log");
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled");
// Clean up
J3270App.configureLogging(false);
}
}
@@ -0,0 +1,343 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.j3270.ui.ConnectDialog;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class HostStorageAndProxyTest {
private List<SavedHost> originalHosts;
@BeforeEach
public void setUp() {
originalHosts = HostStorage.getSavedHosts();
HostStorage.clearAll();
}
@AfterEach
public void tearDown() {
HostStorage.clearAll();
HostStorage.saveAll(originalHosts);
}
@Test
@DisplayName("SavedHost correctly models per-connection proxy and converts to ConnectionConfig")
public void testSavedHostProxyToConnectionConfig() {
// 1. Host with SOCKS5 proxy and auth
SavedHost host1 = new SavedHost(
UUID.randomUUID().toString(),
"Prod Mainframe",
"mvs.corp.local",
2323,
4,
43,
80,
"TSO01",
false,
"TSO",
true,
true,
true,
"BOTH",
"037",
"SOCKS5",
"proxy.internal.corp",
1080,
"admin",
"secret123"
);
ConnectionConfig cfg1 = host1.toConnectionConfig();
assertEquals("mvs.corp.local", cfg1.getHost());
assertEquals(2323, cfg1.getPort());
assertTrue(cfg1.isUseTls());
assertEquals("TSO01", cfg1.getLuName());
assertEquals(ConnectionConfig.ProxyType.SOCKS5, cfg1.getProxyType());
assertEquals("proxy.internal.corp", cfg1.getProxyHost());
assertEquals(1080, cfg1.getProxyPort());
assertEquals("admin", cfg1.getProxyUsername());
assertEquals("secret123", cfg1.getProxyPassword());
// 2. Host with direct connection (NONE proxy)
SavedHost host2 = new SavedHost(
UUID.randomUUID().toString(),
"Local VM370",
"127.0.0.1",
23,
2,
24,
80,
"",
false,
"CMS",
false,
false,
false,
"BOTH",
"037",
"NONE",
"",
0,
"",
""
);
ConnectionConfig cfg2 = host2.toConnectionConfig();
assertEquals("127.0.0.1", cfg2.getHost());
assertEquals(23, cfg2.getPort());
assertFalse(cfg2.isUseTls());
assertEquals(ConnectionConfig.ProxyType.NONE, cfg2.getProxyType());
assertNull(cfg2.getProxyHost());
}
@Test
@DisplayName("HostStorage persists and restores multiple host profiles with isolated per-connection proxies")
public void testHostStoragePersistenceAndIsolation() {
SavedHost h1 = new SavedHost(
"id-1",
"System A (SOCKS5)",
"sysa.corp",
23,
4,
62,
160,
"",
false,
"TSO",
false,
true,
true,
"BOTH",
"037",
"SOCKS5",
"socks.gateway.net",
1080,
"alice",
"passA"
);
SavedHost h2 = new SavedHost(
"id-2",
"System B (HTTP CONNECT)",
"sysb.dmz",
992,
5,
27,
132,
"LU99",
false,
"TSO",
true,
true,
true,
"BOTH",
"1047",
"HTTP",
"squid.proxy.org",
3128,
"bob",
"passB"
);
SavedHost h3 = new SavedHost(
"id-3",
"System C (Direct)",
"sysc.lan",
23,
2,
24,
80,
"",
true, // autoConnect
"CMS",
false,
true,
true,
"BOTH",
"500",
"NONE",
"",
0,
"",
""
);
HostStorage.saveHost(h1);
HostStorage.saveHost(h2);
HostStorage.saveHost(h3);
List<SavedHost> loaded = HostStorage.getSavedHosts();
assertEquals(3, loaded.size());
SavedHost loadedA = HostStorage.findById("id-1");
assertNotNull(loadedA);
assertEquals("System A (SOCKS5)", loadedA.getName());
assertEquals("SOCKS5", loadedA.getProxyType());
assertEquals("socks.gateway.net", loadedA.getProxyHost());
assertEquals(1080, loadedA.getProxyPort());
assertEquals("alice", loadedA.getProxyUsername());
assertEquals("passA", loadedA.getProxyPassword());
SavedHost loadedB = HostStorage.findById("id-2");
assertNotNull(loadedB);
assertEquals("HTTP", loadedB.getProxyType());
assertEquals("squid.proxy.org", loadedB.getProxyHost());
assertEquals(3128, loadedB.getProxyPort());
assertEquals("bob", loadedB.getProxyUsername());
assertEquals("passB", loadedB.getProxyPassword());
SavedHost loadedC = HostStorage.findById("id-3");
assertNotNull(loadedC);
assertEquals("NONE", loadedC.getProxyType());
assertEquals("", loadedC.getProxyHost());
assertTrue(loadedC.isAutoConnect());
// Verify getAutoConnectHost returns System C with its own connection settings
SavedHost auto = HostStorage.getAutoConnectHost();
assertNotNull(auto);
assertEquals("id-3", auto.getId());
assertEquals(ConnectionConfig.ProxyType.NONE, auto.toConnectionConfig().getProxyType());
// Verify deleteHost
HostStorage.deleteHost("id-2");
assertEquals(2, HostStorage.getSavedHosts().size());
assertNull(HostStorage.findById("id-2"));
}
@Test
@DisplayName("HostStorage JSON matches a3270 format and parses a3270 JSON strings")
public void testA3270JsonFormatCompatibility() {
// String representative of a3270 saved_hosts preference JSON
String a3270Json = "[\n" +
" {\n" +
" \"id\": \"android-uuid-1\",\n" +
" \"name\": \"Dallas Mainframe\",\n" +
" \"host\": \"dallas.ibm.com\",\n" +
" \"port\": 2323,\n" +
" \"model\": 4,\n" +
" \"dynamicRows\": 62,\n" +
" \"dynamicCols\": 160,\n" +
" \"luName\": \"DAL01\",\n" +
" \"autoConnect\": false,\n" +
" \"hostType\": \"TSO\",\n" +
" \"useTls\": true,\n" +
" \"tlsVerifyCert\": false,\n" +
" \"tn3270e\": true,\n" +
" \"graphicsMode\": \"BOTH\",\n" +
" \"codePage\": \"037\",\n" +
" \"proxyType\": \"SOCKS5\",\n" +
" \"proxyHost\": \"10.20.30.40\",\n" +
" \"proxyPort\": 9050,\n" +
" \"proxyUsername\": \"dallasuser\",\n" +
" \"proxyPassword\": \"dallaspass\"\n" +
" }\n" +
"]";
List<SavedHost> parsed = HostStorage.parseHostsJson(a3270Json);
assertEquals(1, parsed.size());
SavedHost h = parsed.get(0);
assertEquals("android-uuid-1", h.getId());
assertEquals("Dallas Mainframe", h.getName());
assertEquals("dallas.ibm.com", h.getHost());
assertEquals(2323, h.getPort());
assertEquals(4, h.getModel());
assertTrue(h.isUseTls());
assertFalse(h.isTlsVerifyCert());
assertEquals("SOCKS5", h.getProxyType());
assertEquals("10.20.30.40", h.getProxyHost());
assertEquals(9050, h.getProxyPort());
assertEquals("dallasuser", h.getProxyUsername());
assertEquals("dallaspass", h.getProxyPassword());
// Roundtrip serialization
String serialized = HostStorage.serializeHostsJson(parsed);
List<SavedHost> roundtripped = HostStorage.parseHostsJson(serialized);
assertEquals(1, roundtripped.size());
SavedHost r = roundtripped.get(0);
assertEquals(h.getId(), r.getId());
assertEquals(h.getProxyType(), r.getProxyType());
assertEquals(h.getProxyHost(), r.getProxyHost());
assertEquals(h.getProxyPort(), r.getProxyPort());
assertEquals(h.getProxyUsername(), r.getProxyUsername());
assertEquals(h.getProxyPassword(), r.getProxyPassword());
}
@Test
@DisplayName("ConnectDialog loads profile proxy and applies per-connection proxy to ConnectionConfig")
public void testConnectDialogProxyHandling() {
if (!GraphicsEnvironment.isHeadless()) {
ConnectDialog dialog = new ConnectDialog((Frame) null);
dialog.setInitialHost("test.mainframe.com");
dialog.setInitialPort(23);
dialog.setInitialProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", 1080, "testuser", "testpass");
assertEquals(ConnectionConfig.ProxyType.SOCKS5, dialog.getSelectedProxyType());
assertEquals("127.0.0.1", dialog.getProxyHost());
assertEquals(1080, dialog.getProxyPort());
assertEquals("testuser", dialog.getProxyUsername());
assertEquals("testpass", dialog.getProxyPassword());
// Loading a profile updates dialog proxy fields
SavedHost profile = new SavedHost(
"p1", "Profile 1", "prod.ibm.net", 992, 4, 43, 80, "", false,
"TSO", true, true, true, "BOTH", "037",
"HTTP", "proxy.corp.net", 8080, "corpuser", "corppass"
);
dialog.loadProfile(profile);
assertEquals(ConnectionConfig.ProxyType.HTTP, dialog.getSelectedProxyType());
assertEquals("proxy.corp.net", dialog.getProxyHost());
assertEquals(8080, dialog.getProxyPort());
assertEquals("corpuser", dialog.getProxyUsername());
assertEquals("corppass", dialog.getProxyPassword());
dialog.dispose();
}
}
@Test
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
public void testSavedHostGraphicsModeSerializationRoundtrip() {
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
SavedHost host = new SavedHost(
UUID.randomUUID().toString(),
"Gfx " + gm.name(),
"test.host.com",
23,
4,
24,
80,
"",
false,
"TSO",
false,
false,
true,
gm.name(),
"037",
"NONE",
"",
0,
"",
""
);
String json = HostStorage.serializeHostsJson(List.of(host));
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
assertEquals(1, parsed.size());
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
"ConnectionConfig must reflect GraphicsMode." + gm.name());
}
}
}
@@ -0,0 +1,298 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.*;
public class BehaviorSettingsTest {
private boolean origAutoReconnect;
private int origReconnectMaxRetries;
private boolean origInputMask;
private String origInputMaskChar;
private boolean origBlockPaste;
@BeforeEach
public void setUp() {
origAutoReconnect = Settings.getAutoConnectAutoReconnect();
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
origInputMask = Settings.getInputMask();
origInputMaskChar = Settings.getInputMaskChar();
origBlockPaste = Settings.getBlockPaste();
}
@AfterEach
public void tearDown() {
Settings.setAutoConnectAutoReconnect(origAutoReconnect);
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
Settings.setInputMask(origInputMask);
Settings.setInputMaskChar(origInputMaskChar);
Settings.setBlockPaste(origBlockPaste);
}
@Test
@DisplayName("Auto-reconnect settings get/set and alias consistency")
public void testAutoReconnectSettingsPersistence() {
Settings.setAutoConnectAutoReconnect(true);
assertTrue(Settings.getAutoConnectAutoReconnect());
assertTrue(Settings.getAutoReconnect());
Settings.setAutoReconnect(false);
assertFalse(Settings.getAutoConnectAutoReconnect());
assertFalse(Settings.getAutoReconnect());
Settings.setAutoConnectReconnectMaxRetries(8);
assertEquals(8, Settings.getAutoConnectReconnectMaxRetries());
}
@Test
@DisplayName("Input mask settings get/set and defaults")
public void testInputMaskSettingsPersistence() {
Settings.setInputMask(false);
assertFalse(Settings.getInputMask());
assertFalse(Settings.getInputMaskEnabled());
Settings.setInputMaskEnabled(true);
assertTrue(Settings.getInputMask());
assertTrue(Settings.getInputMaskEnabled());
Settings.setInputMaskChar("#");
assertEquals("#", Settings.getInputMaskChar());
Settings.setInputMaskChar("*");
assertEquals("*", Settings.getInputMaskChar());
// Empty string should fall back to '*'
Settings.setInputMaskChar("");
assertEquals("*", Settings.getInputMaskChar());
}
@Test
@DisplayName("Block paste setting defaults to true and binds to SettingsDialog")
public void testBlockPasteSettingAndDialogBinding() throws Exception {
// Default must be true
assertTrue(Settings.getBlockPaste(), "Settings.getBlockPaste() should default to true");
Settings.setBlockPaste(false);
assertFalse(Settings.getBlockPaste());
Settings.setBlockPaste(true);
assertTrue(Settings.getBlockPaste());
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
Field blockPasteField = SettingsDialog.class.getDeclaredField("blockPasteCheck");
blockPasteField.setAccessible(true);
JCheckBox blockPasteCheck = (JCheckBox) blockPasteField.get(dialog);
assertNotNull(blockPasteCheck, "blockPasteCheck must exist in SettingsDialog");
assertEquals("Block paste", blockPasteCheck.getText());
assertEquals(Settings.getBlockPaste(), blockPasteCheck.isSelected());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("INI export and load preserves autoReconnect and inputMask")
public void testIniExportAndLoad() throws Exception {
Settings.setAutoConnectAutoReconnect(true);
Settings.setAutoConnectReconnectMaxRetries(12);
Settings.setInputMask(false);
Settings.setInputMaskChar("@");
File tempFile = File.createTempFile("j3270_behavior_test", ".ini");
tempFile.deleteOnExit();
Settings.exportToIniFile(tempFile.getAbsolutePath());
// Reset to different values
Settings.setAutoConnectAutoReconnect(false);
Settings.setAutoConnectReconnectMaxRetries(3);
Settings.setInputMask(true);
Settings.setInputMaskChar("*");
// Load back from INI
Settings.loadFromIniFile(tempFile.getAbsolutePath());
assertTrue(Settings.getAutoConnectAutoReconnect());
assertEquals(12, Settings.getAutoConnectReconnectMaxRetries());
assertFalse(Settings.getInputMask());
assertEquals("@", Settings.getInputMaskChar());
tempFile.delete();
}
@Test
@DisplayName("TerminalPanel reloads input mask settings correctly")
public void testTerminalPanelInputMaskReload() {
try {
TerminalPanel panel = new TerminalPanel();
Settings.setInputMask(false);
Settings.setInputMaskChar("$");
panel.reloadSettings();
assertFalse(panel.isInputMaskEnabled());
assertEquals("$", panel.getInputMaskChar());
panel.setInputMaskEnabled(true);
assertTrue(panel.isInputMaskEnabled());
panel.setInputMaskChar("#");
assertEquals("#", panel.getInputMaskChar());
} catch (HeadlessException ignored) {
// Safe fallback for headless runner
}
}
@Test
@DisplayName("SettingsDialog contains Auto-Reconnect checkbox and Input Mask controls in Behavior panel")
public void testSettingsDialogBehaviorControls() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// In automated/headless environments, JFrame cannot be initialized
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Access private fields in SettingsDialog to verify component bindings
Field autoRecField = SettingsDialog.class.getDeclaredField("autoReconnectCheck");
autoRecField.setAccessible(true);
JCheckBox autoReconnectCheck = (JCheckBox) autoRecField.get(dialog);
assertNotNull(autoReconnectCheck, "autoReconnectCheck must exist in SettingsDialog");
assertEquals("Auto-Reconnect on Disconnect", autoReconnectCheck.getText());
assertEquals(Settings.getAutoConnectAutoReconnect(), autoReconnectCheck.isSelected());
Field inputMaskCheckField = SettingsDialog.class.getDeclaredField("inputMaskCheck");
inputMaskCheckField.setAccessible(true);
JCheckBox inputMaskCheck = (JCheckBox) inputMaskCheckField.get(dialog);
assertNotNull(inputMaskCheck, "inputMaskCheck must exist in SettingsDialog");
assertEquals(Settings.getInputMask(), inputMaskCheck.isSelected());
Field inputMaskCharField = SettingsDialog.class.getDeclaredField("inputMaskCharField");
inputMaskCharField.setAccessible(true);
JTextField maskCharField = (JTextField) inputMaskCharField.get(dialog);
assertNotNull(maskCharField, "inputMaskCharField must exist in SettingsDialog");
assertEquals(Settings.getInputMaskChar(), maskCharField.getText());
assertEquals(inputMaskCheck.isSelected(), maskCharField.isEnabled());
// Test interaction: unchecking inputMask disables character field
inputMaskCheck.setSelected(false);
for (java.awt.event.ActionListener al : inputMaskCheck.getActionListeners()) {
al.actionPerformed(new java.awt.event.ActionEvent(inputMaskCheck, java.awt.event.ActionEvent.ACTION_PERFORMED, ""));
}
assertFalse(maskCharField.isEnabled());
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("SettingsDialog tabs are organized into Appearance, Behavior, Connection, and Entry Assist")
public void testSettingsDialogReorganizedTabsAndGrouping() throws Exception {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
// Find top-level JTabbedPane
JTabbedPane mainTabs = null;
for (Component c : dialog.getContentPane().getComponents()) {
if (c instanceof JTabbedPane) {
mainTabs = (JTabbedPane) c;
break;
}
}
assertNotNull(mainTabs, "Main JTabbedPane must exist");
assertEquals(4, mainTabs.getTabCount(), "Must have 4 main tabs");
assertEquals("Appearance", mainTabs.getTitleAt(0));
assertEquals("Behavior", mainTabs.getTitleAt(1));
assertEquals("Connection", mainTabs.getTitleAt(2));
assertEquals("Entry Assist", mainTabs.getTitleAt(3));
// Verify Appearance subtabs: Display & Font, Colors
Component appComp = mainTabs.getComponentAt(0);
assertTrue(appComp instanceof JPanel);
JTabbedPane appTabs = findTabbedPane((JPanel) appComp);
assertNotNull(appTabs, "Appearance panel must have subtabs");
assertEquals(2, appTabs.getTabCount());
assertEquals("Display & Font", appTabs.getTitleAt(0));
assertEquals("Colors", appTabs.getTitleAt(1));
// Verify Behavior subtabs: General, Keymap
Component behComp = mainTabs.getComponentAt(1);
assertTrue(behComp instanceof JPanel);
JTabbedPane behTabs = findTabbedPane((JPanel) behComp);
assertNotNull(behTabs, "Behavior panel must have subtabs");
assertEquals(2, behTabs.getTabCount());
assertEquals("General", behTabs.getTitleAt(0));
assertEquals("Keymap", behTabs.getTitleAt(1));
// Verify controls exist in SettingsDialog
Field cursorStyleField = SettingsDialog.class.getDeclaredField("cursorStyleBox");
cursorStyleField.setAccessible(true);
assertNotNull(cursorStyleField.get(dialog), "cursorStyleBox must exist");
Field crosshairField = SettingsDialog.class.getDeclaredField("crosshairRulerCheck");
crosshairField.setAccessible(true);
assertNotNull(crosshairField.get(dialog), "crosshairRulerCheck must exist");
Field fourColorField = SettingsDialog.class.getDeclaredField("fourColorOverrideCheck");
fourColorField.setAccessible(true);
assertNotNull(fourColorField.get(dialog), "fourColorOverrideCheck must exist");
Field numericLockField = SettingsDialog.class.getDeclaredField("numericFieldLockCheck");
numericLockField.setAccessible(true);
assertNotNull(numericLockField.get(dialog), "numericFieldLockCheck must exist");
Field autoSkipField = SettingsDialog.class.getDeclaredField("autoSkipCheck");
autoSkipField.setAccessible(true);
assertNotNull(autoSkipField.get(dialog), "autoSkipCheck must exist");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
private static JTabbedPane findTabbedPane(Container container) {
for (Component c : container.getComponents()) {
if (c instanceof JTabbedPane) {
return (JTabbedPane) c;
}
if (c instanceof Container) {
JTabbedPane nested = findTabbedPane((Container) c);
if (nested != null) return nested;
}
}
return null;
}
}
@@ -15,17 +15,23 @@ public class FontSettingsPersistenceTest {
private int originalFontSize; private int originalFontSize;
private String originalFontFamily; private String originalFontFamily;
private int originalUiFontSize;
private String originalUiFontFamily;
@BeforeEach @BeforeEach
public void setup() { public void setup() {
originalFontSize = Settings.getFontSize(); originalFontSize = Settings.getFontSize();
originalFontFamily = Settings.getFontFamily(); originalFontFamily = Settings.getFontFamily();
originalUiFontSize = Settings.getUiFontSize();
originalUiFontFamily = Settings.getUiFontFamily();
} }
@AfterEach @AfterEach
public void tearDown() { public void tearDown() {
Settings.setFontSize(originalFontSize); Settings.setFontSize(originalFontSize);
Settings.setFontFamily(originalFontFamily); Settings.setFontFamily(originalFontFamily);
Settings.setUiFontSize(originalUiFontSize);
Settings.setUiFontFamily(originalUiFontFamily);
} }
@Test @Test
@@ -39,6 +45,76 @@ public class FontSettingsPersistenceTest {
assertEquals(18, Settings.getFontSize()); 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 @Test
public void testTerminalPanelResizeDoesNotResetFontSize() { public void testTerminalPanelResizeDoesNotResetFontSize() {
try { try {
@@ -77,4 +153,57 @@ public class FontSettingsPersistenceTest {
// In headless environment without display, Settings persistence is verified above // 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,265 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Cursor;
import java.awt.HeadlessException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class SelectionHandlesAndTrimTest {
private boolean origRemainAfterEdit;
private boolean origSizingHandles;
@BeforeEach
public void setUp() {
origRemainAfterEdit = Settings.getTrimRectRemainAfterEdit();
origSizingHandles = Settings.getTrimRectSizingHandles();
}
@AfterEach
public void tearDown() {
Settings.setTrimRectRemainAfterEdit(origRemainAfterEdit);
Settings.setTrimRectSizingHandles(origSizingHandles);
}
private TerminalPanel createTestPanel() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
panel.setClient(client);
return panel;
}
@Test
@DisplayName("Trailing whitespace is trimmed on copied lines in block selection mode")
public void testBlockSelectionTrailingWhitespaceTrim() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
// Populate row 0: "HELLO "
String text0 = "HELLO";
for (int i = 0; i < text0.length(); i++) {
sb.getCell(i).ucs4 = text0.charAt(i);
}
// Populate row 1: "WORLD 123 "
String text1 = "WORLD 123";
for (int i = 0; i < text1.length(); i++) {
sb.getCell(cols + i).ucs4 = text1.charAt(i);
}
// Select rectangle from (row 0, col 0) to (row 1, col 20)
panel.setBlockSelectMode(true);
panel.setSelectionRange(0, cols + 20);
String copied = panel.getSelectedText();
// Should be "HELLO\nWORLD 123" without the trailing spaces up to col 20
assertEquals("HELLO\nWORLD 123", copied);
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Trailing whitespace is trimmed on copied lines in stream selection mode")
public void testStreamSelectionTrailingWhitespaceTrim() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
// Populate row 0: "LINE1 "
String text0 = "LINE1";
for (int i = 0; i < text0.length(); i++) {
sb.getCell(i).ucs4 = text0.charAt(i);
}
// Populate row 1: "LINE2"
String text1 = "LINE2";
for (int i = 0; i < text1.length(); i++) {
sb.getCell(cols + i).ucs4 = text1.charAt(i);
}
panel.setBlockSelectMode(false);
panel.setTrimRectSizingHandles(false);
// Select from row 0, col 0 to row 1, col 10
panel.setSelectionRange(0, cols + 10);
String copied = panel.getSelectedText();
assertEquals("LINE1\nLINE2", copied);
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Drag handle detection identifies 8 resize handles, MOVE inside box, and NONE outside")
public void testDragHandleDetection() {
try {
TerminalPanel panel = createTestPanel();
panel.setBlockSelectMode(true);
panel.setTrimRectSizingHandles(true);
// Without selection, should return NONE
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(100, 100));
// Select rectangle: rows 2 to 5, cols 10 to 20
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
panel.setSelectionRange(2 * cols + 10, 5 * cols + 20);
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
int cw = panel.cellWidth;
int ch = panel.cellHeight;
int x1 = ox + 10 * cw;
int y1 = oy + 2 * ch;
int x2 = ox + 21 * cw;
int y2 = oy + 6 * ch;
int xm = (x1 + x2) / 2;
int ym = (y1 + y2) / 2;
// Test 8 handles
assertEquals(TerminalPanel.DragHandle.NW, panel.getDragHandleAt(x1, y1));
assertEquals(TerminalPanel.DragHandle.N, panel.getDragHandleAt(xm, y1));
assertEquals(TerminalPanel.DragHandle.NE, panel.getDragHandleAt(x2, y1));
assertEquals(TerminalPanel.DragHandle.E, panel.getDragHandleAt(x2, ym));
assertEquals(TerminalPanel.DragHandle.SE, panel.getDragHandleAt(x2, y2));
assertEquals(TerminalPanel.DragHandle.S, panel.getDragHandleAt(xm, y2));
assertEquals(TerminalPanel.DragHandle.SW, panel.getDragHandleAt(x1, y2));
assertEquals(TerminalPanel.DragHandle.W, panel.getDragHandleAt(x1, ym));
// Test inside box -> MOVE
assertEquals(TerminalPanel.DragHandle.MOVE, panel.getDragHandleAt(xm, ym));
// Test far outside -> NONE
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(0, 0));
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(x2 + 50, y2 + 50));
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Cursor mapping for all drag handles")
public void testCursorForDragHandles() {
try {
TerminalPanel panel = createTestPanel();
assertEquals(Cursor.NW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NW).getType());
assertEquals(Cursor.N_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.N).getType());
assertEquals(Cursor.NE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NE).getType());
assertEquals(Cursor.E_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.E).getType());
assertEquals(Cursor.SE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SE).getType());
assertEquals(Cursor.S_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.S).getType());
assertEquals(Cursor.SW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SW).getType());
assertEquals(Cursor.W_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.W).getType());
assertEquals(Cursor.MOVE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.MOVE).getType());
assertEquals(Cursor.DEFAULT_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NONE).getType());
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("trimRectRemainAfterEdit controls whether selection is retained after copy")
public void testTrimRectRemainAfterEdit() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
sb.getCell(0).ucs4 = 'A';
panel.setSelectionRange(0, 5);
assertTrue(panel.hasSelection());
// By default (false), copySelection() should clear the selection
Settings.setTrimRectRemainAfterEdit(false);
panel.copySelection();
assertFalse(panel.hasSelection(), "Selection should be cleared after copy when remainAfterEdit is false");
// When set to true, copySelection() should preserve the selection
Settings.setTrimRectRemainAfterEdit(true);
panel.setSelectionRange(0, 5);
assertTrue(panel.hasSelection());
panel.copySelection();
assertTrue(panel.hasSelection(), "Selection should remain active after copy when remainAfterEdit is true");
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("cutSelection clears unprotected cells, sets FA_MODIFY, and respects remainAfterEdit")
public void testCutSelectionBehavior() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
// Field at 0: Unprotected field
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
String data = "TESTDATA";
for (int i = 0; i < data.length(); i++) {
sb.getCell(1 + i).ucs4 = data.charAt(i);
}
// Select the field contents
panel.setSelectionRange(1, 8);
assertTrue(panel.hasSelection());
Settings.setTrimRectRemainAfterEdit(false);
panel.cutSelection();
// Characters should be replaced with spaces
for (int i = 1; i <= 8; i++) {
assertEquals(' ', sb.getCell(i).ucs4, "Cell " + i + " should be erased to space");
}
// FA_MODIFY should remain or be set
assertTrue((sb.getCell(0).ec & FA_MODIFY) != 0, "FA_MODIFY should be set on field attribute");
assertFalse(panel.hasSelection(), "Selection should be cleared when remainAfterEdit is false");
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("KeyBindings exportKmp generates HoD format and round-trips through parseKmp")
public void testKeyBindingsExportAndParseRoundTrip() throws Exception {
Map<String, String> keymap = new LinkedHashMap<>();
keymap.put("ENTER", "ENTER");
keymap.put("shift TAB", "TAB");
keymap.put("CLEAR", "PAUSE");
keymap.put("PF1", "F1");
keymap.put("PF13", "shift F1");
keymap.put("ESCAPE", "ESCAPE");
StringWriter writer = new StringWriter();
KeyBindings.exportKmp(writer, keymap);
String exported = writer.toString();
assertTrue(exported.contains("[KeyRemap]"), "Exported KMP must contain [KeyRemap] section header");
assertTrue(exported.contains("[enter]"), "Exported KMP must contain HoD mnemonics");
assertTrue(exported.contains("[backtab]"), "Exported KMP must contain [backtab]");
// Roundtrip parse
Map<String, String> parsed = KeyBindings.parseKmp(new StringReader(exported));
assertNotNull(parsed);
assertEquals("ENTER", parsed.get("ENTER"));
assertEquals("TAB", parsed.get("shift TAB"));
assertEquals("PAUSE", parsed.get("CLEAR"));
assertEquals("F1", parsed.get("PF1"));
assertEquals("shift F1", parsed.get("PF13"));
assertEquals("ESCAPE", parsed.get("ESCAPE"));
}
}
@@ -59,4 +59,49 @@ public class StatusBarTest {
// Ignored in headless environments // Ignored in headless environments
} }
} }
@Test
public void testStatusBarXSystemAndXProt() {
try {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_2, false);
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null);
statusBar.updateStatus();
// Initially not locked
assertEquals("", statusBar.getLockStatusText());
// 1. Setup protected field and type into it -> triggers X PROT
client.getScreenBuffer().setFieldAttribute(0, (byte) haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT);
client.getScreenBuffer().setCursorAddress(1);
client.typeCharacter('A');
statusBar.updateStatus();
assertEquals("X PROT", statusBar.getLockStatusText());
// 2. Reset -> clears X PROT
client.reset();
statusBar.updateStatus();
assertEquals("", statusBar.getLockStatusText());
// 3. Send Enter -> triggers X SYSTEM
client.sendEnter();
statusBar.updateStatus();
assertEquals("X SYSTEM", statusBar.getLockStatusText());
// 4. Host unlocks keyboard
client.getInputProcessor().setKeyboardLocked(false);
statusBar.updateStatus();
assertEquals("", statusBar.getLockStatusText());
// 5. Subsequent Enter -> triggers X SYSTEM again
client.sendEnter();
statusBar.updateStatus();
assertEquals("X SYSTEM", statusBar.getLockStatusText());
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
} }
@@ -0,0 +1,101 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.HeadlessException;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TerminalPanelSelectionTest {
@Test
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
String headerText = " ISPF PRIMARY OPTION MENU";
for (int i = 0; i < headerText.length(); i++) {
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
sb.getCell(1 + i).ec = (byte) (0x40 + i);
}
panel.setClient(client);
// Cell dimensions in panel
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener[] mouseListeners = panel.getMouseListeners();
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
assertTrue(mouseListeners.length > 0);
assertTrue(motionListeners.length > 0);
MouseListener ml = mouseListeners[0];
MouseMotionListener mml = motionListeners[0];
// 1. Mouse Pressed at cell (row 0, col 2)
int startX = ox + 2 * cellW + cellW / 2;
int startY = oy + cellH / 2;
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, startX, startY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(pressEvent);
// 2. Drag to cell (row 0, col 15)
int dragX = ox + 15 * cellW + cellW / 2;
int dragY = oy + cellH / 2;
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
mml.mouseDragged(dragEvent);
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
// 3. Mouse Released at cell (row 0, col 15)
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(releaseEvent);
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
String selected = panel.getSelectedText();
assertNotNull(selected);
assertTrue(selected.length() > 0, "Selected text must not be empty");
// 4. A subsequent single click without drag should clear the selection and move the cursor
int clickX = ox + 5 * cellW + cellW / 2;
int clickY = oy + cellH / 2;
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(clickPress);
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(clickRelease);
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
}
@@ -262,4 +262,81 @@ public class ThemeManagerTest {
// Handled in headless CI // Handled in headless CI
} }
} }
@Test
public void testUiFontRetrievalAndDefaults() {
String prevFamily = Settings.getUiFontFamily();
int prevSize = Settings.getUiFontSize();
try {
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(16);
Font uiFont = ThemeManager.getUiFont();
assertEquals("Dialog", uiFont.getFamily());
assertEquals(16, uiFont.getSize());
assertEquals(Font.PLAIN, uiFont.getStyle());
Font boldOffset = ThemeManager.getUiFont(Font.BOLD, 2);
assertEquals(18, boldOffset.getSize());
assertEquals(Font.BOLD, boldOffset.getStyle());
Font monoFont = ThemeManager.getMonospacedUiFont();
assertEquals(Font.MONOSPACED, monoFont.getName());
assertEquals(16, monoFont.getSize());
// Test applyUIManagerDefaults sets font properties
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
assertEquals(uiFont, UIManager.get("Button.font"));
assertEquals(uiFont, UIManager.get("Label.font"));
assertEquals(uiFont, UIManager.get("Menu.font"));
assertEquals(uiFont, UIManager.get("Table.font"));
assertEquals(monoFont, UIManager.get("TextArea.font"));
} finally {
Settings.setUiFontFamily(prevFamily);
Settings.setUiFontSize(prevSize);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
}
}
@Test
public void testLabelFontNotIncrementedOnRepeatedThemeApplication() {
String prevFamily = Settings.getUiFontFamily();
int prevSize = Settings.getUiFontSize();
try {
Settings.setUiFontFamily("Dialog");
Settings.setUiFontSize(16);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
JLabel label = new JLabel("Settings Label");
ThemeManager.applyTheme(label);
assertEquals(16, label.getFont().getSize(), "Initial label font size must match configured uiFontSize");
// Repeated theme applications (e.g. subsequent clicks of 'Apply' in Settings dialog)
// must never increment font size
for (int i = 1; i <= 5; i++) {
ThemeManager.applyTheme(label);
assertEquals(16, label.getFont().getSize(),
"Label font size must remain 16 on subsequent application #" + i);
}
// Test label with custom delta
JLabel headerLabel = new JLabel("Header Label");
headerLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
headerLabel.putClientProperty("uiFontSizeDelta", 3);
ThemeManager.applyTheme(headerLabel);
assertEquals(19, headerLabel.getFont().getSize(), "Custom header label should have base + 3 size");
for (int i = 1; i <= 5; i++) {
ThemeManager.applyTheme(headerLabel);
assertEquals(19, headerLabel.getFont().getSize(),
"Header label font size must remain 19 on subsequent application #" + i);
}
} finally {
Settings.setUiFontFamily(prevFamily);
Settings.setUiFontSize(prevSize);
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
}
}
} }
@@ -0,0 +1,45 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Communications event matching IBM Host On-Demand ECLCommEvent.
*/
public class ECLCommEvent extends ECLEvent {
private final boolean status;
private final int eventType;
public ECLCommEvent(Object object, boolean status) {
super(object);
this.status = status;
this.eventType = status ? 1 : 2;
}
public ECLCommEvent(Object object, int eventType, boolean status) {
super(object);
this.eventType = eventType;
this.status = status;
}
public boolean connected() {
return this.status;
}
public boolean isConnected() {
return this.status;
}
public int getEventType() {
return this.eventType;
}
public ECLConnection GetConnection() {
Object src = GetSource();
if (src instanceof ECLConnection) return (ECLConnection) src;
if (src instanceof haus.nightmare.lib3270j.ecl.ECLConnection) {
return new ECLConnection((haus.nightmare.lib3270j.ecl.ECLConnection) src);
}
return null;
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.ECL;
/**
* Communications notification interface matching IBM Host On-Demand ECLCommNotify.
*/
public interface ECLCommNotify {
void NotifyEvent(ECLConnection conn, boolean connected);
default void NotifyStop(ECLConnection conn, int reason) {}
default void NotifyError(ECLConnection conn, ECLErr err) {}
}
@@ -0,0 +1,79 @@
package com.ibm.eNetwork.ECL;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Communications and connection management facade matching IBM Host On-Demand ECLConnection.
*/
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection implements ECLConstants {
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
public ECLConnection() {
super();
setupListener();
}
public ECLConnection(Properties props) {
super(props);
setupListener();
}
public ECLConnection(String host, int port) {
super(host, port);
setupListener();
}
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) {
super(session);
setupListener();
}
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLConnection delegate) {
super(delegate != null ? delegate.getSession() : null);
setupListener();
}
private void setupListener() {
RegisterCommEvent(new haus.nightmare.lib3270j.ecl.ECLCommListener() {
@Override
public void CommNotifyEvent(haus.nightmare.lib3270j.ecl.ECLCommEvent event) {
boolean conn = event != null && event.isConnected();
for (ECLCommNotify n : commNotifies) {
try {
n.NotifyEvent(ECLConnection.this, conn);
} catch (Exception ignored) {}
}
}
});
}
public void RegisterCommEvent(ECLCommNotify notify) {
if (notify != null && !commNotifies.contains(notify)) {
commNotifies.add(notify);
}
}
public void registerCommEvent(ECLCommNotify notify) { RegisterCommEvent(notify); }
public void UnregisterCommEvent(ECLCommNotify notify) {
if (notify != null) {
commNotifies.remove(notify);
}
}
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
@Override
public void StartCommunication() throws IOException {
super.StartCommunication();
}
@Override
public void StopCommunication() {
super.StopCommunication();
}
}
@@ -0,0 +1,138 @@
package com.ibm.eNetwork.ECL;
/**
* IBM Host Access Class Library (HACL) common constants.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLConstants.
*/
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
// Presentation space logical planes (HoD bitmask values)
int TEXT_PLANE = 1;
int COLOR_PLANE = 2;
int DBCS_PLANE = 4;
int GRID_PLANE = 8;
int FIELD_PLANE = 16;
int EXFIELD_PLANE = 32;
int NLS_PLANE = 64;
int HOST_PLANE = 128;
int RAWTEXT_PLANE = 256;
int COLOR_ATTRIBUTES_PLANE = 512;
int ALL_PLANES = 319;
// Search directions
int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2;
// Field filters
int ALL_FIELDS = 0;
int MODIFIED_FIELD = 1;
int ALPHANUM_FIELD = 2;
int HILIGHT_FIELD = 16;
int PROTECTED_FIELD = 32;
int DISPLAYABLE_FIELD = 64;
int PENDETECTABLE_FIELD = 128;
int UNMODIFIED_FIELD = 256;
int NUMERIC_FIELD = 512;
int LOLIGHT_FIELD = 4096;
int UNPROTECTED_FIELD = 8192;
int NON_DISPLAYABLE_FIELD = 16384;
int NON_PENDETECTABLE_FIELD = 32768;
// Standard Mnemonics Constants
String ENTER_STR = "[enter]";
String CLEAR_STR = "[clear]";
String RESET_STR = "[reset]";
String TAB_STR = "[tab]";
String FWDTAB_STR = "[tab]";
String BACKTAB_STR = "[backtab]";
String BACKSP_STR = "[backspace]";
String DELCHAR_STR = "[delete]";
String INSERT_STR = "[insert]";
String HOME_STR = "[home]";
String NEWLINE_STR = "[newline]";
String ENDLINE_STR = "[eof]";
String ERASEEOF_STR = "[eraseeof]";
String ERASEFLD_STR = "[erasefld]";
String ERASEINPUT_STR = "[erinp]";
String ATTN_STR = "[attn]";
String SYSREQ_STR = "[sysreq]";
String DUP_STR = "[dup]";
String FLDMRK_STR = "[fieldmark]";
String PA1_STR = "[pa1]";
String PA2_STR = "[pa2]";
String PA3_STR = "[pa3]";
String CURUP_STR = "[up]";
String CURDOWN_STR = "[down]";
String CURLEFT_STR = "[left]";
String CURRIGHT_STR = "[right]";
String PAGEUP_STR = "[pageup]";
String PAGEDWN_STR = "[pagedn]";
String COPY_STR = "[copy]";
String PASTE_STR = "[paste]";
String CUT_STR = "[cut]";
String SELECTALL_STR = "[selectall]";
// Function keys F1 - F24
String F1_STR = "[pf1]";
String F2_STR = "[pf2]";
String F3_STR = "[pf3]";
String F4_STR = "[pf4]";
String F5_STR = "[pf5]";
String F6_STR = "[pf6]";
String F7_STR = "[pf7]";
String F8_STR = "[pf8]";
String F9_STR = "[pf9]";
String F10_STR = "[pf10]";
String F11_STR = "[pf11]";
String F12_STR = "[pf12]";
String F13_STR = "[pf13]";
String F14_STR = "[pf14]";
String F15_STR = "[pf15]";
String F16_STR = "[pf16]";
String F17_STR = "[pf17]";
String F18_STR = "[pf18]";
String F19_STR = "[pf19]";
String F20_STR = "[pf20]";
String F21_STR = "[pf21]";
String F22_STR = "[pf22]";
String F23_STR = "[pf23]";
String F24_STR = "[pf24]";
// Shift F1 - F24
String SHIFT_F1_STR = "[Spf1]";
String SHIFT_F2_STR = "[Spf2]";
String SHIFT_F3_STR = "[Spf3]";
String SHIFT_F4_STR = "[Spf4]";
String SHIFT_F5_STR = "[Spf5]";
String SHIFT_F6_STR = "[Spf6]";
String SHIFT_F7_STR = "[Spf7]";
String SHIFT_F8_STR = "[Spf8]";
String SHIFT_F9_STR = "[Spf9]";
String SHIFT_F10_STR = "[Spf10]";
String SHIFT_F11_STR = "[Spf11]";
String SHIFT_F12_STR = "[Spf12]";
String SHIFT_F13_STR = "[Spf13]";
String SHIFT_F14_STR = "[Spf14]";
String SHIFT_F15_STR = "[Spf15]";
String SHIFT_F16_STR = "[Spf16]";
String SHIFT_F17_STR = "[Spf17]";
String SHIFT_F18_STR = "[Spf18]";
String SHIFT_F19_STR = "[Spf19]";
String SHIFT_F20_STR = "[Spf20]";
// Field attributes
short ATTR_PROTECTED = 32;
short ATTR_ALPHANUM = 16;
short ATTR_DISPLAY1 = 8;
short ATTR_DISPLAY2 = 4;
short ATTR_MDT = 1;
// Extended attributes
short EXT_ATTR_3270_UNDERLINE = 192;
short EXT_ATTR_3270_REVERSE = 128;
short EXT_ATTR_3270_BLINK = 64;
short EXT_ATTR_3270_EBCDIC = 0;
// Timeout
int NO_TIMEOUT = -1;
}
@@ -0,0 +1,54 @@
package com.ibm.eNetwork.ECL;
/**
* IBM Host Access Class Library (HACL) ECLErr checked exception facade.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLErr.
*/
public class ECLErr extends haus.nightmare.lib3270j.ecl.ECLErr {
private static final long serialVersionUID = 1L;
public ECLErr() {
super();
}
public ECLErr(String text) {
super(text);
}
public ECLErr(String location, String msgNum) {
super(location, msgNum);
}
public ECLErr(String location, String msgNum, String insert) {
super(location, msgNum, insert);
}
public ECLErr(String location, String msgNum, String insert1, String insert2) {
super(location, msgNum, insert1, insert2);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
super(location, msgNum, insert1, insert2, log);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
super(location, msgNum, insert1, insert2, insert3);
}
public ECLErr(String location, String msgNum, int reasonCode) {
super(location, msgNum, reasonCode);
}
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
super(location, msgNum, insert1, reasonCode);
}
public ECLErr(Throwable cause) {
super(cause);
}
public ECLErr(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,137 @@
package com.ibm.eNetwork.ECL;
/**
* Discrete 3270 field facade matching IBM Host On-Demand ECLField.
*/
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements ECLConstants {
private final haus.nightmare.lib3270j.ecl.ECLField delegate;
public ECLField(haus.nightmare.lib3270j.ecl.ECLField delegate) {
super(delegate != null ? delegate.getPS() : null,
delegate != null ? delegate.getStart() : 0,
delegate != null ? delegate.getDataStart() : 0,
delegate != null ? delegate.getEnd() : 0,
delegate != null ? delegate.getLength() : 0,
(byte) (delegate != null ? delegate.getAttribute() : 0));
this.delegate = delegate;
}
public int GetStart() {
return delegate != null ? delegate.GetStart() + 1 : 1;
}
public int getStart() { return GetStart(); }
public int GetStartRow() {
return delegate != null ? delegate.GetStartRow() + 1 : 1;
}
public int getStartRow() { return GetStartRow(); }
public int GetStartCol() {
return delegate != null ? delegate.GetStartCol() + 1 : 1;
}
public int getStartCol() { return GetStartCol(); }
public int GetEnd() {
return delegate != null ? delegate.GetEnd() + 1 : 1;
}
public int getEnd() { return GetEnd(); }
public int GetEndRow() {
return delegate != null ? delegate.GetEndRow() + 1 : 1;
}
public int getEndRow() { return GetEndRow(); }
public int GetEndCol() {
return delegate != null ? delegate.GetEndCol() + 1 : 1;
}
public int getEndCol() { return GetEndCol(); }
public int GetLength() {
return delegate != null ? delegate.GetLength() : 0;
}
public int getLength() { return GetLength(); }
public String GetString() {
return delegate != null ? delegate.getString() : "";
}
public String getString() { return GetString(); }
public int GetString(char[] buffer, int len) throws ECLErr {
if (buffer == null) throw new ECLErr("ECLField:GetString", "ECL0011", "1");
String text = GetString();
int count = Math.min(len, Math.min(buffer.length, text.length()));
for (int i = 0; i < count; i++) {
buffer[i] = text.charAt(i);
}
return count;
}
public void SetString(String text) throws ECLErr {
if (delegate != null) {
delegate.setText(text);
}
}
public void SetText(String text) throws ECLErr {
SetString(text);
}
public void setText(String text) {
if (delegate != null) {
delegate.setText(text);
}
}
public boolean IsModified() {
return delegate != null && delegate.isModified();
}
public boolean isModified() { return IsModified(); }
public boolean IsProtected() {
return delegate != null && delegate.isProtected();
}
public boolean isProtected() { return IsProtected(); }
public boolean IsNumeric() {
return delegate != null && delegate.isNumeric();
}
public boolean isNumeric() { return IsNumeric(); }
public boolean IsHighIntensity() {
return delegate != null && delegate.isHighIntensity();
}
public boolean isHighIntensity() { return IsHighIntensity(); }
public boolean IsPenDetectable() {
return delegate != null && delegate.isPenDetectable();
}
public boolean isPenDetectable() { return IsPenDetectable(); }
public boolean IsDisplay() {
return delegate != null && delegate.isDisplay();
}
public boolean isDisplay() { return IsDisplay(); }
public short GetAttribute() {
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
}
public short getAttribute() { return GetAttribute(); }
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
}
@@ -0,0 +1,113 @@
package com.ibm.eNetwork.ECL;
import java.util.Vector;
/**
* Collection of 3270 fields matching IBM Host On-Demand ECLFieldList.
*/
public class ECLFieldList extends Vector<ECLField> implements ECLConstants {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLFieldList delegate;
private final ECLPS ps;
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate, ECLPS ps) {
this.delegate = delegate;
this.ps = ps;
refreshFields();
}
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate) {
this(delegate, null);
}
private void refreshFields() {
clear();
if (delegate != null) {
delegate.refresh();
for (haus.nightmare.lib3270j.ecl.ECLField f : delegate.getFields()) {
add(new ECLField(f));
}
}
}
public void Refresh() {
refreshFields();
}
public void Refresh(int planes) throws ECLErr {
refreshFields();
}
public void refresh() {
refreshFields();
}
public int GetFieldCount() {
return size();
}
public int getFieldCount() { return GetFieldCount(); }
public ECLField GetFirstField() {
return GetFirstField(0);
}
public ECLField GetFirstField(int filter) {
for (ECLField f : this) {
if (matchesFilter(f, filter)) return f;
}
return null;
}
public ECLField GetNextField(ECLField current) {
return GetNextField(current, 0);
}
public ECLField GetNextField(ECLField current, int filter) {
if (current == null) return GetFirstField(filter);
int idx = indexOf(current);
if (idx >= 0) {
for (int i = idx + 1; i < size(); i++) {
ECLField f = get(i);
if (matchesFilter(f, filter)) return f;
}
}
return null;
}
public ECLField FindField(int pos) throws ECLErr {
if (ps != null && (pos < 1 || pos > ps.GetSize())) {
throw new ECLErr("ECLFieldList:FindField", "ECL0010", "1", String.valueOf(pos));
}
for (ECLField f : this) {
int start = f.GetStart();
int end = f.GetEnd();
if (start <= end) {
if (pos >= start && pos <= end) return f;
} else {
// Wrapped field
if (pos >= start || pos <= end) return f;
}
}
return null;
}
public ECLField FindField(int row, int col) throws ECLErr {
if (ps == null) return null;
int pos = ps.ConvertRowColToPos(row, col);
return FindField(pos);
}
private boolean matchesFilter(ECLField f, int filter) {
if (filter == 0 || filter == ALL_FIELDS) return true;
if ((filter & MODIFIED_FIELD) != 0 && !f.IsModified()) return false;
if ((filter & PROTECTED_FIELD) != 0 && !f.IsProtected()) return false;
if ((filter & UNPROTECTED_FIELD) != 0 && f.IsProtected()) return false;
if ((filter & NUMERIC_FIELD) != 0 && !f.IsNumeric()) return false;
if ((filter & HILIGHT_FIELD) != 0 && !f.IsHighIntensity()) return false;
if ((filter & DISPLAYABLE_FIELD) != 0 && !f.IsDisplay()) return false;
return true;
}
}
@@ -0,0 +1,39 @@
package com.ibm.eNetwork.ECL;
import java.util.Properties;
/**
* Host Print Session facade matching IBM Host On-Demand ECLHostPrintSession.
*/
public class ECLHostPrintSession extends ECLSession {
public static final String SESSION_PRINT_SESSION_LABEL_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_TYPE_DEFAULT = "5";
public static final String SESSION_PRINT_SESSION_HOST_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_HOST_PORT_DEFAULT = "23";
public static final String SESSION_PRINT_SESSION_LU_NAME_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_AUTOCONNECT_DEFAULT = "true";
public static final String SESSION_PRINT_SESSION_AUTORECONNECT_DEFAULT = "true";
public static final String SESSION_LU_TYPE = "printerLUType";
public ECLHostPrintSession(Properties properties) throws ECLErr {
super(properties);
}
public ECLHostPrintSession() throws ECLErr {
super();
}
public void cancelPrintJob() {
ECLPS ps = GetPS();
if (ps != null) {
try {
ps.SendKeys("[cancelpjob]");
} catch (Exception ignored) {}
}
}
public void CancelPrintJob() {
cancelPrintJob();
}
}
@@ -0,0 +1,130 @@
package com.ibm.eNetwork.ECL;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Operator Information Area facade matching IBM Host On-Demand ECLOIA.
*/
public class ECLOIA implements ECLConstants {
private final haus.nightmare.lib3270j.ecl.ECLOIA delegate;
private final List<ECLOIANotify> oiaNotifies = new CopyOnWriteArrayList<>();
public ECLOIA(haus.nightmare.lib3270j.ecl.ECLOIA delegate) {
this.delegate = delegate;
if (delegate != null) {
delegate.RegisterOIAEvent(new haus.nightmare.lib3270j.ecl.ECLOIAListener() {
@Override
public void OIANotifyEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent event) {
ECLOIAEvent facadeEvt = new ECLOIAEvent(event);
for (ECLOIANotify n : oiaNotifies) {
try {
n.OIANotifyEvent(facadeEvt);
} catch (Exception ignored) {}
}
}
});
}
}
public void RegisterOIAEvent(ECLOIANotify notify) {
if (notify != null && !oiaNotifies.contains(notify)) {
oiaNotifies.add(notify);
}
}
public void registerOIAEvent(ECLOIANotify notify) { RegisterOIAEvent(notify); }
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
if (delegate != null) {
delegate.RegisterOIAEvent(listener);
}
}
public void UnregisterOIAEvent(ECLOIANotify notify) {
if (notify != null) {
oiaNotifies.remove(notify);
}
}
public void unregisterOIAEvent(ECLOIANotify notify) { UnregisterOIAEvent(notify); }
public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
if (delegate != null) {
delegate.UnregisterOIAEvent(listener);
}
}
public boolean IsAlphanumeric() {
return delegate != null && delegate.IsAlphanumeric();
}
public boolean isAlphanumeric() { return IsAlphanumeric(); }
public boolean IsNumeric() {
return delegate != null && delegate.IsNumeric();
}
public boolean isNumeric() { return IsNumeric(); }
public boolean IsDBCS() {
return delegate != null && delegate.IsDBCS();
}
public boolean isDBCS() { return IsDBCS(); }
public boolean IsInsertMode() {
return delegate != null && delegate.IsInsertMode();
}
public boolean isInsertMode() { return IsInsertMode(); }
public boolean IsCommError() {
return delegate != null && delegate.IsCommError();
}
public boolean isCommError() { return IsCommError(); }
public boolean IsKeyLocked() {
return delegate != null && delegate.isKeyLocked();
}
public boolean isKeyLocked() { return IsKeyLocked(); }
public boolean IsMessageWaiting() {
return delegate != null && delegate.IsMessageWaiting();
}
public boolean isMessageWaiting() { return IsMessageWaiting(); }
public int GetInputInhibited() {
return delegate != null ? delegate.getInputInhibited() : 0;
}
public int getInputInhibited() { return GetInputInhibited(); }
public int GetAlphanumericType() {
return delegate != null ? delegate.getAlphanumericType() : 0;
}
public int getAlphanumericType() { return GetAlphanumericType(); }
public boolean WaitForInput(long timeout) {
return delegate != null && delegate.WaitForInput(timeout);
}
public boolean WaitForSystemAvailable(long timeout) {
return delegate != null && delegate.WaitForSystemAvailable(timeout);
}
public boolean WaitForAppAvailable(long timeout) {
return delegate != null && delegate.WaitForAppAvailable(timeout);
}
public boolean WaitForTransition(long timeout) {
return delegate != null && delegate.WaitForTransition(timeout);
}
public haus.nightmare.lib3270j.ecl.ECLOIA getDelegate() { return delegate; }
}
@@ -0,0 +1,80 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Event object dispatched on Operator Information Area (ECLOIA) status changes matching IBM HoD.
*/
public class ECLOIAEvent extends ECLEvent {
public String data;
public long newState;
public long changedMask;
private int inputInhibited = 0;
private int alphanumericType = 0;
private boolean insertMode = false;
public ECLOIAEvent(Object source, long newState, long changedMask, String data) {
super(source);
this.newState = newState;
this.changedMask = changedMask;
this.data = data != null ? data : "";
}
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
boolean insertMode, String statusString) {
super(source);
this.inputInhibited = inputInhibited;
this.alphanumericType = alphanumericType;
this.insertMode = insertMode;
this.data = statusString != null ? statusString : "";
this.newState = inputInhibited;
this.changedMask = eventType;
}
public ECLOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent delegate) {
super(delegate != null ? delegate.getSource() : null);
if (delegate != null) {
this.inputInhibited = delegate.getInputInhibited();
this.alphanumericType = delegate.getAlphanumericType();
this.insertMode = delegate.isInsertMode();
this.data = delegate.getStatusString();
this.newState = delegate.getInputInhibited();
this.changedMask = delegate.getEventType();
}
}
public ECLOIA GetOIA() {
Object src = GetSource();
if (src instanceof ECLOIA) {
return (ECLOIA) src;
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLOIA) {
return new ECLOIA((haus.nightmare.lib3270j.ecl.ECLOIA) src);
}
return null;
}
public ECLOIA getOIA() { return GetOIA(); }
public int GetInputInhibited() { return inputInhibited; }
public int getInputInhibited() { return inputInhibited; }
public int GetAlphanumericType() { return alphanumericType; }
public int getAlphanumericType() { return alphanumericType; }
public boolean IsInsertMode() { return insertMode; }
public boolean isInsertMode() { return insertMode; }
public boolean IsInputInhibited() { return inputInhibited != 0; }
public boolean isInputInhibited() { return IsInputInhibited(); }
public String GetStatusString() { return data; }
public String getStatusString() { return data; }
@Override
public String toString() {
return "ECLOIAEvent: changedMask=0x" + Long.toHexString(this.changedMask) +
", newState=0x" + Long.toHexString(this.newState) + ", data=" + this.data;
}
}
@@ -0,0 +1,17 @@
package com.ibm.eNetwork.ECL;
/**
* Operator Information Area notification interface matching IBM Host On-Demand ECLOIANotify.
*/
public interface ECLOIANotify {
void NotifyEvent(ECLOIA oia, int inhibitReason, int state, String text);
default void NotifyStop(ECLOIA oia, int reason) {}
default void NotifyError(ECLOIA oia, ECLErr err) {}
default void OIANotifyStop(ECLOIA oia, int reason) { NotifyStop(oia, reason); }
default void OIANotifyError(ECLOIA oia, ECLErr err) { NotifyError(oia, err); }
default void OIANotifyEvent(ECLOIAEvent event) {
if (event != null) {
NotifyEvent(event.GetOIA(), event.GetInputInhibited(), 0, "");
}
}
}
@@ -0,0 +1,514 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLPSListener;
import java.io.Serializable;
/**
* Presentation Space facade matching IBM Host On-Demand ECLPS.
* Provides 1-based indexing, method signature parity, and checked ECLErr exception semantics.
*/
public class ECLPS implements ECLConstants, Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLPS delegate;
public ECLPS(haus.nightmare.lib3270j.ecl.ECLPS delegate) {
this.delegate = delegate;
}
public ECLPS(haus.nightmare.lib3270j.ecl.ECLSession session) {
this(session != null ? session.GetPS() : null);
}
public ECLPS(ECLSession session) {
this(session != null ? session.getDelegate() : null);
}
public haus.nightmare.lib3270j.ecl.ECLPS getDelegate() {
return delegate;
}
// ========== Dimensions & Position Conversion (1-based) ==========
public int GetSize() {
return delegate != null ? delegate.GetSize() : 0;
}
public int getSize() { return GetSize(); }
public int GetRows() {
return delegate != null ? delegate.GetRows() : 0;
}
public int getRows() { return GetRows(); }
public int GetCols() {
return delegate != null ? delegate.GetCols() : 0;
}
public int getCols() { return GetCols(); }
public int GetSizeRows() { return GetRows(); }
public int GetSizeCols() { return GetCols(); }
public int ConvertRowColToPos(int row, int col) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertRowColToPos(row, col);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertRowColToPos(int row, int col) throws ECLErr {
return ConvertRowColToPos(row, col);
}
public int ConvertPosToRow(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToRow(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertPosToRow(int pos) throws ECLErr {
return ConvertPosToRow(pos);
}
public int ConvertPosToCol(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToCol(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertPosToCol(int pos) throws ECLErr {
return ConvertPosToCol(pos);
}
public void ConvertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
if (delegate != null) {
try {
delegate.ConvertPosToRowCol(pos, rowCol);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void convertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
ConvertPosToRowCol(pos, rowCol);
}
public int[] ConvertPosToRowCol(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToRowCol(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return new int[]{1, 1};
}
public int[] convertPosToRowCol(int pos) throws ECLErr {
return ConvertPosToRowCol(pos);
}
// ========== Cursor Operations (1-based) ==========
public int GetCursorPos() {
return delegate != null ? delegate.GetCursorPos() : 1;
}
public int getCursorPos() { return GetCursorPos(); }
public int GetCursorRow() {
return delegate != null ? delegate.GetCursorRow() : 1;
}
public int getCursorRow() { return GetCursorRow(); }
public int GetCursorCol() {
return delegate != null ? delegate.GetCursorCol() : 1;
}
public int getCursorCol() { return GetCursorCol(); }
public void SetCursorPos(int pos) throws ECLErr {
if (delegate != null) {
try {
delegate.SetCursorPos(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setCursorPos(int pos) throws ECLErr {
SetCursorPos(pos);
}
public void SetCursorPos(int row, int col) throws ECLErr {
if (delegate != null) {
try {
delegate.SetCursorPos(row, col);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setCursorPos(int row, int col) throws ECLErr {
SetCursorPos(row, col);
}
// ========== GetString & SetString Operations (1-based) ==========
public int GetString(char[] buffer, int len, int pos) throws ECLErr {
return GetString(buffer, len, pos, len);
}
public int GetString(char[] buffer, int len, int pos, int length) throws ECLErr {
if (delegate != null) {
try {
return delegate.GetString(buffer, len, pos, length);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 0;
}
public int GetString(char[] buffer, int len, int row, int col, int length) throws ECLErr {
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
}
public String GetString(int pos, int length) throws ECLErr {
if (delegate != null) {
try {
return delegate.GetString(pos, length);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return "";
}
public String getString(int pos, int length) throws ECLErr {
return GetString(pos, length);
}
public String GetString(int row, int col, int length) throws ECLErr {
return GetString(ConvertRowColToPos(row, col), length);
}
public String getString(int row, int col, int length) throws ECLErr {
return GetString(row, col, length);
}
public void SetString(String text, int pos) throws ECLErr {
if (delegate != null) {
try {
delegate.SetString(text, pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setString(String text, int pos) throws ECLErr {
SetString(text, pos);
}
public void SetString(String text, int row, int col) throws ECLErr {
SetString(text, ConvertRowColToPos(row, col));
}
public void setString(String text, int row, int col) throws ECLErr {
SetString(text, row, col);
}
public void SetString(String text) throws ECLErr {
SetString(text, GetCursorPos());
}
public void setString(String text) throws ECLErr {
SetString(text);
}
public void SetText(String text, int pos) throws ECLErr {
SetString(text, pos);
}
public void SetText(String text, int row, int col) throws ECLErr {
SetString(text, row, col);
}
public void setText(String text, int pos) {
if (delegate != null) delegate.setText(text, pos);
}
public void setText(String text, int row, int col) {
if (delegate != null) delegate.setText(text, row, col);
}
// ========== Search Operations (1-based) ==========
public int SearchText(String text, int dir, int startPos) {
return delegate != null ? delegate.SearchText(text, dir, startPos) : 0;
}
public int SearchText(String text, int dir) {
return delegate != null ? delegate.SearchText(text, dir) : 0;
}
public int SearchText(String text) {
return delegate != null ? delegate.SearchText(text) : 0;
}
public int SearchText(String text, int row, int col, int dir) throws ECLErr {
return SearchText(text, dir, ConvertRowColToPos(row, col));
}
public int SearchString(String text, int dir, int startPos) {
return SearchText(text, dir, startPos);
}
public int SearchString(String text, int dir) {
return SearchText(text, dir);
}
public int SearchString(String text) {
return SearchText(text);
}
public int SearchString(String text, int row, int col, int dir) throws ECLErr {
return SearchText(text, row, col, dir);
}
public int SearchPS(String text) {
return delegate != null ? delegate.SearchPS(text) : 0;
}
public int SearchPS(String text, int startPos) {
return delegate != null ? delegate.SearchPS(text, startPos) : 0;
}
public int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
return delegate != null ? delegate.SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap) : 0;
}
// ========== Keystrokes & Mnemonics ==========
public void SendKeys(String keys) {
if (delegate != null) delegate.SendKeys(keys);
}
public void sendKeys(String keys) { SendKeys(keys); }
public void SendKeys(String keys, int pos) {
if (delegate != null) delegate.SendKeys(keys, pos);
}
public void sendKeys(String keys, int pos) { SendKeys(keys, pos); }
public void SendKeys(String keys, int row, int col) {
if (delegate != null) delegate.SendKeys(keys, row, col);
}
public void sendKeys(String keys, int row, int col) { SendKeys(keys, row, col); }
public void SendCharacters(String keys) {
if (delegate != null) delegate.SendCharacters(keys);
}
public void sendCharacters(String keys) { SendCharacters(keys); }
public void SendCharacters(String keys, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, delayMs);
}
public void sendCharacters(String keys, int delayMs) { SendCharacters(keys, delayMs); }
public void SendCharacters(String keys, int pos, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, pos, delayMs);
}
public void sendCharacters(String keys, int pos, int delayMs) { SendCharacters(keys, pos, delayMs); }
public void SendCharacters(String keys, int row, int col, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, row, col, delayMs);
}
public void sendCharacters(String keys, int row, int col, int delayMs) { SendCharacters(keys, row, col, delayMs); }
public String[] GetSendKeyMnemonics() {
return delegate != null ? delegate.GetSendKeyMnemonics() : new String[0];
}
public String[] getSendKeyMnemonics() { return GetSendKeyMnemonics(); }
// ========== Field Operations ==========
public ECLFieldList GetFieldList() {
haus.nightmare.lib3270j.ecl.ECLFieldList fl = delegate != null ? delegate.GetFieldList() : null;
return new ECLFieldList(fl, this);
}
public ECLFieldList getFieldList() { return GetFieldList(); }
public ECLField GetField(int pos) throws ECLErr {
return GetFieldList().FindField(pos);
}
public ECLField getField(int pos) throws ECLErr { return GetField(pos); }
public ECLField GetField(int row, int col) throws ECLErr {
return GetFieldList().FindField(row, col);
}
public ECLField getField(int row, int col) throws ECLErr { return GetField(row, col); }
public ECLField GetFirstField() {
return GetFieldList().GetFirstField();
}
public ECLField getFirstField() { return GetFirstField(); }
public ECLField GetNextField(ECLField prev) {
return GetFieldList().GetNextField(prev);
}
public ECLField getNextField(ECLField prev) { return GetNextField(prev); }
// ========== Listener Overloads for HoD Interfaces ==========
public void RegisterPSEvent(ECLPSNotify notify) {
if (delegate != null) delegate.RegisterPSEvent(notify);
}
public void registerPSEvent(ECLPSNotify notify) { RegisterPSEvent(notify); }
public void RegisterPSEvent(ECLPSNotify notify, int eventType) {
if (delegate != null) delegate.RegisterPSEvent(notify, eventType);
}
public void registerPSEvent(ECLPSNotify notify, int eventType) { RegisterPSEvent(notify, eventType); }
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc) {
if (delegate != null) delegate.RegisterPSEvent(notify, desc);
}
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { RegisterPSEvent(notify, desc); }
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
if (delegate != null) delegate.RegisterPSEvent(notify, desc, eventType);
}
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { RegisterPSEvent(notify, desc, eventType); }
public void UnregisterPSEvent(ECLPSNotify notify) {
if (delegate != null) delegate.UnregisterPSEvent(notify);
}
public void unregisterPSEvent(ECLPSNotify notify) { UnregisterPSEvent(notify); }
public void RegisterPSEvent(ECLPSListener listener) {
if (delegate != null) delegate.RegisterPSEvent(listener);
}
public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); }
public void UnregisterPSEvent(ECLPSListener listener) {
if (delegate != null) delegate.UnregisterPSEvent(listener);
}
public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); }
// ========== Synchronization & Waits ==========
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
return delegate != null && delegate.WaitForScreen(desc, timeoutMs);
}
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
public boolean WaitForScreen(ECLScreenDesc desc) {
return delegate != null && delegate.WaitForScreen(desc);
}
public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); }
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
return delegate != null && delegate.WaitWhileScreen(desc, timeoutMs);
}
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); }
public boolean WaitWhileScreen(ECLScreenDesc desc) {
return delegate != null && delegate.WaitWhileScreen(desc);
}
public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); }
public boolean WaitForString(String text, long timeoutMs) {
return delegate != null && delegate.WaitForString(text, timeoutMs);
}
public boolean waitForString(String text, long timeoutMs) { return WaitForString(text, timeoutMs); }
public boolean WaitForString(String text) {
return delegate != null && delegate.WaitForString(text);
}
public boolean waitForString(String text) { return WaitForString(text); }
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
return delegate != null && delegate.WaitForString(text, row, col, timeoutMs);
}
public boolean waitForString(String text, int row, int col, long timeoutMs) { return WaitForString(text, row, col, timeoutMs); }
public boolean WaitForCursor(int row, int col, long timeoutMs) {
return delegate != null && delegate.WaitForCursor(row, col, timeoutMs);
}
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
// ========== Screen Update Inquiries ==========
public ECLPSUpdate GetScreen(int startRow, int startCol, int length) throws ECLErr {
return GetScreen(startRow, startCol, length, false);
}
public ECLPSUpdate GetScreen(int startRow, int startCol, int length, boolean fillNulls) throws ECLErr {
int pos = ConvertRowColToPos(startRow, startCol);
char[] buf = new char[length];
GetString(buf, length, pos, length);
if (fillNulls) {
for (int i = 0; i < buf.length; i++) {
if (buf[i] == '\0') buf[i] = ' ';
}
}
int endPos = pos + length - 1;
int endRow = ConvertPosToRow(endPos);
int endCol = ConvertPosToCol(endPos);
return new ECLPSUpdate(this, startRow, startCol, endRow, endCol, new String(buf));
}
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
return delegate.GetScreen(buffer, len, row, col, length, plane);
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.ECL;
/**
* Presentation space notification interface matching IBM Host On-Demand ECLPSNotify.
*/
public interface ECLPSNotify {
void NotifyEvent(ECLPS ps);
default void NotifyStop(ECLPS ps, int reason) {}
default void NotifyError(ECLPS ps, ECLErr err) {}
}
@@ -0,0 +1,90 @@
package com.ibm.eNetwork.ECL;
import java.io.Serializable;
/**
* Conforms to IBM Host On-Demand ECLPSUpdate.
*/
public class ECLPSUpdate implements Serializable {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final int start;
private final int end;
private final boolean fullUpdate;
private final String text;
private final transient ECLPS ps;
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol,
int start, int end, boolean fullUpdate, String text) {
this.ps = ps;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.start = start;
this.end = end;
this.fullUpdate = fullUpdate;
this.text = text != null ? text : "";
this.delegate = null;
}
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, String text) {
this(ps, startRow, startCol, endRow, endCol, 0, 0, false, text);
}
public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, String text) {
this(null, startRow, startCol, endRow, endCol, 0, 0, false, text);
}
public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate) {
this.delegate = delegate;
this.ps = (delegate != null && delegate.getPS() != null) ? new ECLPS(delegate.getPS()) : null;
this.startRow = delegate != null ? delegate.getStartRow() : 0;
this.startCol = delegate != null ? delegate.getStartCol() : 0;
this.endRow = delegate != null ? delegate.getEndRow() : 0;
this.endCol = delegate != null ? delegate.getEndCol() : 0;
this.start = delegate != null ? delegate.getStart() : 0;
this.end = delegate != null ? delegate.getEnd() : 0;
this.fullUpdate = delegate != null && delegate.isFullUpdate();
this.text = delegate != null ? delegate.getText() : "";
}
public ECLPS getPS() { return ps; }
public ECLPS GetPS() { return ps; }
public int getStartRow() { return startRow; }
public int GetStartRow() { return startRow + 1; }
public int getStartCol() { return startCol; }
public int GetStartCol() { return startCol + 1; }
public int getEndRow() { return endRow; }
public int GetEndRow() { return endRow + 1; }
public int getEndCol() { return endCol; }
public int GetEndCol() { return endCol + 1; }
public int getStart() { return start; }
public int GetStart() { return start + 1; }
public int getEnd() { return end; }
public int GetEnd() { return end + 1; }
public boolean isFullUpdate() { return fullUpdate; }
public boolean IsFullUpdate() { return fullUpdate; }
public String getText() { return text; }
public String GetText() { return text; }
@Override
public String toString() {
return String.format("ECLPSUpdate[start=(%d,%d), end=(%d,%d), full=%b]",
startRow, startCol, endRow, endCol, fullUpdate);
}
}
@@ -0,0 +1,21 @@
package com.ibm.eNetwork.ECL;
import java.io.Serializable;
/**
* Screen descriptor matching engine facade matching IBM Host On-Demand ECLScreenDesc.
*/
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc implements Serializable {
private static final long serialVersionUID = 1L;
public ECLScreenDesc() {
super();
}
public boolean Matches(ECLPS ps, ECLOIA oia) {
haus.nightmare.lib3270j.ecl.ECLPS p = (ps != null) ? ps.getDelegate() : null;
haus.nightmare.lib3270j.ecl.ECLOIA o = (oia != null) ? oia.getDelegate() : null;
return super.Matches(p, o);
}
}
@@ -0,0 +1,8 @@
package com.ibm.eNetwork.ECL;
/**
* Listener interface for ECLScreenReco matching notifications matching IBM HoD.
*/
public interface ECLScreenNotify {
void screenMatching(ECLScreenRecoEvent event);
}
@@ -0,0 +1,78 @@
package com.ibm.eNetwork.ECL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Screen recognition facade matching IBM Host On-Demand ECLScreenReco.
*/
public class ECLScreenReco {
private final haus.nightmare.lib3270j.ecl.ECLScreenReco delegate;
private final ECLPS ps;
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
public ECLScreenReco() {
this((ECLPS) null);
}
public ECLScreenReco(ECLSession session) {
this(session != null ? session.GetPS() : null);
}
public ECLScreenReco(ECLPS ps) {
this(ps, (ECLOIA) null);
}
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
this.ps = ps;
this.delegate = new haus.nightmare.lib3270j.ecl.ECLScreenReco(ps != null ? ps.getDelegate() : null,
oia != null ? oia.getDelegate() : null);
}
public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLScreenReco delegate) {
this.delegate = delegate;
this.ps = null;
}
public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
if (desc != null && notify != null) {
notifyMap.put(desc, notify);
if (delegate != null) {
delegate.RegisterScreen(desc, event -> notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps)));
}
}
}
public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
RegisterScreen(desc, notify);
}
public void UnregisterScreen(ECLScreenDesc desc) {
if (desc != null) {
notifyMap.remove(desc);
if (delegate != null) {
delegate.UnregisterScreen(desc);
}
}
}
public void unregisterScreen(ECLScreenDesc desc) {
UnregisterScreen(desc);
}
public boolean IsMatch(ECLScreenDesc desc) {
if (delegate != null) {
return delegate.IsMatch(desc);
}
return false;
}
public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) {
if (ps == null) return false;
if (desc == null) return true;
return desc.Matches(ps, (ECLOIA) null);
}
public haus.nightmare.lib3270j.ecl.ECLScreenReco getDelegate() { return delegate; }
}
@@ -0,0 +1,39 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Event delivered when an ECLScreenDesc matches the Presentation Space.
*/
public class ECLScreenRecoEvent extends ECLEvent {
private final ECLScreenReco source;
private final ECLScreenDesc screenDesc;
private final ECLPS ps;
public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) {
super(source);
this.source = source;
this.screenDesc = screenDesc;
this.ps = ps;
}
public ECLScreenRecoEvent(Object source, ECLPS ps) {
super(source);
this.source = (source instanceof ECLScreenReco) ? (ECLScreenReco) source : null;
this.screenDesc = null;
this.ps = ps;
}
@Override
public Object GetSource() { return source != null ? source : super.GetSource(); }
public ECLScreenReco GetScreenReco() { return source; }
public ECLScreenReco getScreenReco() { return source; }
public ECLScreenDesc GetScreenDesc() { return screenDesc; }
public ECLScreenDesc getScreenDesc() { return screenDesc; }
public ECLPS GetPS() { return ps; }
public ECLPS getPS() { return ps; }
}
@@ -0,0 +1,190 @@
package com.ibm.eNetwork.ECL;
import java.io.IOException;
import java.util.Properties;
/**
* Top-level session facade matching IBM Host On-Demand ECLSession.
* Extends ECLConnection and provides access to Presentation Space (ECLPS),
* Operator Information Area (ECLOIA), File Transfer (ECLXfer), and Screen Recognition (ECLScreenReco).
*/
public class ECLSession extends ECLConnection implements ECLConstants {
public static final String SESSION_NAME = "SESSION_NAME";
public static final String SESSION_LABEL = "SESSION_LABEL";
public static final String SESSION_TYPE = "SESSION_TYPE";
public static final String SESSION_HOST = "SESSION_HOST";
public static final String SESSION_HOST_PORT = "SESSION_HOST_PORT";
public static final String SESSION_PORT = "SESSION_PORT";
public static final String SESSION_PS_SIZE = "SESSION_PS_SIZE";
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
public static final String SESSION_SSL = "SESSION_SSL";
public static final String SESSION_SECURITY_PROTOCOL = "SESSION_SECURITY_PROTOCOL";
public static final String SESSION_TLS_PROTOCOL_VERSION = "tlsProtocolVersion";
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
public static final String SESSION_TN3270E = "SESSION_TN3270E";
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE";
public static final String KEY_KEEPALIVE_TYPE = "keepAliveType";
public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout";
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
public static final String BLOCK_PASTE = "blockPaste";
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
private final haus.nightmare.lib3270j.ecl.ECLSession delegate;
private final ECLPS ps;
private final ECLOIA oia;
private final ECLXfer xfer;
private final ECLScreenReco screenReco;
public ECLSession(Properties properties) throws ECLErr {
super(properties);
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(properties);
this.ps = new ECLPS(delegate.getPS());
this.oia = new ECLOIA(delegate.getOIA());
this.xfer = new ECLXfer(delegate.getXfer());
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLSession() throws ECLErr {
this(new Properties());
}
public ECLSession(String host, int port) throws ECLErr {
super(host, port);
Properties props = new Properties();
if (host != null) props.setProperty(SESSION_HOST, host);
props.setProperty(SESSION_PORT, String.valueOf(port));
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(props);
this.ps = new ECLPS(delegate.getPS());
this.oia = new ECLOIA(delegate.getOIA());
this.xfer = new ECLXfer(delegate.getXfer());
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLSession(haus.nightmare.lib3270j.ecl.ECLSession delegate) {
super(delegate);
this.delegate = delegate;
this.ps = new ECLPS(delegate != null ? delegate.getPS() : null);
this.oia = new ECLOIA(delegate != null ? delegate.getOIA() : null);
this.xfer = new ECLXfer(delegate != null ? delegate.getXfer() : null);
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLPS GetPS() {
return ps;
}
public ECLPS getPS() {
return ps;
}
public ECLOIA GetOIA() {
return oia;
}
public ECLOIA getOIA() {
return oia;
}
public ECLXfer GetXfer() {
return xfer;
}
public ECLXfer getXfer() {
return xfer;
}
public ECLScreenReco GetScreenReco() {
return screenReco;
}
public ECLScreenReco getScreenReco() {
return screenReco;
}
public ECLConnection GetConnection() {
return this;
}
public ECLConnection getConnection() {
return this;
}
public haus.nightmare.lib3270j.ecl.ECLSession getDelegate() {
return delegate;
}
@Override
public void StartCommunication() throws IOException {
if (delegate != null) {
delegate.StartCommunication();
} else {
super.StartCommunication();
}
}
@Override
public void StopCommunication() {
if (delegate != null) {
delegate.StopCommunication();
} else {
super.StopCommunication();
}
}
public boolean StartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null);
}
public boolean startCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout);
}
public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
if (delegate != null) {
try {
return delegate.StartCommunicationWithBlocking(timeout, desc);
} catch (IOException e) {
throw new ECLErr("ECLSession.StartCommunicationWithBlocking", "ECL0001", e.getMessage());
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return false;
}
public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout, desc);
}
public boolean RestartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
if (delegate != null) {
try {
return delegate.RestartCommunicationWithBlocking(timeout);
} catch (IOException e) {
throw new ECLErr("ECLSession.RestartCommunicationWithBlocking", "ECL0001", e.getMessage());
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return false;
}
public boolean restartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return RestartCommunicationWithBlocking(timeout);
}
public void dispose() {
if (delegate != null) {
delegate.dispose();
}
}
}
@@ -0,0 +1,54 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLXferListener;
/**
* File transfer facade matching IBM Host On-Demand ECLXfer.
*/
public class ECLXfer {
private final haus.nightmare.lib3270j.ecl.ECLXfer delegate;
public ECLXfer(ECLSession session, Boolean bidi) {
this.delegate = (session != null && session.getDelegate() != null)
? session.getDelegate().GetXfer() : null;
}
public ECLXfer(haus.nightmare.lib3270j.ecl.ECLXfer delegate) {
this.delegate = delegate;
}
public void SendFile(String localFile, String hostFile, String options) throws ECLErr {
if (delegate != null) {
int rc = delegate.SendFile(localFile, hostFile, options);
if (rc != 0) {
throw new ECLErr("ECLXfer:SendFile", "ECL0150", String.valueOf(rc));
}
}
}
public void ReceiveFile(String localFile, String hostFile, String options) throws ECLErr {
if (delegate != null) {
int rc = delegate.ReceiveFile(localFile, hostFile, options);
if (rc != 0) {
throw new ECLErr("ECLXfer:ReceiveFile", "ECL0150", String.valueOf(rc));
}
}
}
public void Cancel() {
if (delegate != null) {
delegate.Cancel();
}
}
public void RegisterXferEvent(ECLXferListener listener) {
// Can bridge listener if needed
}
public void UnregisterXferEvent(ECLXferListener listener) {
// Can bridge listener if needed
}
public haus.nightmare.lib3270j.ecl.ECLXfer getDelegate() { return delegate; }
}
@@ -0,0 +1,25 @@
package com.ibm.eNetwork.ECL.event;
/**
* Base event class matching IBM Host On-Demand ECLEvent.
*/
public class ECLEvent {
private final Object originator;
public ECLEvent(Object object) {
this.originator = object;
}
public Object GetSource() {
return this.originator;
}
public Object getSource() {
return this.originator;
}
@Override
public String toString() {
return super.toString() + ": " + (this.originator != null ? this.originator.toString() : "null");
}
}
@@ -0,0 +1,126 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLPS;
import com.ibm.eNetwork.ECL.ECLPSUpdate;
/**
* Presentation Space event class matching IBM Host On-Demand ECLPSEvent.
*/
public class ECLPSEvent extends ECLEvent {
public static final int USER_EVENTS = 1;
public static final int HOST_EVENTS = 2;
public static final int ALL_EVENTS = 3;
private int type = 0;
private int start = 1;
private int end = 1;
private int startRow = 1;
private int startCol = 1;
private int endRow = 1;
private int endCol = 1;
private int oldCursorPos = 1;
private int newCursorPos = 1;
private boolean fullUpdate = true;
private boolean cursorVisible = true;
private int ringCounter = 0;
private ECLPSUpdate psUpdate = null;
private haus.nightmare.lib3270j.ecl.ECLPSEvent delegate = null;
public ECLPSEvent(Object source) {
super(source);
if (source instanceof ECLPS) {
ECLPS ps = (ECLPS) source;
this.end = ps.GetSize();
this.endRow = ps.GetRows();
this.endCol = ps.GetCols();
this.newCursorPos = ps.GetCursorPos();
} else if (source instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
haus.nightmare.lib3270j.ecl.ECLPS ps = (haus.nightmare.lib3270j.ecl.ECLPS) source;
this.end = ps.GetSize();
this.endRow = ps.GetRows();
this.endCol = ps.GetCols();
this.newCursorPos = ps.GetCursorPos();
}
}
public ECLPSEvent(Object source, int type) {
this(source);
this.type = type;
}
public ECLPSEvent(haus.nightmare.lib3270j.ecl.ECLPSEvent delegate) {
super(delegate != null ? delegate.getSource() : null);
this.delegate = delegate;
if (delegate != null) {
this.type = delegate.getType();
this.start = delegate.getStart();
this.end = delegate.getEnd();
this.startRow = delegate.getStartRow();
this.startCol = delegate.getStartCol();
this.endRow = delegate.getEndRow();
this.endCol = delegate.getEndCol();
this.oldCursorPos = delegate.getOldCursorAddress() + 1;
this.newCursorPos = delegate.getNewCursorAddress() + 1;
this.fullUpdate = delegate.isFullUpdate();
this.cursorVisible = delegate.isCursorVisible();
this.ringCounter = delegate.getRingCounter();
if (delegate.getPSUpdate() != null) {
this.psUpdate = new ECLPSUpdate(delegate.getPSUpdate());
}
}
}
public int GetType() { return type; }
public int getType() { return type; }
public ECLPS GetPS() {
Object src = GetSource();
if (src instanceof ECLPS) {
return (ECLPS) src;
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
return new ECLPS((haus.nightmare.lib3270j.ecl.ECLPS) src);
}
return null;
}
public void SetStart(int start) { this.start = start; }
public int GetStart() { return start; }
public int getStart() { return start; }
public int GetStartRow() { return startRow; }
public int getStartRow() { return startRow; }
public int GetStartCol() { return startCol; }
public int getStartCol() { return startCol; }
public void SetEnd(int end) { this.end = end; }
public int GetEnd() { return end; }
public int getEnd() { return end; }
public int GetEndRow() { return endRow; }
public int getEndRow() { return endRow; }
public int GetEndCol() { return endCol; }
public int getEndCol() { return endCol; }
public int GetOldCursorPos() { return oldCursorPos; }
public int getOldCursorPos() { return oldCursorPos; }
public int GetNewCursorPos() { return newCursorPos; }
public int getNewCursorPos() { return newCursorPos; }
public boolean IsFullUpdate() { return fullUpdate; }
public boolean isFullUpdate() { return fullUpdate; }
public boolean IsCursorVisible() { return cursorVisible; }
public boolean isCursorVisible() { return cursorVisible; }
public int GetRingCounter() { return ringCounter; }
public int getRingCounter() { return ringCounter; }
public ECLPSUpdate GetPSUpdate() { return psUpdate; }
public ECLPSUpdate getPSUpdate() { return psUpdate; }
public haus.nightmare.lib3270j.ecl.ECLPSEvent getDelegate() { return delegate; }
}
@@ -0,0 +1,13 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLPS;
/**
* Presentation Space listener interface matching IBM Host On-Demand ECLPSListener.
*/
public interface ECLPSListener {
void PSNotifyEvent(ECLPSEvent event);
default void PSNotifyStop(ECLPS ps, int reason) {}
default void PSNotifyError(ECLPS ps, ECLErr err) {}
}
@@ -0,0 +1,48 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLXfer;
/**
* File transfer event matching IBM Host On-Demand ECLXferEvent.
*/
public class ECLXferEvent extends ECLEvent {
private final String sourceFile;
private final String targetFile;
private final boolean receive;
private final long unitsDone;
private final long unitsToDo;
public ECLXferEvent(Object source, long unitsDone, String sourceFile, String targetFile, boolean receive) {
this(source, unitsDone, -1L, sourceFile, targetFile, receive);
}
public ECLXferEvent(Object source, long unitsDone, long unitsToDo, String sourceFile, String targetFile, boolean receive) {
super(source);
this.unitsDone = unitsDone;
this.unitsToDo = unitsToDo;
this.sourceFile = sourceFile;
this.targetFile = targetFile;
this.receive = receive;
}
public ECLXfer GetECLXfer() {
Object src = GetSource();
return (src instanceof ECLXfer) ? (ECLXfer) src : null;
}
public String GetSourceFile() { return sourceFile; }
public String getSourceFile() { return sourceFile; }
public String GetTargetFile() { return targetFile; }
public String getTargetFile() { return targetFile; }
public long GetBytesSent() { return unitsDone; }
public long getBytesSent() { return unitsDone; }
public long GetBytesToSend() { return unitsToDo; }
public long getBytesToSend() { return unitsToDo; }
public boolean IsReceive() { return receive; }
public boolean isReceive() { return receive; }
}
@@ -0,0 +1,13 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLXfer;
/**
* File transfer listener interface matching IBM Host On-Demand ECLXferListener.
*/
public interface ECLXferListener {
void XferNotifyEvent(ECLXferEvent event);
default void XferNotifyStop(ECLXfer xfer, int reason) {}
default void XferNotifyError(ECLXfer xfer, ECLErr err) {}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.ECL.macrovariable;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroExpressionParser.
*/
public class MacroExpressionParser extends haus.nightmare.lib3270j.macro.variable.MacroExpressionParser implements Serializable {
private static final long serialVersionUID = 1L;
public MacroExpressionParser() {
super();
}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.ECL.macrovariable;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroVariables.
*/
public class MacroVariables extends haus.nightmare.lib3270j.macro.variable.MacroVariables implements Serializable {
private static final long serialVersionUID = 1L;
public MacroVariables() {
super();
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDAttrib drop-in facade.
*/
public class ECLSDAttrib extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib {
private static final long serialVersionUID = 1L;
public ECLSDAttrib() {
super();
}
public ECLSDAttrib(char attrib, int row, int col, int plane) {
super(attrib, row, col, plane);
}
public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) {
super(attrib, row, col, plane, optional);
}
}
@@ -0,0 +1,24 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDBlock drop-in facade.
*/
public class ECLSDBlock extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock {
private static final long serialVersionUID = 1L;
public ECLSDBlock() {
super();
}
public ECLSDBlock(int srow, int scol, boolean caseSense, boolean optional) {
super(srow, scol, caseSense, optional);
}
public ECLSDBlock(int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
super(srow, scol, erow, ecol, caseSense, optional);
}
public ECLSDBlock(String[] textLines, int srow, int scol, boolean caseSense) {
super(textLines, srow, scol, caseSense);
}
}
@@ -0,0 +1,16 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDBlockString drop-in facade.
*/
public class ECLSDBlockString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlockString {
private static final long serialVersionUID = 1L;
public ECLSDBlockString() {
super();
}
public ECLSDBlockString(String text) {
super(text);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDChecksum drop-in facade.
*/
public class ECLSDChecksum extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDChecksum {
private static final long serialVersionUID = 1L;
public ECLSDChecksum() {
super();
}
public ECLSDChecksum(int value) {
super(value);
}
public ECLSDChecksum(int value, boolean optional) {
super(value, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDCursor drop-in facade.
*/
public class ECLSDCursor extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDCursor {
private static final long serialVersionUID = 1L;
public ECLSDCursor() {
super();
}
public ECLSDCursor(int row, int col) {
super(row, col);
}
public ECLSDCursor(int row, int col, boolean optional) {
super(row, col, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDFields drop-in facade.
*/
public class ECLSDFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDFields {
private static final long serialVersionUID = 1L;
public ECLSDFields() {
super();
}
public ECLSDFields(int num) {
super(num);
}
public ECLSDFields(int num, boolean optional) {
super(num, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDInputFields drop-in facade.
*/
public class ECLSDInputFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDInputFields {
private static final long serialVersionUID = 1L;
public ECLSDInputFields() {
super();
}
public ECLSDInputFields(int num) {
super(num);
}
public ECLSDInputFields(int num, boolean optional) {
super(num, optional);
}
}
@@ -0,0 +1,24 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDOIA drop-in facade.
*/
public class ECLSDOIA extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDOIA {
private static final long serialVersionUID = 1L;
public ECLSDOIA() {
super();
}
public ECLSDOIA(int type) {
super(type);
}
public ECLSDOIA(int type, boolean optional) {
super(type, optional);
}
public ECLSDOIA(String status, boolean optional) {
super(status, optional);
}
}
@@ -0,0 +1,36 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDString drop-in facade.
*/
public class ECLSDString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDString {
private static final long serialVersionUID = 1L;
public ECLSDString() {
super();
}
public ECLSDString(String text) {
super(text);
}
public ECLSDString(String text, int row, int col) {
super(text, row, col);
}
public ECLSDString(String text, int row, int col, boolean caseSense) {
super(text, row, col, caseSense);
}
public ECLSDString(String text, int row, int col, boolean caseSense, boolean optional) {
super(text, row, col, caseSense, optional);
}
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
super(text, srow, scol, erow, ecol, caseSense, optional);
}
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional, boolean wrap) {
super(text, srow, scol, erow, ecol, caseSense, optional, wrap);
}
}
@@ -0,0 +1,16 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLScreenDescriptor drop-in facade.
*/
public abstract class ECLScreenDescriptor extends haus.nightmare.lib3270j.ecl.screenreco.ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
protected ECLScreenDescriptor() {
super();
}
protected ECLScreenDescriptor(boolean optional) {
super(optional);
}
}
@@ -0,0 +1,23 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
import com.ibm.eNetwork.ECL.ECLSession;
/**
* Compatibility facade for IBM Host On-Demand Macro bean.
*/
public class Macro extends haus.nightmare.lib3270j.macro.Macro implements Serializable {
private static final long serialVersionUID = 1L;
public Macro() {
super();
}
public Macro(ECLSession session) {
super(session != null ? session.getDelegate() : null);
}
public Macro(haus.nightmare.lib3270j.ecl.ECLSession session) {
super(session);
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroAction.
*/
public abstract class MacroAction extends haus.nightmare.lib3270j.macro.action.MacroAction implements Serializable {
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroActions.
*/
public class MacroActions extends haus.nightmare.lib3270j.macro.MacroActions implements Serializable {
private static final long serialVersionUID = 1L;
public MacroActions() {
super();
}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroNextScreens.
*/
public class MacroNextScreens extends haus.nightmare.lib3270j.macro.MacroNextScreens implements Serializable {
private static final long serialVersionUID = 1L;
public MacroNextScreens() {
super();
}
}
@@ -0,0 +1,18 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroScreen.
*/
public class MacroScreen extends haus.nightmare.lib3270j.macro.MacroScreen implements Serializable {
private static final long serialVersionUID = 1L;
public MacroScreen() {
super();
}
public MacroScreen(String name) {
super(name);
}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroScreens.
*/
public class MacroScreens extends haus.nightmare.lib3270j.macro.MacroScreens implements Serializable {
private static final long serialVersionUID = 1L;
public MacroScreens() {
super();
}
}
@@ -435,6 +435,14 @@ public class ConnectionConfig {
} }
} }
// Parse [lu@]host[:port] (e.g. "00C2@mvs.host.com:1023")
String parsedLu = null;
int atIdx = s.indexOf('@');
if (atIdx > 0 && atIdx < s.length() - 1) {
parsedLu = s.substring(0, atIdx).trim();
s = s.substring(atIdx + 1).trim();
}
String host = s; String host = s;
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23); int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
@@ -458,6 +466,9 @@ public class ConnectionConfig {
} }
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4); ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
if (parsedLu != null && !parsedLu.isEmpty()) {
config.setLuName(parsedLu);
}
config.setUseTls(tls); config.setUseTls(tls);
config.setTn3270eEnabled(tn3270e); config.setTn3270eEnabled(tn3270e);
config.setKeepAliveEnabled(keepAlive); config.setKeepAliveEnabled(keepAlive);
@@ -92,6 +92,9 @@ public class Telnet3270Client {
ps.notifyAlarm(); ps.notifyAlarm();
} }
@Override public void onKeyboardUnlocked() { @Override public void onKeyboardUnlocked() {
if (screenBuffer != null) {
screenBuffer.getHistoryBuffer().snapshot(screenBuffer);
}
ps.notifyKeyUnlocked(); ps.notifyKeyUnlocked();
} }
}); });
@@ -658,6 +661,9 @@ public class Telnet3270Client {
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); } public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleWordWrap() { setWordWrap(!isWordWrap()); } public void toggleWordWrap() { setWordWrap(!isWordWrap()); }
public boolean isPasteWordBreak() { return inputProcessor != null && inputProcessor.isPasteWordBreak(); }
public void setPasteWordBreak(boolean b) { if (inputProcessor != null) inputProcessor.setPasteWordBreak(b); }
/** APL Keyboard Mode operations. */ /** APL Keyboard Mode operations. */
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); } public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); } public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
@@ -705,4 +711,8 @@ public class Telnet3270Client {
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() { public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
return dsProcessor.getGocaDecoder(); return dsProcessor.getGocaDecoder();
} }
public haus.nightmare.lib3270j.screen.ScreenHistoryBuffer getScreenHistoryBuffer() {
return screenBuffer != null ? screenBuffer.getHistoryBuffer() : null;
}
} }
@@ -4,10 +4,10 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* EBCDIC ↔ Unicode character translator conforming to IBM Host On-Demand (HoD v14). * EBCDIC ↔ Unicode character translator conforming to Host On-Demand (HoD v14).
* Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS). * Modular architecture delegating to pluggable CodePage implementations (SBCS and DBCS).
* Supports custom per-instance character translation override tables and complete * Supports custom per-instance character translation override tables and complete
* IBM 3270 APL / Graphic Escape (GA23-0059) character mappings. * 3270 APL / Graphic Escape (GA23-0059) character mappings.
* Default: Code Page 037 (US/Canada EBCDIC). * Default: Code Page 037 (US/Canada EBCDIC).
*/ */
public class EbcdicTranslator { public class EbcdicTranslator {
@@ -210,10 +210,12 @@ public class EbcdicTranslator {
*/ */
public char ebcdicToUnicode(int ebc) { public char ebcdicToUnicode(int ebc) {
int b = ebc & 0xFF; int b = ebc & 0xFF;
if (!customEbcdicToUnicode.isEmpty()) {
Character custom = customEbcdicToUnicode.get(b); Character custom = customEbcdicToUnicode.get(b);
if (custom != null) { if (custom != null) {
return custom; return custom;
} }
}
return activeCodePage.ebcdicToUnicode(b); return activeCodePage.ebcdicToUnicode(b);
} }
@@ -236,10 +238,12 @@ public class EbcdicTranslator {
* Returns -1 if the character cannot be mapped. * Returns -1 if the character cannot be mapped.
*/ */
public int unicodeToEbcdic(char unicode) { public int unicodeToEbcdic(char unicode) {
if (!customUnicodeToEbcdic.isEmpty()) {
Integer custom = customUnicodeToEbcdic.get(unicode); Integer custom = customUnicodeToEbcdic.get(unicode);
if (custom != null) { if (custom != null) {
return custom; return custom;
} }
}
return activeCodePage.unicodeToEbcdic(unicode); return activeCodePage.unicodeToEbcdic(unicode);
} }
@@ -296,8 +300,8 @@ public class EbcdicTranslator {
} }
/** /**
* Map an IBM 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph. * Map a 3270 APL / Graphic Escape (GE) EBCDIC code point to its Unicode glyph.
* Conforms to IBM 3270 APL / Text character set and GA23-0059 specification. * Conforms to 3270 APL / Text character set and GA23-0059 specification.
*/ */
public char mapAPL(int ebcdicCodePoint) { public char mapAPL(int ebcdicCodePoint) {
switch (ebcdicCodePoint & 0xFF) { switch (ebcdicCodePoint & 0xFF) {
@@ -331,7 +335,7 @@ public class EbcdicTranslator {
case 0xBF: return '\u00B5'; // Micro 'µ' case 0xBF: return '\u00B5'; // Micro 'µ'
case 0x5F: return '\u00AC'; // Not sign '¬' case 0x5F: return '\u00AC'; // Not sign '¬'
// IBM 3270 APL Operational & Structural Glyphs // 3270 APL Operational & Structural Glyphs
case 0x80: return '\u22C4'; // Diamond '⋄' case 0x80: return '\u22C4'; // Diamond '⋄'
case 0x81: return '\u237A'; // APL Alpha '⍺' case 0x81: return '\u237A'; // APL Alpha '⍺'
case 0x82: return '\u22A5'; // Up Tack / Decode '⊥' case 0x82: return '\u22A5'; // Up Tack / Decode '⊥'
@@ -383,7 +387,7 @@ public class EbcdicTranslator {
} }
/** /**
* Translate an IBM 3270 Graphic Escape (GE) / APL character code to Unicode. * Translate a 3270 Graphic Escape (GE) / APL character code to Unicode.
*/ */
public char getAplGraphic(int ec) { public char getAplGraphic(int ec) {
return mapAPL(ec); return mapAPL(ec);
@@ -54,6 +54,13 @@ public class DataStreamProcessor {
private boolean unlockSysPending = false; private boolean unlockSysPending = false;
private boolean rcvdRead = false; private boolean rcvdRead = false;
// Modal SA (set attribute) character attributes
private byte currentFg = 0;
private byte currentBg = 0;
private byte currentGr = 0;
private byte currentCs = 0;
private byte currentOl = 0;
/** Functional interface for sending output back through the telnet stack. */ /** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface @FunctionalInterface
public interface OutputSender { public interface OutputSender {
@@ -240,6 +247,11 @@ public class DataStreamProcessor {
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
log.info(">>> EAU: erasing all unprotected fields"); log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected(); screen.eraseAllUnprotected();
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
currentOl = 0;
break; break;
case CMD_WSF: case CMD_WSF:
case SNA_CMD_WSF: case SNA_CMD_WSF:
@@ -378,11 +390,21 @@ public class DataStreamProcessor {
if (wccReset(wcc)) { if (wccReset(wcc)) {
// Reset all character attributes to defaults // Reset all character attributes to defaults
log.fine("WCC reset: clearing default attributes"); log.fine("WCC reset: clearing default attributes");
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
currentOl = 0;
} }
if (eraseFirst) { if (eraseFirst) {
screen.clear(); screen.clear();
log.fine("Cleared screen for Erase/Write"); log.fine("Cleared screen for Erase/Write");
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
currentOl = 0;
} }
// Process orders and data starting at byte 2 // Process orders and data starting at byte 2
@@ -390,9 +412,6 @@ public class DataStreamProcessor {
int end = offset + length; int end = offset + length;
int baddr = screen.getBufferAddress(); int baddr = screen.getBufferAddress();
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
// Current SA (set attribute) values for character-mode
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
boolean lastWasOrder = false; boolean lastWasOrder = false;
while (pos < end) { while (pos < end) {
@@ -427,10 +446,6 @@ public class DataStreamProcessor {
// FA position is a display position that shows as blank // FA position is a display position that shows as blank
ea.ec = 0; ea.ec = 0;
ea.ucs4 = ' '; ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
screen.setFormatted(true); screen.setFormatted(true);
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -455,10 +470,6 @@ public class DataStreamProcessor {
ea.clear(); ea.clear();
ea.ec = 0; ea.ec = 0;
ea.ucs4 = ' '; ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
for (int i = 0; i < nPairs; i++) { for (int i = 0; i < nPairs; i++) {
int attrType = data[pos + 2 + i * 2] & 0xFF; int attrType = data[pos + 2 + i * 2] & 0xFF;
@@ -485,6 +496,13 @@ public class DataStreamProcessor {
int attrType = data[pos + 1] & 0xFF; int attrType = data[pos + 1] & 0xFF;
int attrValue = data[pos + 2] & 0xFF; int attrValue = data[pos + 2] & 0xFF;
switch (attrType) { switch (attrType) {
case XA_ALL:
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
currentOl = 0;
break;
case XA_FOREGROUND: case XA_FOREGROUND:
currentFg = (byte) attrValue; currentFg = (byte) attrValue;
break; break;
@@ -508,6 +526,10 @@ public class DataStreamProcessor {
case XA_CHARSET: case XA_CHARSET:
currentCs = (byte) attrValue; currentCs = (byte) attrValue;
break; break;
case XA_OUTLINING:
case 0x84:
currentOl = (byte) attrValue;
break;
} }
pos += 3; pos += 3;
lastWasOrder = true; lastWasOrder = true;
@@ -535,6 +557,8 @@ public class DataStreamProcessor {
applyExtendedAttribute(ea, attrType, attrValue); applyExtendedAttribute(ea, attrType, attrValue);
} }
} }
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 2 + nPairs * 2; pos += 2 + nPairs * 2;
lastWasOrder = true; lastWasOrder = true;
break; break;
@@ -590,6 +614,7 @@ public class DataStreamProcessor {
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.cs = fillCs; ea.cs = fillCs;
ea.ol = currentOl;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
} while (baddr != toAddr); } while (baddr != toAddr);
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -618,6 +643,7 @@ public class DataStreamProcessor {
ea.bg = 0; ea.bg = 0;
ea.gr = 0; ea.gr = 0;
ea.cs = 0; ea.cs = 0;
ea.ol = 0;
} }
} }
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
@@ -642,6 +668,7 @@ public class DataStreamProcessor {
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.ol = currentOl;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
pos += 2; pos += 2;
@@ -668,6 +695,7 @@ public class DataStreamProcessor {
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.cs = currentCs; ea.cs = currentCs;
ea.ol = currentOl;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -980,11 +1008,18 @@ public class DataStreamProcessor {
byte[] psData = new byte[fieldLen - 3]; byte[] psData = new byte[fieldLen - 3];
System.arraycopy(data, pos + 3, psData, 0, psData.length); System.arraycopy(data, pos + 3, psData, 0, psData.length);
programSymbolManager.loadps(psData); programSymbolManager.loadps(psData);
notifyScreenUpdated();
} }
break; break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
if (fieldLen > 3) { if (fieldLen > 3) {
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * qrBuilder.getCharWidth();
int targetH = screen.getRows() * qrBuilder.getCharHeight();
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3); gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3);
notifyScreenUpdated(); notifyScreenUpdated();
} }
@@ -1284,6 +1319,7 @@ public class DataStreamProcessor {
byte[] psData = new byte[fieldLen - 4]; byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, offset + 4, psData, 0, psData.length); System.arraycopy(data, offset + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData); programSymbolManager.loadps(psData);
notifyScreenUpdated();
} }
break; break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
@@ -1298,9 +1334,10 @@ public class DataStreamProcessor {
int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4); int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - offset)); int orderLen = Math.max(0, fieldLen - (orderOffset - offset));
graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight());
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9; int targetW = screen.getCols() * qrBuilder.getCharWidth();
int targetH = screen.getRows() * 16; int targetH = screen.getRows() * qrBuilder.getCharHeight();
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) { if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH); graphicsPlane.resize(targetW, targetH);
} }
@@ -0,0 +1,86 @@
package haus.nightmare.lib3270j.datastream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Reusable, resizable byte buffer that provides zero-copy access to its internal
* array and supports ByteBuffer slicing to minimize heap allocations during
* network stream processing.
*/
public class FastByteBuffer extends OutputStream {
private byte[] buf;
private int count;
public FastByteBuffer() {
this(32768);
}
public FastByteBuffer(int initialCapacity) {
this.buf = new byte[Math.max(32, initialCapacity)];
this.count = 0;
}
private void ensureCapacity(int minCapacity) {
if (minCapacity > buf.length) {
int newCap = Math.max(buf.length << 1, minCapacity);
buf = Arrays.copyOf(buf, newCap);
}
}
@Override
public synchronized void write(int b) {
ensureCapacity(count + 1);
buf[count++] = (byte) b;
}
@Override
public synchronized void write(byte[] b, int off, int len) {
if (b == null || len <= 0) return;
ensureCapacity(count + len);
System.arraycopy(b, off, buf, count, len);
count += len;
}
/**
* Direct reference to internal buffer array.
* Use {@link #size()} to determine active length.
*/
public synchronized byte[] buffer() {
return buf;
}
public synchronized int size() {
return count;
}
public synchronized void reset() {
count = 0;
}
/**
* Creates a read-only ByteBuffer view wrapping active bytes without copying.
*/
public synchronized ByteBuffer asByteBuffer() {
return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer();
}
/**
* Creates a read-only ByteBuffer slice for a sub-range without copying.
*/
public synchronized ByteBuffer slice(int offset, int length) {
if (offset < 0 || length < 0 || offset + length > count) {
throw new IndexOutOfBoundsException("offset=" + offset + " length=" + length + " size=" + count);
}
return ByteBuffer.wrap(buf, offset, length).slice().asReadOnlyBuffer();
}
/**
* Produces a copied byte array if an isolated copy is explicitly required.
*/
public synchronized byte[] toByteArray() {
return Arrays.copyOf(buf, count);
}
}
@@ -9,7 +9,7 @@ import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
/** /**
* Builds Query Reply structured fields in response to host Read Partition queries. * Builds Query Reply structured fields in response to host Read Partition queries.
* Matches IBM 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout. * Matches 3270 Architecture (GA23-0059) and x3270 sf.c exact binary layout.
*/ */
public class QueryReplyBuilder { public class QueryReplyBuilder {
@@ -18,10 +18,102 @@ public class QueryReplyBuilder {
private static final int SW_3279_2 = 0x09; private static final int SW_3279_2 = 0x09;
private static final int SH_3279_2 = 0x0c; private static final int SH_3279_2 = 0x0c;
// Usable Area physical dimensions matching IBM Host On-Demand DS3270.java (Inches, 96 dpi: 0x00010060) // Usable Area physical dimensions matching HOD DS3270.java (Inches, 96 dpi: 0x00010060)
private static final int Xr_HOD = 0x00010060; private static final int Xr_HOD = 0x00010060;
private static final int Yr_HOD = 0x00010060; private static final int Yr_HOD = 0x00010060;
// Pre-computed static query reply payloads to eliminate allocation churn
private static final byte[] STATIC_QR_COLOR = new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4,
(byte) 0xF1, (byte) 0xF1, // Blue
(byte) 0xF2, (byte) 0xF2, // Red
(byte) 0xF3, (byte) 0xF3, // Pink
(byte) 0xF4, (byte) 0xF4, // Green
(byte) 0xF5, (byte) 0xF5, // Turquoise
(byte) 0xF6, (byte) 0xF6, // Yellow
(byte) 0xF7, (byte) 0xF7 // Neutral/White
};
private static final byte[] STATIC_QR_HIGHLIGHTING = new byte[] {
0x04, 0x00, (byte) 0xF0,
(byte) 0xF1, (byte) 0xF1,
(byte) 0xF2, (byte) 0xF2,
(byte) 0xF4, (byte) 0xF4
};
private static final byte[] STATIC_QR_REPLY_MODES = new byte[] {
SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR
};
private static final byte[] STATIC_QR_OUTLINING = new byte[] {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final byte[] STATIC_QR_DBCS_ASIA = new byte[] {
0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01
};
private static final byte[] STATIC_QR_AUXDA = new byte[] {
0x00, 0x00
};
private static final byte[] STATIC_QR_TRANSPARENCY = new byte[] {
0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF
};
private static final byte[] STATIC_QR_SEGMENT = new byte[] {
(byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00
};
private static final byte[] STATIC_QR_PROCEDURE = new byte[] {
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
private static final byte[] STATIC_QR_LINETYPE = new byte[] {
0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
};
private static final byte[] STATIC_PORT_BLOCKS;
static {
ByteArrayOutputStream pOut = new ByteArrayOutputStream(64);
byte[][] data = {
{ 0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
{ 0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02 },
{ 0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF },
{ 0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C }
};
for (byte[] d : data) {
int len = 4 + d.length;
pOut.write((len >> 8) & 0xFF);
pOut.write(len & 0xFF);
pOut.write(SFID_QREPLY);
pOut.write(QR_PORT);
pOut.write(d, 0, d.length);
}
STATIC_PORT_BLOCKS = pOut.toByteArray();
}
private static final byte[] STATIC_QR_GRCOLOR;
static {
ByteArrayOutputStream gOut = new ByteArrayOutputStream(110);
gOut.write(0x00); gOut.write(0x04); gOut.write(0x00); gOut.write(0xFF); gOut.write(0xFF);
gOut.write(0x00); gOut.write(0x10); gOut.write(0x00); gOut.write(0x10);
for (int i = 0; i < 16; i++) {
gOut.write(0x00);
gOut.write(i);
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
gOut.write((argb >> 16) & 0xFF);
gOut.write((argb >> 8) & 0xFF);
gOut.write(argb & 0xFF);
gOut.write(0x00);
}
STATIC_QR_GRCOLOR = gOut.toByteArray();
}
private final ScreenBuffer screen; private final ScreenBuffer screen;
private GraphicsMode graphicsMode = GraphicsMode.BOTH; private GraphicsMode graphicsMode = GraphicsMode.BOTH;
@@ -101,27 +193,27 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
// Color (0x86) // Color (0x86)
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
// Highlighting (0x87) // Highlighting (0x87)
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
// Reply Modes (0x88) // Reply Modes (0x88)
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) { if (isDbcs) {
// Outlining (0x8C) // Outlining (0x8C)
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
// DBCS Asia (0x91) // DBCS Asia (0x91)
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} }
// Distributed Data Management (0x95) // Distributed Data Management (0x95)
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
// Auxiliary Devices (0x99) // Auxiliary Devices (0x99)
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
// Implicit Partition (0xA6) // Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
@@ -141,26 +233,26 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize)); appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows)); appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
if (isDbcs) { if (isDbcs) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} }
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); // 0xA8 appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); // 0xB0 appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); // 0xB1 appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
appendQueryReply(out, QR_LINETYPE, buildLineType()); // 0xB2 appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
appendPort(out); // 0xB3 appendPort(out); // 0xB3
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); // 0xB4 appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR); // 0xB4
appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6 appendQueryReply(out, QR_GRSYMBOLSET, buildGrSymbolSet()); // 0xB6
} }
@@ -197,20 +289,20 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_CHARSETS, buildCharsets()); appendQueryReply(out, QR_CHARSETS, buildCharsets());
break; break;
case QR_COLOR: case QR_COLOR:
appendQueryReply(out, QR_COLOR, buildColor()); appendQueryReply(out, QR_COLOR, STATIC_QR_COLOR);
break; break;
case QR_HIGHLIGHTING: case QR_HIGHLIGHTING:
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, STATIC_QR_HIGHLIGHTING);
break; break;
case QR_REPLY_MODES: case QR_REPLY_MODES:
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, STATIC_QR_REPLY_MODES);
break; break;
case QR_OUTLINING: // 0x8C case QR_OUTLINING: // 0x8C
appendQueryReply(out, QR_OUTLINING, buildOutlining()); appendQueryReply(out, QR_OUTLINING, STATIC_QR_OUTLINING);
break; break;
case QR_DBCS_ASIA: // 0x91 case QR_DBCS_ASIA: // 0x91
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
appendQueryReply(out, QR_DBCS_ASIA, buildDbcsAsia()); appendQueryReply(out, QR_DBCS_ASIA, STATIC_QR_DBCS_ASIA);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -219,35 +311,35 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
break; break;
case QR_AUXDA: // 0x99 case QR_AUXDA: // 0x99
appendQueryReply(out, QR_AUXDA, buildAuxDa()); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
break; break;
case QR_IMP_PART: // 0xA6 case QR_IMP_PART: // 0xA6
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break; break;
case QR_TRANSPARENCY: // 0xA8 case QR_TRANSPARENCY: // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_SEGMENT: // 0xB0 case QR_SEGMENT: // 0xB0
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_SEGMENT, buildSegment(maxCols, maxRows)); appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_PROCEDURE: // 0xB1 case QR_PROCEDURE: // 0xB1
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_PROCEDURE, buildProcedure(maxCols, maxRows)); appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
break; break;
case QR_LINETYPE: // 0xB2 case QR_LINETYPE: // 0xB2
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_LINETYPE, buildLineType()); appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -261,7 +353,7 @@ public class QueryReplyBuilder {
break; break;
case QR_GRCOLOR: // 0xB4 case QR_GRCOLOR: // 0xB4
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_GRCOLOR, buildGrColor()); appendQueryReply(out, QR_GRCOLOR, STATIC_QR_GRCOLOR);
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -321,19 +413,19 @@ public class QueryReplyBuilder {
out.write(maxCols & 0xFF); // usable width low out.write(maxCols & 0xFF); // usable width low
out.write((maxRows >> 8) & 0xFF); // usable height high out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low out.write(maxRows & 0xFF); // usable height low
out.write(0x00); // units (0x00 = inches, matching IBM Host On-Demand QR_USEAREA_STRING) out.write(0x00); // units (0x00 = inches, matching HOD QR_USEAREA_STRING)
// Xr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI) // Xr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
out.write((Xr_HOD >> 24) & 0xFF); out.write((Xr_HOD >> 24) & 0xFF);
out.write((Xr_HOD >> 16) & 0xFF); out.write((Xr_HOD >> 16) & 0xFF);
out.write((Xr_HOD >> 8) & 0xFF); out.write((Xr_HOD >> 8) & 0xFF);
out.write(Xr_HOD & 0xFF); out.write(Xr_HOD & 0xFF);
// Yr (4 bytes) - matching IBM Host On-Demand QR_USEAREA_STRING (96 DPI) // Yr (4 bytes) - matching HOD QR_USEAREA_STRING (96 DPI)
out.write((Yr_HOD >> 24) & 0xFF); out.write((Yr_HOD >> 24) & 0xFF);
out.write((Yr_HOD >> 16) & 0xFF); out.write((Yr_HOD >> 16) & 0xFF);
out.write((Yr_HOD >> 8) & 0xFF); out.write((Yr_HOD >> 8) & 0xFF);
out.write(Yr_HOD & 0xFF); out.write(Yr_HOD & 0xFF);
int charW = getCharWidth(); int charW = getCharWidth(maxRows);
int charH = getCharHeight(); int charH = getCharHeight(maxRows);
out.write(charW); // AW out.write(charW); // AW
out.write(charH); // AH out.write(charH); // AH
int buf = maxCols * maxRows; int buf = maxCols * maxRows;
@@ -343,6 +435,11 @@ public class QueryReplyBuilder {
} }
public int getCharWidth() { public int getCharWidth() {
int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return getCharWidth(rows);
}
public int getCharWidth(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 12; return 12;
} }
@@ -350,18 +447,11 @@ public class QueryReplyBuilder {
} }
public int getCharHeight() { public int getCharHeight() {
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES: int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations: return getCharHeight(rows);
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes }
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
// GDDM then places the top menu bar at Row 1 (gy = 187..200). public int getCharHeight(int rows) {
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
//
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
} }
@@ -392,8 +482,8 @@ public class QueryReplyBuilder {
cpgid = screen.getTranslator().getCpgid(); cpgid = screen.getTranslator().getCpgid();
} }
if (graphicsMode == GraphicsMode.PROGRAMMED_SYMBOLS) { if (graphicsMode.isProgrammedSymbolsEnabled()) {
// Programmed Symbols only mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID) // Programmed Symbols mode (3279 PS / 3179G with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65); ByteArrayOutputStream out = new ByteArrayOutputStream(65);
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02) out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
out.write(0x00); // more flags out.write(0x00); // more flags
@@ -442,31 +532,15 @@ public class QueryReplyBuilder {
} }
public byte[] buildColor() { public byte[] buildColor() {
// Alphanumeric Color (matches HOD: 8 pairs, F1..F7 + default F4 green, 18 bytes payload / 22 bytes total) return STATIC_QR_COLOR.clone();
return new byte[] {
0x00, 0x08, 0x00, (byte) 0xF4,
(byte) 0xF1, (byte) 0xF1, // Blue
(byte) 0xF2, (byte) 0xF2, // Red
(byte) 0xF3, (byte) 0xF3, // Pink
(byte) 0xF4, (byte) 0xF4, // Green
(byte) 0xF5, (byte) 0xF5, // Turquoise
(byte) 0xF6, (byte) 0xF6, // Yellow
(byte) 0xF7, (byte) 0xF7 // Neutral/White
};
} }
public byte[] buildHighlighting() { public byte[] buildHighlighting() {
// Highlighting (matches HOD: 4 pairs: default F0, Blink F1, Reverse F2, Underscore F4, 9 bytes payload / 13 bytes total) return STATIC_QR_HIGHLIGHTING.clone();
return new byte[] {
0x04, 0x00, (byte) 0xF0,
(byte) 0xF1, (byte) 0xF1,
(byte) 0xF2, (byte) 0xF2,
(byte) 0xF4, (byte) 0xF4
};
} }
public byte[] buildReplyModes() { public byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR }; return STATIC_QR_REPLY_MODES.clone();
} }
public byte[] buildDdm() { public byte[] buildDdm() {
@@ -517,39 +591,27 @@ public class QueryReplyBuilder {
} }
public byte[] buildOutlining() { public byte[] buildOutlining() {
// HOD QueryReply3270Constants.java QR_OUTLINING_STRING ("\u0000\n\u0081\u008c\u0000\u0000\u0000\u0000\u0000\u0000") return STATIC_QR_OUTLINING.clone();
return new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
} }
public byte[] buildDbcsAsia() { public byte[] buildDbcsAsia() {
// HOD QueryReply3270Constants.java QR_DBCS_ASIA_STRING ("\u0000\u000b\u0081\u0091\u0000\u0003\u0001\u0080\u0003\u0002\u0001") return STATIC_QR_DBCS_ASIA.clone();
return new byte[]{ 0x00, 0x03, 0x01, (byte) 0x80, 0x03, 0x02, 0x01 };
} }
public byte[] buildAuxDa() { public byte[] buildAuxDa() {
// HOD QueryReply3270Constants.java QR_AUXDA_STRING ("\u0000\u0006\u0081\u0099\u0000\u0000") return STATIC_QR_AUXDA.clone();
return new byte[]{ 0x00, 0x00 };
} }
public byte[] buildTransparency() { public byte[] buildTransparency() {
// HOD QueryReply3270Constants.java QR_TRANSPARENCY_STRING ("\u0000\t\u0081\u00a8\u0002\u0000\u00f0\u00ff\u00ff") return STATIC_QR_TRANSPARENCY.clone();
return new byte[]{ 0x02, 0x00, (byte) 0xF0, (byte) 0xFF, (byte) 0xFF };
} }
public byte[] buildSegment() { public byte[] buildSegment() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS; return STATIC_QR_SEGMENT.clone();
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildSegment(maxCols, maxRows);
} }
public byte[] buildSegment(int maxCols, int maxRows) { public byte[] buildSegment(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_SEGMENT_STRING ("\u0000\u000b\u0081\u00b0\u0080\u0002\u0000\u0000\u0000\u00fc\u0000") return STATIC_QR_SEGMENT.clone();
return new byte[]{
(byte) 0x80, 0x02,
0x00, 0x00,
0x00, (byte) 0xFC,
0x00
};
} }
public byte[] buildGraphics() { public byte[] buildGraphics() {
@@ -561,21 +623,11 @@ public class QueryReplyBuilder {
} }
public byte[] buildProcedure() { public byte[] buildProcedure() {
int maxCols = (screen != null) ? screen.getMaxCols() : MODEL_2_COLS; return STATIC_QR_PROCEDURE.clone();
int maxRows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS;
return buildProcedure(maxCols, maxRows);
} }
public byte[] buildProcedure(int maxCols, int maxRows) { public byte[] buildProcedure(int maxCols, int maxRows) {
// HOD QueryReply3270Constants.java QR_PROCEDURE_STRING ("\u0000\u0015\u0081\u00b1\u0000\u0001\u0000\u0000\u0000\u00fc\u0000\u0006@\u0006@\u0006\u0001\u00ff\u00ff\u00ff\u00f0") return STATIC_QR_PROCEDURE.clone();
return new byte[]{
0x00, 0x01,
0x00, 0x00,
0x00, (byte) 0xFC,
0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
};
} }
public byte[] buildGImage() { public byte[] buildGImage() {
@@ -587,12 +639,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildLineType() { public byte[] buildLineType() {
// HOD QueryReply3270Constants.java QR_LINETYPE_STRING ("\u0000\u0018\u0081\u00b2\u0000\t\u0000\u0007\u0001\u0001\u0002\u0002\u0003\u0003\u0004\u0004\u0005\u0005\u0006\u0006\u0007\u0007\b\b") return STATIC_QR_LINETYPE.clone();
return new byte[]{
0x00, 0x09, 0x00, 0x07,
0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04,
0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08
};
} }
public byte[] buildAuxDev() { public byte[] buildAuxDev() {
@@ -604,19 +651,7 @@ public class QueryReplyBuilder {
} }
public void appendPort(ByteArrayOutputStream out) { public void appendPort(ByteArrayOutputStream out) {
// HOD QueryReply3270Constants.java QR_PORT_STRING (4 OEM format sub-fields, 64 bytes total) out.write(STATIC_PORT_BLOCKS, 0, STATIC_PORT_BLOCKS.length);
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x03, 0x02, (byte) 0x90, 0x09, 0x01, 0x00, 0x03, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x04, 0x08, 0x40, 0x07, 0x03, 0x00, 0x03, (byte) 0x80, 0x00, 0x02
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x05, 0x02, (byte) 0x80, 0x09, 0x01, 0x00, 0x01, 0x02, (byte) 0x80, 0x00, 0x7F, (byte) 0xFF
});
appendQueryReply(out, QR_PORT, new byte[]{
0x00, 0x07, 0x08, 0x40, 0x07, 0x03, 0x00, 0x01, 0x00, 0x00, 0x1C
});
} }
public void appendOemFmt(ByteArrayOutputStream out) { public void appendOemFmt(ByteArrayOutputStream out) {
@@ -624,9 +659,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildPort() { public byte[] buildPort() {
ByteArrayOutputStream out = new ByteArrayOutputStream(70); return STATIC_PORT_BLOCKS.clone();
appendPort(out);
return out.toByteArray();
} }
public byte[] buildOemFormat() { public byte[] buildOemFormat() {
@@ -634,24 +667,7 @@ public class QueryReplyBuilder {
} }
public byte[] buildGrColor() { public byte[] buildGrColor() {
// HOD QueryReply3270Constants.java QR_GRCOLOR_STRING (109 bytes total) return STATIC_QR_GRCOLOR.clone();
ByteArrayOutputStream out = new ByteArrayOutputStream(110);
out.write(0x00); out.write(0x04); out.write(0x00); out.write(0xFF); out.write(0xFF);
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x10);
for (int i = 0; i < 16; i++) {
out.write(0x00);
out.write(i);
int argb = haus.nightmare.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = argb & 0xFF;
out.write(r);
out.write(g);
out.write(b);
out.write(0x00); // 6th byte in HOD color table
}
return out.toByteArray();
} }
public byte[] buildGraphicColor() { public byte[] buildGraphicColor() {
@@ -665,16 +681,18 @@ public class QueryReplyBuilder {
public byte[] buildGrSymbolSet() { public byte[] buildGrSymbolSet() {
int charW = getCharWidth(); int charW = getCharWidth();
int charH = getCharHeight(); int charH = getCharHeight();
int cgcsgid = 0x02B9; if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
int cpgid = 0x0025; return new byte[]{
if (screen != null && screen.getTranslator() != null) { 0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
cgcsgid = screen.getTranslator().getCgcsgid(); 0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00, (byte) charW, (byte) charH,
cpgid = screen.getTranslator().getCpgid(); 0x00, 0x01, 0x4C, 0x01, 0x22, 0x00, 0x00, 0x12, 0x01, 0x00, 0x20, (byte) 0xF8, (byte) 0xC1, (byte) 0xC1,
0x00, 0x00, 0x18, 0x18, 0x00, 0x01, 0x72, 0x01, 0x2C, 0x00, (byte) 0xFF
};
} }
return new byte[]{ return new byte[]{
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
0x01, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00, 0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
(byte) charW, (byte) charH, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00 (byte) charW, (byte) charH, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
}; };
} }
@@ -0,0 +1,72 @@
package haus.nightmare.lib3270j.datastream;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Thread-safe memory pool providing reusable byte buffers for high-throughput
* 3270 stream operations, minimizing garbage collector pressure.
*/
public final class ReusableByteBufferPool {
public static final int SIZE_SMALL = 512;
public static final int SIZE_MEDIUM = 4096;
public static final int SIZE_LARGE = 32768;
private static final int MAX_POOLED_PER_TIER = 32;
private static final Queue<byte[]> smallPool = new ConcurrentLinkedQueue<>();
private static final Queue<byte[]> mediumPool = new ConcurrentLinkedQueue<>();
private static final Queue<byte[]> largePool = new ConcurrentLinkedQueue<>();
private ReusableByteBufferPool() {}
/**
* Acquires a pooled byte array with at least the specified capacity.
*/
public static byte[] acquire(int minCapacity) {
if (minCapacity <= SIZE_SMALL) {
byte[] b = smallPool.poll();
return (b != null) ? b : new byte[SIZE_SMALL];
} else if (minCapacity <= SIZE_MEDIUM) {
byte[] b = mediumPool.poll();
return (b != null) ? b : new byte[SIZE_MEDIUM];
} else if (minCapacity <= SIZE_LARGE) {
byte[] b = largePool.poll();
return (b != null) ? b : new byte[SIZE_LARGE];
}
return new byte[minCapacity];
}
/**
* Acquires a ByteBuffer wrapping a pooled array up to minCapacity.
*/
public static ByteBuffer acquireByteBuffer(int minCapacity) {
byte[] b = acquire(minCapacity);
return ByteBuffer.wrap(b);
}
/**
* Returns a buffer to the pool for reuse if it matches a standard tier.
*/
public static void release(byte[] buffer) {
if (buffer == null) return;
if (buffer.length == SIZE_SMALL && smallPool.size() < MAX_POOLED_PER_TIER) {
smallPool.offer(buffer);
} else if (buffer.length == SIZE_MEDIUM && mediumPool.size() < MAX_POOLED_PER_TIER) {
mediumPool.offer(buffer);
} else if (buffer.length == SIZE_LARGE && largePool.size() < MAX_POOLED_PER_TIER) {
largePool.offer(buffer);
}
}
/**
* Clears all pools to release held memory.
*/
public static void clear() {
smallPool.clear();
mediumPool.clear();
largePool.clear();
}
}
@@ -77,6 +77,34 @@ public class ECLConnection {
if (props != null) { if (props != null) {
this.properties.putAll(props); this.properties.putAll(props);
convertData(this.properties); convertData(this.properties);
String h = this.properties.getProperty(ECLSession.SESSION_HOST);
if (h == null) h = this.properties.getProperty("host");
if (h == null) h = this.properties.getProperty("Host");
if (h == null) h = this.properties.getProperty("SESSION_PRINT_SESSION_HOST");
if (h != null && !h.trim().isEmpty()) this.host = h.trim();
String p = this.properties.getProperty(ECLSession.SESSION_PORT);
if (p == null) p = this.properties.getProperty("port");
if (p == null) p = this.properties.getProperty("Port");
if (p == null) p = this.properties.getProperty("SESSION_HOST_PORT");
if (p == null) p = this.properties.getProperty("SESSION_PRINT_SESSION_HOST_PORT");
if (p != null && !p.trim().isEmpty()) {
try { this.port = Integer.parseInt(p.trim()); } catch (NumberFormatException ignored) {}
}
String cp = this.properties.getProperty(ECLSession.SESSION_CODE_PAGE);
if (cp == null) cp = this.properties.getProperty("code_page");
if (cp == null) cp = this.properties.getProperty("codepage");
if (cp == null) cp = this.properties.getProperty("CodePage");
if (cp != null && !cp.trim().isEmpty()) this.codePage = cp.trim();
String lu = this.properties.getProperty(ECLSession.SESSION_LU_NAME);
if (lu == null) lu = this.properties.getProperty("lu_name");
if (lu == null) lu = this.properties.getProperty("luname");
if (lu == null) lu = this.properties.getProperty("SESSION_PRINT_SESSION_LU_NAME");
if (lu != null && !lu.trim().isEmpty()) this.luName = lu.trim();
String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK); String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
if (asu != null) { if (asu != null) {
this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu); this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu);
@@ -11,6 +11,8 @@ public class ECLErr extends Exception {
private String id = ""; private String id = "";
private String text = ""; private String text = "";
private int reasonCode = 0;
public ECLErr() { public ECLErr() {
super(); super();
} }
@@ -20,6 +22,21 @@ public class ECLErr extends Exception {
this.text = text; this.text = text;
} }
public ECLErr(String location, String msgNum, int reasonCode) {
super(formatMsg(location, msgNum, "Reason " + reasonCode));
this.tag = location;
this.id = msgNum;
this.text = "Reason " + reasonCode;
this.reasonCode = reasonCode;
}
public ECLErr(String location, String msgNum) {
super(formatMsg(location, msgNum, ""));
this.tag = location;
this.id = msgNum;
this.text = "";
}
public ECLErr(String tag, String id, String text) { public ECLErr(String tag, String id, String text) {
super(formatMsg(tag, id, text)); super(formatMsg(tag, id, text));
this.tag = tag; this.tag = tag;
@@ -27,11 +44,30 @@ public class ECLErr extends Exception {
this.text = text; this.text = text;
} }
public ECLErr(String tag, String id, String text, String extra) { public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
super(formatMsg(tag, id, text + " " + extra)); super(formatMsg(location, msgNum, insert1 + " (Reason " + reasonCode + ")"));
this.tag = tag; this.tag = location;
this.id = id; this.id = msgNum;
this.text = text + " " + extra; this.text = insert1 + " (Reason " + reasonCode + ")";
this.reasonCode = reasonCode;
}
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
super(formatMsg(location, msgNum, insert1 + " " + insert2));
this.tag = location;
this.id = msgNum;
this.text = insert1 + " " + insert2;
}
public ECLErr(String location, String msgNum, String insert1, String insert2) {
this(location, msgNum, insert1, insert2, false);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
super(formatMsg(location, msgNum, insert1 + " " + insert2 + " " + insert3));
this.tag = location;
this.id = msgNum;
this.text = insert1 + " " + insert2 + " " + insert3;
} }
public ECLErr(Throwable cause) { public ECLErr(Throwable cause) {
@@ -57,14 +93,33 @@ public class ECLErr extends Exception {
public String getID() { return id; } public String getID() { return id; }
public String GetID() { return id; } public String GetID() { return id; }
public String getLocation() { return tag; }
public String GetLocation() { return tag; }
public int getReasonCode() { return reasonCode; }
public int GetReasonCode() { return reasonCode; }
public int getMsgNumber() {
if (id != null && id.length() >= 7 && id.startsWith("ECL")) {
try {
return Integer.parseInt(id.substring(3, 7));
} catch (NumberFormatException ignored) {}
}
return 0;
}
public int GetMsgNumber() { return getMsgNumber(); }
public String getText() { return text; } public String getText() { return text; }
public String GetText() { return text; } public String GetText() { return text; }
public String getMsgText() { return getMessage(); }
public String GetMsgText() { return getMessage(); }
public String getErrorText() { return getMessage(); } public String getErrorText() { return getMessage(); }
public String GetErrorText() { return getMessage(); } public String GetErrorText() { return getMessage(); }
@Override @Override
public String toString() { public String toString() {
return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + "]"; return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + ", reasonCode=" + reasonCode + "]";
} }
} }

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