Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0201c7c277
|
|||
|
b61702a57c
|
|||
|
bd4c1f3040
|
|||
|
0d6ab31f0a
|
|||
|
1f0e36c561
|
|||
|
8286af19e6
|
|||
|
3d3a939ece
|
|||
|
d05f1a4c45
|
|||
|
aa724fbcef
|
|||
|
8a30efe9c0
|
|||
|
5049d8df74
|
|||
|
badd026055
|
|||
|
ef70ea0f5a
|
|||
|
30b1a77b61
|
|||
|
9bd1676ec1
|
|||
|
cfb32bdca5
|
|||
|
754b1de7cb
|
|||
|
7892229a2c
|
|||
|
14bf7ba4b1
|
|||
|
bdfe6eec2a
|
|||
|
c28c097e25
|
@@ -20,21 +20,21 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Java JDK (Temurin 17)
|
||||
- name: Set up Java JDK (Temurin 11)
|
||||
uses: actions/setup-java@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Ensure Java Available
|
||||
run: |
|
||||
if ! command -v javac >/dev/null 2>&1; then
|
||||
echo "Installing OpenJDK 17 via package manager..."
|
||||
echo "Installing OpenJDK 11 via package manager..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update && apt-get install -y openjdk-17-jdk-headless || true
|
||||
apt-get update && apt-get install -y openjdk-11-jdk-headless || true
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache openjdk17 || true
|
||||
apk add --no-cache openjdk11 || true
|
||||
fi
|
||||
fi
|
||||
echo "Java compiler:"
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="124 738 320 320"
|
||||
width="320"
|
||||
height="320"
|
||||
xml:space="preserve"
|
||||
id="svg_j3270_icon"
|
||||
version="1.1">
|
||||
<metadata id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
|
||||
<rect x="124" y="738" width="320" height="320" fill="#000000" />
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g id="g14">
|
||||
<!-- Green Terminal Screen Frame -->
|
||||
<path
|
||||
id="path28"
|
||||
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
|
||||
<!-- Blue Wave / Exponential Curve -->
|
||||
<path
|
||||
id="path30"
|
||||
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
|
||||
<!-- Red Circular Arc -->
|
||||
<path
|
||||
id="path32"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
|
||||
<!-- Yellow Smiley Head -->
|
||||
<path
|
||||
id="path34"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
|
||||
<!-- 'l' -->
|
||||
<path
|
||||
id="path36"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<path
|
||||
id="path38"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<!-- 'i' -->
|
||||
<path
|
||||
id="path40"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<path
|
||||
id="path42"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<!-- 'b' -->
|
||||
<path
|
||||
id="path44"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<path
|
||||
id="path46"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<!-- '3' -->
|
||||
<path
|
||||
id="path48"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<path
|
||||
id="path50"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<!-- '2' -->
|
||||
<path
|
||||
id="path52"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<path
|
||||
id="path54"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<!-- '7' -->
|
||||
<path
|
||||
id="path56"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<path
|
||||
id="path58"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<!-- '0' -->
|
||||
<path
|
||||
id="path60"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<path
|
||||
id="path62"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<!-- 'j' -->
|
||||
<path
|
||||
id="path64"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<path
|
||||
id="path66"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<!-- Smile -->
|
||||
<path
|
||||
id="path68"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
|
||||
<!-- Left Eye (cyan) -->
|
||||
<path
|
||||
id="path70"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
|
||||
<!-- Right Eye (cyan) -->
|
||||
<path
|
||||
id="path72"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
|
||||
<!-- Nose -->
|
||||
<path
|
||||
id="path74"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2892.03,1528.23 -29.21,-20.68" />
|
||||
<path
|
||||
id="path76"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2862.82,1507.55 h 32.86" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="144 814 280 169"
|
||||
width="280"
|
||||
height="169"
|
||||
xml:space="preserve"
|
||||
id="svg_lib3270j_cropped"
|
||||
version="1.1">
|
||||
<metadata id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
|
||||
<rect x="144" y="814" width="280" height="169" fill="#000000" />
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g id="g14">
|
||||
<!-- Green Terminal Screen Frame -->
|
||||
<path
|
||||
id="path28"
|
||||
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
|
||||
<!-- Blue Wave / Exponential Curve -->
|
||||
<path
|
||||
id="path30"
|
||||
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
|
||||
<!-- Red Circular Arc -->
|
||||
<path
|
||||
id="path32"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
|
||||
<!-- Yellow Smiley Head -->
|
||||
<path
|
||||
id="path34"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
|
||||
<!-- 'l' -->
|
||||
<path
|
||||
id="path36"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<path
|
||||
id="path38"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<!-- 'i' -->
|
||||
<path
|
||||
id="path40"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<path
|
||||
id="path42"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<!-- 'b' -->
|
||||
<path
|
||||
id="path44"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<path
|
||||
id="path46"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<!-- '3' -->
|
||||
<path
|
||||
id="path48"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<path
|
||||
id="path50"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<!-- '2' -->
|
||||
<path
|
||||
id="path52"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<path
|
||||
id="path54"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<!-- '7' -->
|
||||
<path
|
||||
id="path56"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<path
|
||||
id="path58"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<!-- '0' -->
|
||||
<path
|
||||
id="path60"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<path
|
||||
id="path62"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<!-- 'j' -->
|
||||
<path
|
||||
id="path64"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<path
|
||||
id="path66"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<!-- Smile -->
|
||||
<path
|
||||
id="path68"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
|
||||
<!-- Left Eye (cyan) -->
|
||||
<path
|
||||
id="path70"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
|
||||
<!-- Right Eye (cyan) -->
|
||||
<path
|
||||
id="path72"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
|
||||
<!-- Nose -->
|
||||
<path
|
||||
id="path74"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2892.03,1528.23 -29.21,-20.68" />
|
||||
<path
|
||||
id="path76"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2862.82,1507.55 h 32.86" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -3,6 +3,11 @@
|
||||
; or: ./run.sh -c j3270.ini
|
||||
|
||||
[appearance]
|
||||
; UI font settings (menus, dialogs, status bar)
|
||||
; uiFontFamily = SansSerif
|
||||
; uiFontSize = 13
|
||||
;
|
||||
; Terminal font settings (3270 screen display)
|
||||
; fontFamily = IBM 3270
|
||||
; fontSize = 18
|
||||
|
||||
@@ -10,6 +15,7 @@
|
||||
; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING
|
||||
; autoConnectHost = mainframe.example.com
|
||||
; autoConnectPort = 23
|
||||
; blockPaste = true
|
||||
|
||||
[colors]
|
||||
; Host colors 0-15 (hex RGB)
|
||||
|
||||
@@ -27,6 +27,30 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
private static final Logger log = Logger.getLogger(J3270App.class.getName());
|
||||
|
||||
// Active open windows tracking
|
||||
private static final java.util.List<J3270App> openWindows = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
|
||||
public static java.util.List<J3270App> getOpenWindows() {
|
||||
return java.util.Collections.unmodifiableList(openWindows);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConnectChoiceHandler {
|
||||
ConnectChoiceDialog.Choice askChoice(Window owner, String currentTarget);
|
||||
}
|
||||
|
||||
private ConnectChoiceHandler connectChoiceHandler = null;
|
||||
|
||||
public void setConnectChoiceHandler(ConnectChoiceHandler handler) {
|
||||
this.connectChoiceHandler = handler;
|
||||
}
|
||||
|
||||
public ConnectChoiceHandler getConnectChoiceHandler() {
|
||||
return connectChoiceHandler;
|
||||
}
|
||||
|
||||
private final java.util.function.Consumer<UITheme> themeChangeListener = this::onThemeChanged;
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
private StatusBar statusBar;
|
||||
@@ -42,8 +66,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
// Dialog instances
|
||||
private FindDialog findDialog;
|
||||
private ScriptDialog scriptDialog;
|
||||
private MacroDialog macroDialog;
|
||||
private FieldInspectorDialog fieldInspectorDialog;
|
||||
private PrinterSessionDialog printerSessionDialog;
|
||||
private ScreenHistoryDialog screenHistoryDialog;
|
||||
|
||||
// Modes menu items
|
||||
private JCheckBoxMenuItem docModeItem;
|
||||
@@ -58,13 +84,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
public J3270App() {
|
||||
super("j3270 — Java TN3270 Terminal Emulator");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
setBackground(Color.BLACK);
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
openWindows.add(this);
|
||||
|
||||
buildUI();
|
||||
buildMenuBar();
|
||||
|
||||
ThemeManager.addThemeChangeListener(this::onThemeChanged);
|
||||
ThemeManager.addThemeChangeListener(themeChangeListener);
|
||||
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
@@ -75,7 +104,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
if (client != null) {
|
||||
statusBar.updateStatus();
|
||||
syncModeMenuItems();
|
||||
if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
|
||||
if (!isModalDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
|
||||
if (!terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
@@ -84,38 +113,62 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
});
|
||||
refreshTimer.start();
|
||||
|
||||
setAutoRequestFocus(true);
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
disconnect();
|
||||
if (refreshTimer != null)
|
||||
refreshTimer.stop();
|
||||
if (printerSessionDialog != null) {
|
||||
printerSessionDialog.dispose();
|
||||
}
|
||||
terminalPanel.dispose();
|
||||
closeWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
if (!isAnyChildDialogActive()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
if (!isModalDialogActive() && terminalPanel != null) {
|
||||
terminalPanel.requestFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addWindowFocusListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowGainedFocus(WindowEvent e) {
|
||||
if (!isModalDialogActive() && terminalPanel != null) {
|
||||
terminalPanel.requestFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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 && !isModalDialogActive()) {
|
||||
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
|
||||
J3270App.this.toFront();
|
||||
}
|
||||
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.requestFocus();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (terminalPanel != null && !terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, AWTEvent.MOUSE_EVENT_MASK);
|
||||
}
|
||||
|
||||
private boolean isAnyChildDialogActive() {
|
||||
Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
|
||||
if (focusedWindow != null && focusedWindow != this) {
|
||||
return true;
|
||||
}
|
||||
if (findDialog != null && findDialog.isVisible()) return true;
|
||||
if (scriptDialog != null && scriptDialog.isVisible()) return true;
|
||||
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
|
||||
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
|
||||
private boolean isModalDialogActive() {
|
||||
for (Window owned : getOwnedWindows()) {
|
||||
if (owned != null && owned.isVisible()) {
|
||||
if (owned instanceof Dialog && owned.isVisible() && ((Dialog) owned).isModal()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -166,6 +219,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
||||
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Close Window", KeyEvent.VK_W, this::closeWindow));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel)));
|
||||
fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog));
|
||||
fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen));
|
||||
@@ -173,13 +228,51 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
|
||||
fileMenu.addSeparator();
|
||||
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
|
||||
disconnect();
|
||||
for (J3270App win : new java.util.ArrayList<>(openWindows)) {
|
||||
win.disconnect();
|
||||
win.dispose();
|
||||
}
|
||||
System.exit(0);
|
||||
}));
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
// 2. Edit menu
|
||||
// 2. Connection menu
|
||||
JMenu connectionMenu = createMenu("Connection");
|
||||
connectionMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
|
||||
connectionMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
|
||||
connectionMenu.addSeparator();
|
||||
|
||||
// Host Type Submenu (CMS/TSO/CICS)
|
||||
JMenu hostTypeMenu = createMenu("Host Type");
|
||||
ButtonGroup hostTypeGroup = new ButtonGroup();
|
||||
for (haus.nightmare.lib3270j.ft.FTConfig.HostType ht : haus.nightmare.lib3270j.ft.FTConfig.HostType.values()) {
|
||||
JRadioButtonMenuItem htItem = new JRadioButtonMenuItem(ht.name());
|
||||
ThemeManager.styleMenuItem(htItem);
|
||||
hostTypeGroup.add(htItem);
|
||||
htItem.addActionListener(e -> setConnectionHostType(ht));
|
||||
hostTypeMenu.add(htItem);
|
||||
}
|
||||
connectionMenu.add(hostTypeMenu);
|
||||
connectionMenu.addSeparator();
|
||||
|
||||
// Saved Connections Submenu
|
||||
JMenu savedHostsMenu = createMenu("Saved Connections");
|
||||
connectionMenu.add(savedHostsMenu);
|
||||
|
||||
connectionMenu.addMenuListener(new javax.swing.event.MenuListener() {
|
||||
@Override
|
||||
public void menuSelected(javax.swing.event.MenuEvent e) {
|
||||
updateConnectionMenu(hostTypeMenu, hostTypeGroup, savedHostsMenu);
|
||||
}
|
||||
@Override public void menuDeselected(javax.swing.event.MenuEvent e) {}
|
||||
@Override public void menuCanceled(javax.swing.event.MenuEvent e) {}
|
||||
});
|
||||
|
||||
menuBar.add(connectionMenu);
|
||||
|
||||
// 3. Edit menu
|
||||
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("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
|
||||
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
|
||||
@@ -311,6 +404,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
viewMenu.add(gfxMenu);
|
||||
|
||||
viewMenu.addSeparator();
|
||||
viewMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
|
||||
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
@@ -429,6 +523,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}, true));
|
||||
actionsMenu.addSeparator();
|
||||
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));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
@@ -535,6 +631,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
scriptDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showMacroDialog() {
|
||||
if (macroDialog == null) {
|
||||
macroDialog = new MacroDialog(this, client, terminalPanel);
|
||||
}
|
||||
macroDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showFieldInspectorDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
@@ -558,6 +661,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
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() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
@@ -684,7 +799,20 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
// ========== Connection management ==========
|
||||
|
||||
private void showConnectDialog() {
|
||||
public void showConnectDialog() {
|
||||
boolean wasConnected = isConnected();
|
||||
if (wasConnected && (!GraphicsEnvironment.isHeadless() || connectChoiceHandler != null)) {
|
||||
ConnectChoiceDialog.Choice choice = promptConnectConflict();
|
||||
if (choice == ConnectChoiceDialog.Choice.NEW_WINDOW) {
|
||||
J3270App newWindow = createNewWindow(null);
|
||||
SwingUtilities.invokeLater(newWindow::showConnectDialog);
|
||||
return;
|
||||
} else if (choice == ConnectChoiceDialog.Choice.CANCEL) {
|
||||
return;
|
||||
}
|
||||
// choice == ABORT_CURRENT: proceed to showConnectDialog in this window
|
||||
}
|
||||
|
||||
ConnectDialog dialog = new ConnectDialog(this);
|
||||
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||
int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||
@@ -693,13 +821,77 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
||||
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
||||
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
|
||||
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
dialog.setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed()) {
|
||||
ConnectionConfig config = dialog.getConnectionConfig();
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
connect(config);
|
||||
connect(config, !wasConnected);
|
||||
}
|
||||
}
|
||||
|
||||
public void setConnectionHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ht) {
|
||||
if (ht == null) return;
|
||||
haus.nightmare.j3270.config.Settings.setDefaultHostType(ht.name());
|
||||
if (client != null && client.getConfig() != null) {
|
||||
client.getConfig().setFtHostType(ht);
|
||||
String profileId = client.getConfig().getSavedProfileId();
|
||||
if (profileId != null) {
|
||||
haus.nightmare.j3270.storage.SavedHost saved = haus.nightmare.j3270.storage.HostStorage.findById(profileId);
|
||||
if (saved != null) {
|
||||
saved.setHostType(ht.name());
|
||||
haus.nightmare.j3270.storage.HostStorage.saveHost(saved);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (FileTransferDialog.getLastTransferState() != null) {
|
||||
FileTransferDialog.getLastTransferState().hostType = ht;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateConnectionMenu(JMenu hostTypeMenu, ButtonGroup hostTypeGroup, JMenu savedHostsMenu) {
|
||||
haus.nightmare.lib3270j.ft.FTConfig.HostType currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
|
||||
if (client != null && client.getConfig() != null) {
|
||||
currentHt = client.getConfig().getFtHostType();
|
||||
} else {
|
||||
String def = haus.nightmare.j3270.config.Settings.getDefaultHostType();
|
||||
try {
|
||||
if (def != null) currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(def.trim().toUpperCase());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
for (int i = 0; i < hostTypeMenu.getItemCount(); i++) {
|
||||
JMenuItem item = hostTypeMenu.getItem(i);
|
||||
if (item instanceof JRadioButtonMenuItem) {
|
||||
JRadioButtonMenuItem rb = (JRadioButtonMenuItem) item;
|
||||
if (rb.getText().equalsIgnoreCase(currentHt.name())) {
|
||||
rb.setSelected(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
savedHostsMenu.removeAll();
|
||||
java.util.List<haus.nightmare.j3270.storage.SavedHost> savedList = haus.nightmare.j3270.storage.HostStorage.getSavedHosts();
|
||||
if (savedList.isEmpty()) {
|
||||
JMenuItem noneItem = new JMenuItem("(No Saved Connections)");
|
||||
noneItem.setEnabled(false);
|
||||
ThemeManager.styleMenuItem(noneItem);
|
||||
savedHostsMenu.add(noneItem);
|
||||
} else {
|
||||
for (haus.nightmare.j3270.storage.SavedHost sh : savedList) {
|
||||
String label = sh.getName();
|
||||
if (label == null || label.trim().isEmpty()) {
|
||||
label = sh.getHost() + ":" + sh.getPort();
|
||||
}
|
||||
String ht = (sh.getHostType() != null && !sh.getHostType().trim().isEmpty()) ? sh.getHostType().trim() : "TSO";
|
||||
JMenuItem hostItem = new JMenuItem(label + " [" + ht + "]");
|
||||
ThemeManager.styleMenuItem(hostItem);
|
||||
hostItem.addActionListener(ev -> connect(sh.toConnectionConfig(), true));
|
||||
savedHostsMenu.add(hostItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +900,79 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
public void closeWindow() {
|
||||
disconnect();
|
||||
if (refreshTimer != null) {
|
||||
refreshTimer.stop();
|
||||
}
|
||||
if (printerSessionDialog != null) {
|
||||
printerSessionDialog.dispose();
|
||||
}
|
||||
if (screenHistoryDialog != null) {
|
||||
screenHistoryDialog.dispose();
|
||||
}
|
||||
terminalPanel.dispose();
|
||||
ThemeManager.removeThemeChangeListener(themeChangeListener);
|
||||
openWindows.remove(this);
|
||||
dispose();
|
||||
if (openWindows.isEmpty() && !GraphicsEnvironment.isHeadless()) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
openWindows.remove(this);
|
||||
ThemeManager.removeThemeChangeListener(themeChangeListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return client != null && (client.isConnected() ||
|
||||
(client.getConnectionState() != null && client.getConnectionState().isConnected()));
|
||||
}
|
||||
|
||||
public String getConnectedTarget() {
|
||||
if (client != null && client.getConfig() != null) {
|
||||
String profileName = client.getConfig().getSavedProfileName();
|
||||
String host = client.getConfig().getHost();
|
||||
int port = client.getConfig().getPort();
|
||||
if (profileName != null && !profileName.trim().isEmpty()) {
|
||||
return profileName + " (" + host + ":" + port + ")";
|
||||
}
|
||||
if (host != null && !host.trim().isEmpty()) {
|
||||
return host + ":" + port;
|
||||
}
|
||||
}
|
||||
if (lastHost != null && !lastHost.trim().isEmpty()) {
|
||||
return lastHost + ":" + lastPort;
|
||||
}
|
||||
return "the current host";
|
||||
}
|
||||
|
||||
public J3270App createNewWindow(ConnectionConfig config) {
|
||||
J3270App newApp = new J3270App();
|
||||
try {
|
||||
Point loc = getLocation();
|
||||
newApp.setLocation(loc.x + 30, loc.y + 30);
|
||||
} catch (Exception ignored) {}
|
||||
newApp.setVisible(true);
|
||||
if (config != null) {
|
||||
newApp.connect(config, false);
|
||||
}
|
||||
return newApp;
|
||||
}
|
||||
|
||||
public ConnectChoiceDialog.Choice promptConnectConflict() {
|
||||
if (connectChoiceHandler != null) {
|
||||
return connectChoiceHandler.askChoice(this, getConnectedTarget());
|
||||
}
|
||||
if (GraphicsEnvironment.isHeadless()) {
|
||||
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
|
||||
}
|
||||
return ConnectChoiceDialog.showPrompt(this, getConnectedTarget());
|
||||
}
|
||||
|
||||
public TerminalPanel getTerminalPanel() {
|
||||
return terminalPanel;
|
||||
}
|
||||
@@ -716,7 +981,26 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
return client;
|
||||
}
|
||||
|
||||
void connect(ConnectionConfig config) {
|
||||
public StatusBar getStatusBar() {
|
||||
return statusBar;
|
||||
}
|
||||
|
||||
public void connect(ConnectionConfig config) {
|
||||
connect(config, true);
|
||||
}
|
||||
|
||||
public void connect(ConnectionConfig config, boolean promptIfConnected) {
|
||||
if (promptIfConnected && isConnected() && (!GraphicsEnvironment.isHeadless() || connectChoiceHandler != null)) {
|
||||
ConnectChoiceDialog.Choice choice = promptConnectConflict();
|
||||
if (choice == ConnectChoiceDialog.Choice.NEW_WINDOW) {
|
||||
createNewWindow(config);
|
||||
return;
|
||||
} else if (choice == ConnectChoiceDialog.Choice.CANCEL) {
|
||||
return;
|
||||
}
|
||||
// choice == ABORT_CURRENT: proceed to connect in this window
|
||||
}
|
||||
|
||||
lastHost = config.getHost();
|
||||
lastPort = config.getPort();
|
||||
|
||||
@@ -725,8 +1009,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
||||
if (config.getHostType() != null) {
|
||||
haus.nightmare.j3270.config.Settings.setDefaultHostType(config.getHostType());
|
||||
}
|
||||
if (FileTransferDialog.getLastTransferState() != null && config.isSavedConnection()) {
|
||||
FileTransferDialog.getLastTransferState().hostType = config.getFtHostType();
|
||||
}
|
||||
if (config.getGraphicsMode() != null) {
|
||||
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
|
||||
}
|
||||
|
||||
disconnect();
|
||||
Telnet3270Client oldClient = client;
|
||||
if (oldClient != null) {
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
if (macroDialog != null) {
|
||||
macroDialog.stopMacro();
|
||||
}
|
||||
oldClient.removeConnectionListener(this);
|
||||
oldClient.removeScreenUpdateListener(this);
|
||||
client = null;
|
||||
}
|
||||
|
||||
if (config.getCertificateVerifier() == null) {
|
||||
config.setCertificateVerifier((chain, authType, exception) ->
|
||||
@@ -744,10 +1049,27 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
||||
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
|
||||
|
||||
terminalPanel.guardedPack();
|
||||
int targetRows = 24;
|
||||
int targetCols = 80;
|
||||
TerminalModel model = config.getModel();
|
||||
if (model != null) {
|
||||
if (model.isDynamic()) {
|
||||
targetRows = config.getDynamicRows();
|
||||
targetCols = config.getDynamicCols();
|
||||
} else {
|
||||
targetRows = model.getAlternateRows();
|
||||
targetCols = model.getAlternateCols();
|
||||
}
|
||||
}
|
||||
terminalPanel.packForModel(targetRows, targetCols);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (oldClient != null) {
|
||||
try {
|
||||
oldClient.disconnect(1000);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
client.connect();
|
||||
} catch (IOException e) {
|
||||
log.log(Level.WARNING, "Connection failed", e);
|
||||
@@ -762,31 +1084,43 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
public void disconnect() {
|
||||
disconnect(true);
|
||||
}
|
||||
|
||||
public void disconnect(boolean resetUi) {
|
||||
if (client != null) {
|
||||
if (fileTransfer != null) {
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
if (macroDialog != null) {
|
||||
macroDialog.stopMacro();
|
||||
}
|
||||
Telnet3270Client c = client;
|
||||
client = null;
|
||||
c.removeConnectionListener(this);
|
||||
c.removeScreenUpdateListener(this);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
c.disconnect();
|
||||
c.disconnect(1000);
|
||||
} catch (Exception ignored) {}
|
||||
}, "Disconnect-Thread").start();
|
||||
|
||||
Runnable uiReset = () -> {
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null, terminalPanel);
|
||||
syncModeMenuItems();
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
};
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
uiReset.run();
|
||||
} else {
|
||||
SwingUtilities.invokeLater(uiReset);
|
||||
if (resetUi) {
|
||||
Runnable uiReset = () -> {
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null, terminalPanel);
|
||||
syncModeMenuItems();
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
};
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
uiReset.run();
|
||||
} else {
|
||||
SwingUtilities.invokeLater(uiReset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -838,7 +1172,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ?
|
||||
" [" + client.getTelnetFSM().getConnectedLu() + "]" : "";
|
||||
setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev);
|
||||
terminalPanel.guardedPack();
|
||||
terminalPanel.revalidate();
|
||||
terminalPanel.repaint();
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
});
|
||||
@@ -889,7 +1224,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
terminalPanel.guardedPack();
|
||||
terminalPanel.revalidate();
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
@@ -945,7 +1281,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
area.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextArea(area);
|
||||
JScrollPane sp = new JScrollPane(area);
|
||||
ThemeManager.styleScrollPane(sp);
|
||||
@@ -978,7 +1314,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
JTextArea area = new JTextArea(text);
|
||||
area.setEditable(false);
|
||||
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
area.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextArea(area);
|
||||
JScrollPane sp = new JScrollPane(area);
|
||||
ThemeManager.styleScrollPane(sp);
|
||||
@@ -1002,6 +1338,109 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
"About j3270", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure application logging. When debug is false, disables all logging
|
||||
* to console and file, preventing creation of j3270.log.
|
||||
*/
|
||||
public static void configureLogging(boolean debug) {
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
try {
|
||||
h.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
Level logLevel = Level.ALL;
|
||||
globalRoot.setLevel(Level.ALL);
|
||||
|
||||
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
|
||||
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(Level.ALL);
|
||||
consoleHandler.setFormatter(new SimpleFormatter());
|
||||
consoleHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(consoleHandler);
|
||||
|
||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||
|
||||
try {
|
||||
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||
@Override
|
||||
public synchronized void publish(java.util.logging.LogRecord record) {
|
||||
super.publish(record);
|
||||
flush();
|
||||
}
|
||||
};
|
||||
fileHandler.setLevel(Level.ALL);
|
||||
fileHandler.setFormatter(new SimpleFormatter());
|
||||
fileHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(fileHandler);
|
||||
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
||||
} catch (Exception e) {
|
||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
globalRoot.setLevel(Level.OFF);
|
||||
for (String pkg : new String[]{"haus.nightmare", "haus.nightmare.j3270", "haus.nightmare.lib3270j", "org.pubvm"}) {
|
||||
Logger l = Logger.getLogger(pkg);
|
||||
l.setLevel(Level.OFF);
|
||||
for (java.util.logging.Handler h : l.getHandlers()) {
|
||||
l.removeHandler(h);
|
||||
try {
|
||||
h.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -1012,6 +1451,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
Boolean cliAutoSysUnlock = null;
|
||||
GraphicsMode cliGraphicsMode = null;
|
||||
String configFile = null;
|
||||
String cliProxyUrl = null;
|
||||
String cliProfileName = null;
|
||||
String cliHostType = null;
|
||||
|
||||
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
@@ -1030,6 +1472,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
cliAutoSysUnlock = true;
|
||||
} else if ("--no-auto-sys-unlock".equals(arg)) {
|
||||
cliAutoSysUnlock = false;
|
||||
} else if (arg.startsWith("--host-type=")) {
|
||||
cliHostType = arg.substring(12).trim().toUpperCase();
|
||||
} else if (("--host-type".equals(arg) || "-ht".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||
cliHostType = args[++i].trim().toUpperCase();
|
||||
} 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("-")) {
|
||||
@@ -1038,6 +1484,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
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 {
|
||||
@@ -1045,42 +1499,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
}
|
||||
|
||||
Level logLevel = debug ? Level.FINE : Level.INFO;
|
||||
configureLogging(debug);
|
||||
|
||||
Logger globalRoot = Logger.getLogger("");
|
||||
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
|
||||
globalRoot.removeHandler(h);
|
||||
}
|
||||
|
||||
java.util.logging.Filter appFilter = record -> record.getLoggerName() != null &&
|
||||
(record.getLoggerName().startsWith("haus.nightmare") || record.getLoggerName().startsWith("org.pubvm"));
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(Level.ALL);
|
||||
consoleHandler.setFormatter(new SimpleFormatter());
|
||||
consoleHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(consoleHandler);
|
||||
|
||||
Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
|
||||
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
|
||||
|
||||
try {
|
||||
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
|
||||
@Override
|
||||
public synchronized void publish(java.util.logging.LogRecord record) {
|
||||
super.publish(record);
|
||||
flush();
|
||||
}
|
||||
};
|
||||
fileHandler.setLevel(Level.ALL);
|
||||
fileHandler.setFormatter(new SimpleFormatter());
|
||||
fileHandler.setFilter(appFilter);
|
||||
globalRoot.addHandler(fileHandler);
|
||||
log.info("Logging protocol trace to j3270.log (debug=" + debug + ", level=" + logLevel + ")");
|
||||
} catch (Exception e) {
|
||||
System.err.println("Could not create j3270.log: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (configFile != null) {
|
||||
try {
|
||||
@@ -1108,11 +1528,35 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
final Boolean finalTn3270e = cliTn3270e;
|
||||
final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
|
||||
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||
final String finalProxyUrl = cliProxyUrl;
|
||||
final String finalProfileName = cliProfileName;
|
||||
final String finalHostType = cliHostType;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
J3270App app = new J3270App();
|
||||
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 (finalHostType != null) config.setHostType(finalHostType);
|
||||
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()) {
|
||||
String hostArg = remainingArgs.get(0);
|
||||
int port = finalTls ? 992 : 23;
|
||||
@@ -1153,6 +1597,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
if (finalProxyUrl != null) {
|
||||
applyProxyUrl(config, finalProxyUrl);
|
||||
}
|
||||
if (finalHostType != null) {
|
||||
config.setHostType(finalHostType);
|
||||
} else {
|
||||
config.setHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
|
||||
}
|
||||
app.connect(config);
|
||||
} else {
|
||||
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
|
||||
@@ -1162,26 +1614,47 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
break;
|
||||
case AUTO_CONNECT:
|
||||
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
|
||||
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert();
|
||||
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e();
|
||||
if (host != null && !host.isEmpty()) {
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||
config.setTlsVerifyCert(verify);
|
||||
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
||||
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
|
||||
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
|
||||
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost();
|
||||
if (autoHost != null) {
|
||||
ConnectionConfig config = autoHost.toConnectionConfig();
|
||||
if (finalHostType != null) config.setHostType(finalHostType);
|
||||
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 {
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
|
||||
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
|
||||
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
|
||||
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert();
|
||||
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e();
|
||||
if (host != null && !host.isEmpty()) {
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||
config.setTlsVerifyCert(verify);
|
||||
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
||||
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
|
||||
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
|
||||
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
}
|
||||
if (finalProxyUrl != null) {
|
||||
applyProxyUrl(config, finalProxyUrl);
|
||||
}
|
||||
if (finalHostType != null) {
|
||||
config.setHostType(finalHostType);
|
||||
} else {
|
||||
config.setHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
|
||||
}
|
||||
app.connect(config);
|
||||
} else {
|
||||
SwingUtilities.invokeLater(app::showConnectDialog);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case DO_NOTHING:
|
||||
|
||||
@@ -45,6 +45,24 @@ public class Settings {
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static String getUiFontFamily() {
|
||||
return prefs.get("uiFontFamily", "SansSerif");
|
||||
}
|
||||
|
||||
public static void setUiFontFamily(String family) {
|
||||
prefs.put("uiFontFamily", (family != null && !family.trim().isEmpty()) ? family : "SansSerif");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static int getUiFontSize() {
|
||||
return prefs.getInt("uiFontSize", 13);
|
||||
}
|
||||
|
||||
public static void setUiFontSize(int size) {
|
||||
prefs.putInt("uiFontSize", Math.max(8, Math.min(size, 48)));
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
private static void flushPrefs() {
|
||||
try {
|
||||
prefs.flush();
|
||||
@@ -145,6 +163,15 @@ public class Settings {
|
||||
return getAutoConnectAutoReconnect();
|
||||
}
|
||||
|
||||
public static String getDefaultHostType() {
|
||||
return prefs.get("defaultHostType", "TSO");
|
||||
}
|
||||
|
||||
public static void setDefaultHostType(String hostType) {
|
||||
prefs.put("defaultHostType", (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static void setAutoReconnect(boolean autoReconnect) {
|
||||
setAutoConnectAutoReconnect(autoReconnect);
|
||||
}
|
||||
@@ -390,6 +417,17 @@ public class Settings {
|
||||
prefs.putBoolean("blockSelectMode", block);
|
||||
}
|
||||
|
||||
// ========== Block paste ==========
|
||||
|
||||
public static boolean getBlockPaste() {
|
||||
return prefs.getBoolean("blockPaste", true);
|
||||
}
|
||||
|
||||
public static void setBlockPaste(boolean block) {
|
||||
prefs.putBoolean("blockPaste", block);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Clipboard & Tabular Paste ==========
|
||||
|
||||
public static boolean getEnablePasteFromExcel() {
|
||||
@@ -410,6 +448,33 @@ public class Settings {
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getTrimRectSizingHandles() {
|
||||
return prefs.getBoolean("trimRectSizingHandles", false);
|
||||
}
|
||||
|
||||
public static void setTrimRectSizingHandles(boolean val) {
|
||||
prefs.putBoolean("trimRectSizingHandles", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getTrimRectRemainAfterEdit() {
|
||||
return prefs.getBoolean("trimRectRemainAfterEdit", false);
|
||||
}
|
||||
|
||||
public static void setTrimRectRemainAfterEdit(boolean val) {
|
||||
prefs.putBoolean("trimRectRemainAfterEdit", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getPasteWordBreak() {
|
||||
return prefs.getBoolean("pasteWordBreak", true);
|
||||
}
|
||||
|
||||
public static void setPasteWordBreak(boolean val) {
|
||||
prefs.putBoolean("pasteWordBreak", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Crosshair Ruler ==========
|
||||
|
||||
public static boolean getCrosshairRuler() {
|
||||
@@ -420,6 +485,71 @@ public class Settings {
|
||||
prefs.putBoolean("crosshairRuler", enabled);
|
||||
}
|
||||
|
||||
// ========== Phase 25: Hotspots & Mouse Wheel Navigation ==========
|
||||
|
||||
public static boolean getHotspotsEnabled() {
|
||||
return prefs.getBoolean("hotspotsEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsUrlEnabled() {
|
||||
return prefs.getBoolean("hotspotsUrlEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsUrlEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsUrlEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsPfEnabled() {
|
||||
return prefs.getBoolean("hotspotsPfEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsPfEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsPfEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsMenuChoicesEnabled() {
|
||||
return prefs.getBoolean("hotspotsMenuChoicesEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsMenuChoicesEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsMenuChoicesEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getMouseWheelEnabled() {
|
||||
return prefs.getBoolean("mouseWheelEnabled", true);
|
||||
}
|
||||
|
||||
public static void setMouseWheelEnabled(boolean enabled) {
|
||||
prefs.putBoolean("mouseWheelEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static String getMouseWheelUpKey() {
|
||||
return prefs.get("mouseWheelUpKey", "[pf7]");
|
||||
}
|
||||
|
||||
public static void setMouseWheelUpKey(String key) {
|
||||
prefs.put("mouseWheelUpKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf7]");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static String getMouseWheelDownKey() {
|
||||
return prefs.get("mouseWheelDownKey", "[pf8]");
|
||||
}
|
||||
|
||||
public static void setMouseWheelDownKey(String key) {
|
||||
prefs.put("mouseWheelDownKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf8]");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Status Bar Visibility ==========
|
||||
|
||||
public static boolean getStatusBarVisible() {
|
||||
@@ -541,6 +671,16 @@ public class Settings {
|
||||
switch (key) {
|
||||
case "fontFamily": setFontFamily(value); break;
|
||||
case "fontSize": setFontSize(Integer.parseInt(value)); break;
|
||||
case "uiFontFamily":
|
||||
case "uifontfamily":
|
||||
case "uifont":
|
||||
case "uiFont":
|
||||
setUiFontFamily(value);
|
||||
break;
|
||||
case "uiFontSize":
|
||||
case "uifontsize":
|
||||
setUiFontSize(Integer.parseInt(value));
|
||||
break;
|
||||
case "javaUiTheme":
|
||||
case "theme":
|
||||
case "uiTheme":
|
||||
@@ -607,7 +747,13 @@ public class Settings {
|
||||
case "dynamic_cols":
|
||||
setDynamicCols(Integer.parseInt(value));
|
||||
break;
|
||||
case "hostType":
|
||||
case "hosttype":
|
||||
case "host_type":
|
||||
setDefaultHostType(value);
|
||||
break;
|
||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
|
||||
case "autoReconnect":
|
||||
case "auto_reconnect":
|
||||
case "autoConnectAutoReconnect":
|
||||
@@ -632,6 +778,9 @@ public class Settings {
|
||||
case "auto_sys_unlock":
|
||||
setAutoSysUnlock(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "blockPasteMode":
|
||||
setBlockPaste(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "enablePasteFromExcel":
|
||||
case "pasteFromExcel":
|
||||
case "excelPaste":
|
||||
@@ -642,6 +791,18 @@ public class Settings {
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectSizingHandles":
|
||||
case "sizingHandles":
|
||||
setTrimRectSizingHandles(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectRemainAfterEdit":
|
||||
case "remainAfterEdit":
|
||||
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteWordBreak":
|
||||
case "wordBreak":
|
||||
setPasteWordBreak(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown behavior/connection key: " + key);
|
||||
}
|
||||
@@ -649,6 +810,10 @@ public class Settings {
|
||||
|
||||
case "clipboard":
|
||||
switch (key) {
|
||||
case "blockPaste":
|
||||
case "blockPasteMode":
|
||||
setBlockPaste(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "enablePasteFromExcel":
|
||||
case "pasteFromExcel":
|
||||
case "excelPaste":
|
||||
@@ -659,6 +824,18 @@ public class Settings {
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectSizingHandles":
|
||||
case "sizingHandles":
|
||||
setTrimRectSizingHandles(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectRemainAfterEdit":
|
||||
case "remainAfterEdit":
|
||||
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteWordBreak":
|
||||
case "wordBreak":
|
||||
setPasteWordBreak(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown clipboard key: " + key);
|
||||
}
|
||||
@@ -761,6 +938,8 @@ public class Settings {
|
||||
// [appearance]
|
||||
w.println("[appearance]");
|
||||
w.println("javaUiTheme = " + getJavaUiTheme().name());
|
||||
w.println("uiFontFamily = " + getUiFontFamily());
|
||||
w.println("uiFontSize = " + getUiFontSize());
|
||||
w.println("fontFamily = " + getFontFamily());
|
||||
w.println("fontSize = " + getFontSize());
|
||||
w.println("crosshairRuler = " + getCrosshairRuler());
|
||||
@@ -780,9 +959,11 @@ public class Settings {
|
||||
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
|
||||
}
|
||||
w.println("codePage = " + getCodePage());
|
||||
w.println("hostType = " + getDefaultHostType());
|
||||
w.println("dynamicRows = " + getDynamicRows());
|
||||
w.println("dynamicCols = " + getDynamicCols());
|
||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||
w.println("blockPaste = " + getBlockPaste());
|
||||
w.println("autoReconnect = " + getAutoConnectAutoReconnect());
|
||||
w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries());
|
||||
w.println("inputMask = " + getInputMask());
|
||||
@@ -790,6 +971,9 @@ public class Settings {
|
||||
w.println("autoSysUnlock = " + getAutoSysUnlock());
|
||||
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
|
||||
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
|
||||
w.println("trimRectSizingHandles = " + getTrimRectSizingHandles());
|
||||
w.println("trimRectRemainAfterEdit = " + getTrimRectRemainAfterEdit());
|
||||
w.println("pasteWordBreak = " + getPasteWordBreak());
|
||||
w.println();
|
||||
|
||||
// [entryassist]
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.ui.AppIcon;
|
||||
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.j3270.ui.ThemeManager;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.FTConstants;
|
||||
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
@@ -27,6 +29,8 @@ public class FileTransferDialog extends JDialog {
|
||||
public String lrecl = "";
|
||||
public String blksize = "";
|
||||
public String space = "";
|
||||
public FTConfig.AllocationUnit units = FTConfig.AllocationUnit.TRACKS;
|
||||
public String dirBlocks = "";
|
||||
public String options = "";
|
||||
}
|
||||
|
||||
@@ -81,6 +85,8 @@ public class FileTransferDialog extends JDialog {
|
||||
private JTextField lreclField;
|
||||
private JTextField blksizeField;
|
||||
private JTextField spaceField;
|
||||
private JComboBox<FTConfig.AllocationUnit> unitsCombo;
|
||||
private JTextField dirField;
|
||||
|
||||
// VM specific
|
||||
private JTextField optionsField;
|
||||
@@ -92,6 +98,7 @@ public class FileTransferDialog extends JDialog {
|
||||
super(owner, "File Transfer (IND$FILE)", false);
|
||||
this.owner = owner;
|
||||
this.coordinator = coordinator;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI();
|
||||
pack();
|
||||
@@ -218,7 +225,7 @@ public class FileTransferDialog extends JDialog {
|
||||
formPanel.add(createRow("File Options:", optionsPanel2));
|
||||
|
||||
// Host-specific options panel (TSO dataset allocation)
|
||||
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
|
||||
JPanel hostOptsPanel = new JPanel(new GridLayout(3, 4, 5, 5));
|
||||
hostOptsPanel.setOpaque(false);
|
||||
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
|
||||
|
||||
@@ -234,10 +241,24 @@ public class FileTransferDialog extends JDialog {
|
||||
blksizeField = new JTextField(5);
|
||||
hostOptsPanel.add(blksizeField);
|
||||
|
||||
hostOptsPanel.add(new JLabel("SPACE:"));
|
||||
hostOptsPanel.add(new JLabel("Units:"));
|
||||
unitsCombo = new JComboBox<>(new FTConfig.AllocationUnit[]{
|
||||
FTConfig.AllocationUnit.TRACKS,
|
||||
FTConfig.AllocationUnit.CYLINDERS,
|
||||
FTConfig.AllocationUnit.AVBLOCK,
|
||||
FTConfig.AllocationUnit.DEFAULT
|
||||
});
|
||||
unitsCombo.setSelectedItem(FTConfig.AllocationUnit.TRACKS);
|
||||
hostOptsPanel.add(unitsCombo);
|
||||
|
||||
hostOptsPanel.add(new JLabel("SPACE (Pri,Sec):"));
|
||||
spaceField = new JTextField(15);
|
||||
hostOptsPanel.add(spaceField);
|
||||
|
||||
hostOptsPanel.add(new JLabel("Directory (PDS):"));
|
||||
dirField = new JTextField(5);
|
||||
hostOptsPanel.add(dirField);
|
||||
|
||||
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
|
||||
formPanel.add(hostOptsPanel);
|
||||
|
||||
@@ -304,9 +325,25 @@ public class FileTransferDialog extends JDialog {
|
||||
if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm);
|
||||
if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl);
|
||||
if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize);
|
||||
if (lastTransferState.units != null) unitsCombo.setSelectedItem(lastTransferState.units);
|
||||
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
|
||||
if (lastTransferState.dirBlocks != null) dirField.setText(lastTransferState.dirBlocks);
|
||||
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
|
||||
}
|
||||
|
||||
// When using a saved connection (or when no previous transfer state exists for this connection),
|
||||
// use the configured host type (CMS/TSO/CICS) for File Transfers
|
||||
if (coordinator != null && coordinator.getClient() != null) {
|
||||
haus.nightmare.lib3270j.ConnectionConfig cfg = coordinator.getClient().getConfig();
|
||||
if (cfg != null) {
|
||||
FTConfig.HostType configuredType = cfg.getFtHostType();
|
||||
if (configuredType != null) {
|
||||
if (cfg.isSavedConnection() || lastTransferState == null) {
|
||||
hostTypeCombo.setSelectedItem(configuredType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOptionStates() {
|
||||
@@ -325,7 +362,9 @@ public class FileTransferDialog extends JDialog {
|
||||
recfmField.setEnabled(isSend && isTso);
|
||||
lreclField.setEnabled(isSend && isTso);
|
||||
blksizeField.setEnabled(isSend && isTso);
|
||||
unitsCombo.setEnabled(isSend && isTso);
|
||||
spaceField.setEnabled(isSend && isTso);
|
||||
dirField.setEnabled(isSend && isTso);
|
||||
optionsField.setEnabled(isCms);
|
||||
}
|
||||
|
||||
@@ -357,11 +396,26 @@ public class FileTransferDialog extends JDialog {
|
||||
private void browseHostDirectory() {
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
String initialQuery = hostFileField.getText().trim();
|
||||
if (hostType == FTConfig.HostType.CMS && (initialQuery.isEmpty() || initialQuery.equals("* * *") || initialQuery.equals("*"))) {
|
||||
initialQuery = "* * A";
|
||||
}
|
||||
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
|
||||
hostFileField.setText(dialog.getSelectedHostFile());
|
||||
HostDirectoryEntry entry = dialog.getSelectedEntry();
|
||||
if (entry != null) {
|
||||
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
|
||||
recfmField.setText(entry.getRecfm().trim());
|
||||
}
|
||||
if (entry.getLrecl() > 0) {
|
||||
lreclField.setText(String.valueOf(entry.getLrecl()));
|
||||
}
|
||||
if (entry.getBlksize() > 0) {
|
||||
blksizeField.setText(String.valueOf(entry.getBlksize()));
|
||||
}
|
||||
}
|
||||
boolean shouldAutofill = localFileField.getText().trim().isEmpty() ||
|
||||
(lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile));
|
||||
if (shouldAutofill) {
|
||||
@@ -408,7 +462,10 @@ public class FileTransferDialog extends JDialog {
|
||||
config.setRecfm(recfmField.getText().trim());
|
||||
config.setLrecl(lreclField.getText().trim());
|
||||
config.setBlksize(blksizeField.getText().trim());
|
||||
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
|
||||
config.setSpace(spaceField.getText().trim());
|
||||
config.setDirectoryBlocks(dirField.getText().trim());
|
||||
config.setOptions(optionsField.getText().trim());
|
||||
|
||||
recordSessionTransfer();
|
||||
@@ -437,7 +494,9 @@ public class FileTransferDialog extends JDialog {
|
||||
state.recfm = recfmField.getText().trim();
|
||||
state.lrecl = lreclField.getText().trim();
|
||||
state.blksize = blksizeField.getText().trim();
|
||||
state.units = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
|
||||
state.space = spaceField.getText().trim();
|
||||
state.dirBlocks = dirField.getText().trim();
|
||||
state.options = optionsField.getText().trim();
|
||||
lastTransferState = state;
|
||||
}
|
||||
@@ -457,7 +516,9 @@ public class FileTransferDialog extends JDialog {
|
||||
public JTextField getRecfmField() { return recfmField; }
|
||||
public JTextField getLreclField() { return lreclField; }
|
||||
public JTextField getBlksizeField() { return blksizeField; }
|
||||
public JComboBox<FTConfig.AllocationUnit> getUnitsCombo() { return unitsCombo; }
|
||||
public JTextField getSpaceField() { return spaceField; }
|
||||
public JTextField getDirField() { return dirField; }
|
||||
public JTextField getOptionsField() { return optionsField; }
|
||||
|
||||
private void applyTheme(Container container) {
|
||||
|
||||
@@ -0,0 +1,695 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Storage manager for saved host profiles, maintaining 1:1 format compatibility with a3270.
|
||||
* Persists profiles in Java user preferences with JSON serialization.
|
||||
*/
|
||||
public class HostStorage {
|
||||
|
||||
private static final Logger log = Logger.getLogger(HostStorage.class.getName());
|
||||
private static final Preferences prefs = Preferences.userNodeForPackage(HostStorage.class);
|
||||
public static final String KEY_SAVED_HOSTS = "saved_hosts";
|
||||
|
||||
public static synchronized List<SavedHost> getSavedHosts() {
|
||||
String jsonStr = prefs.get(KEY_SAVED_HOSTS, null);
|
||||
if (jsonStr == null || jsonStr.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return parseHostsJson(jsonStr);
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Failed to parse saved hosts JSON", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void saveHost(SavedHost host) {
|
||||
if (host == null) return;
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
int index = -1;
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
if (Objects.equals(current.get(i).getId(), host.getId())) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If this host is set to autoConnect, clear autoConnect on all others
|
||||
SavedHost hostToSave = host.copy();
|
||||
if (hostToSave.isAutoConnect()) {
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
current.get(i).setAutoConnect(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
current.set(index, hostToSave);
|
||||
} else {
|
||||
current.add(hostToSave);
|
||||
}
|
||||
saveAll(current);
|
||||
}
|
||||
|
||||
public static synchronized void deleteHost(String hostId) {
|
||||
if (hostId == null) return;
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
current.removeIf(h -> Objects.equals(h.getId(), hostId));
|
||||
saveAll(current);
|
||||
}
|
||||
|
||||
public static synchronized SavedHost getAutoConnectHost() {
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (h.isAutoConnect()) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized SavedHost findById(String id) {
|
||||
if (id == null) return null;
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (id.equals(h.getId())) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized SavedHost findByName(String name) {
|
||||
if (name == null) return null;
|
||||
for (SavedHost h : getSavedHosts()) {
|
||||
if (name.equalsIgnoreCase(h.getName())) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized void saveAll(List<SavedHost> hosts) {
|
||||
if (hosts == null) return;
|
||||
String json = serializeHostsJson(hosts);
|
||||
prefs.put(KEY_SAVED_HOSTS, json);
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void clearAll() {
|
||||
prefs.remove(KEY_SAVED_HOSTS);
|
||||
try {
|
||||
prefs.flush();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ================= INI File Export / Import =================
|
||||
|
||||
/**
|
||||
* Export all currently saved host connections to an INI file.
|
||||
*/
|
||||
public static synchronized void exportToIniFile(File file) throws IOException {
|
||||
List<SavedHost> hosts = getSavedHosts();
|
||||
exportToIniFile(file, hosts);
|
||||
}
|
||||
|
||||
public static synchronized void exportToIniFile(String path) throws IOException {
|
||||
exportToIniFile(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a specified list of host connections to an INI file.
|
||||
*/
|
||||
public static void exportToIniFile(File file, List<SavedHost> hosts) throws IOException {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("Target file cannot be null");
|
||||
}
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
|
||||
w.println("; j3270 Saved Connections Configuration File");
|
||||
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
|
||||
w.println("; Connections count: " + (hosts != null ? hosts.size() : 0));
|
||||
w.println();
|
||||
|
||||
if (hosts != null) {
|
||||
for (SavedHost h : hosts) {
|
||||
String section = (h.getName() != null && !h.getName().trim().isEmpty())
|
||||
? h.getName().trim()
|
||||
: (h.getHost() != null && !h.getHost().trim().isEmpty() ? h.getHost().trim() : "Connection");
|
||||
w.println("[" + section + "]");
|
||||
if (h.getId() != null && !h.getId().trim().isEmpty()) {
|
||||
w.println("id = " + h.getId());
|
||||
}
|
||||
w.println("name = " + (h.getName() != null ? h.getName() : ""));
|
||||
w.println("host = " + (h.getHost() != null ? h.getHost() : ""));
|
||||
w.println("port = " + h.getPort());
|
||||
w.println("model = " + h.getModel());
|
||||
w.println("dynamicRows = " + h.getDynamicRows());
|
||||
w.println("dynamicCols = " + h.getDynamicCols());
|
||||
w.println("luName = " + (h.getLuName() != null ? h.getLuName() : ""));
|
||||
w.println("autoConnect = " + h.isAutoConnect());
|
||||
w.println("hostType = " + (h.getHostType() != null ? h.getHostType() : "TSO"));
|
||||
w.println("useTls = " + h.isUseTls());
|
||||
w.println("tlsVerifyCert = " + h.isTlsVerifyCert());
|
||||
w.println("tn3270e = " + h.isTn3270e());
|
||||
w.println("graphicsMode = " + (h.getGraphicsMode() != null ? h.getGraphicsMode() : "BOTH"));
|
||||
w.println("codePage = " + (h.getCodePage() != null ? h.getCodePage() : "037"));
|
||||
w.println("proxyType = " + (h.getProxyType() != null ? h.getProxyType() : "NONE"));
|
||||
w.println("proxyHost = " + (h.getProxyHost() != null ? h.getProxyHost() : ""));
|
||||
w.println("proxyPort = " + h.getProxyPort());
|
||||
w.println("proxyUsername = " + (h.getProxyUsername() != null ? h.getProxyUsername() : ""));
|
||||
w.println("proxyPassword = " + (h.getProxyPassword() != null ? h.getProxyPassword() : ""));
|
||||
w.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Saved connections exported to: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load connections from an INI file without saving them to persistent storage.
|
||||
*/
|
||||
public static List<SavedHost> loadFromIniFile(File file) throws IOException {
|
||||
if (file == null || !file.exists()) {
|
||||
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
|
||||
}
|
||||
List<SavedHost> hosts = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
SavedHost current = null;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
if (current != null) {
|
||||
hosts.add(current);
|
||||
}
|
||||
String section = line.substring(1, line.length() - 1).trim();
|
||||
if (section.toLowerCase().startsWith("connection:")) {
|
||||
section = section.substring("connection:".length()).trim();
|
||||
} else if (section.toLowerCase().startsWith("host:")) {
|
||||
section = section.substring("host:".length()).trim();
|
||||
}
|
||||
current = new SavedHost();
|
||||
current.setName(section);
|
||||
continue;
|
||||
}
|
||||
int eq = line.indexOf('=');
|
||||
if (eq < 0) {
|
||||
continue;
|
||||
}
|
||||
String key = line.substring(0, eq).trim();
|
||||
String value = line.substring(eq + 1).trim();
|
||||
if (current == null) {
|
||||
current = new SavedHost();
|
||||
current.setName("Default");
|
||||
}
|
||||
applyIniEntry(current, key, value);
|
||||
}
|
||||
if (current != null) {
|
||||
hosts.add(current);
|
||||
}
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
public static List<SavedHost> loadFromIniFile(String path) throws IOException {
|
||||
return loadFromIniFile(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Import connections from an INI file and merge them into persistent storage.
|
||||
* Updates existing profiles matching by ID or Name, and appends new profiles.
|
||||
*/
|
||||
public static synchronized List<SavedHost> importFromIniFile(File file) throws IOException {
|
||||
List<SavedHost> imported = loadFromIniFile(file);
|
||||
if (imported == null || imported.isEmpty()) {
|
||||
return getSavedHosts();
|
||||
}
|
||||
List<SavedHost> current = new ArrayList<>(getSavedHosts());
|
||||
for (SavedHost host : imported) {
|
||||
int index = -1;
|
||||
for (int i = 0; i < current.size(); i++) {
|
||||
SavedHost existing = current.get(i);
|
||||
if ((host.getId() != null && !host.getId().trim().isEmpty() && Objects.equals(existing.getId(), host.getId())) ||
|
||||
(host.getName() != null && !host.getName().trim().isEmpty() &&
|
||||
host.getName().equalsIgnoreCase(existing.getName()))) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (host.isAutoConnect()) {
|
||||
for (SavedHost c : current) {
|
||||
c.setAutoConnect(false);
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
if (host.getId() == null || host.getId().trim().isEmpty()) {
|
||||
host.setId(current.get(index).getId());
|
||||
}
|
||||
current.set(index, host);
|
||||
} else {
|
||||
current.add(host);
|
||||
}
|
||||
}
|
||||
saveAll(current);
|
||||
return current;
|
||||
}
|
||||
|
||||
public static synchronized List<SavedHost> importFromIniFile(String path) throws IOException {
|
||||
return importFromIniFile(new File(path));
|
||||
}
|
||||
|
||||
private static void applyIniEntry(SavedHost h, String key, String value) {
|
||||
String lowerKey = key.toLowerCase();
|
||||
switch (lowerKey) {
|
||||
case "id":
|
||||
if (!value.isEmpty()) h.setId(value);
|
||||
break;
|
||||
case "name":
|
||||
case "profilename":
|
||||
case "profile":
|
||||
if (!value.isEmpty()) h.setName(value);
|
||||
break;
|
||||
case "host":
|
||||
case "hostname":
|
||||
case "address":
|
||||
h.setHost(value);
|
||||
break;
|
||||
case "port":
|
||||
h.setPort(parseInt(value, 23));
|
||||
break;
|
||||
case "model":
|
||||
case "terminalmodel":
|
||||
h.setModel(parseInt(value, 4));
|
||||
break;
|
||||
case "dynamicrows":
|
||||
case "dynamic_rows":
|
||||
case "rows":
|
||||
h.setDynamicRows(parseInt(value, 62));
|
||||
break;
|
||||
case "dynamiccols":
|
||||
case "dynamic_cols":
|
||||
case "cols":
|
||||
h.setDynamicCols(parseInt(value, 160));
|
||||
break;
|
||||
case "luname":
|
||||
case "lu_name":
|
||||
case "lu":
|
||||
h.setLuName(value);
|
||||
break;
|
||||
case "autoconnect":
|
||||
case "auto_connect":
|
||||
h.setAutoConnect(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "hosttype":
|
||||
case "host_type":
|
||||
case "type":
|
||||
h.setHostType(value.toUpperCase());
|
||||
break;
|
||||
case "usetls":
|
||||
case "use_tls":
|
||||
case "tls":
|
||||
case "ssl":
|
||||
h.setUseTls(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "tlsverifycert":
|
||||
case "verifycert":
|
||||
case "tls_verify_cert":
|
||||
h.setTlsVerifyCert(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "tn3270e":
|
||||
case "enabletn3270e":
|
||||
h.setTn3270e(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "graphicsmode":
|
||||
case "graphics_mode":
|
||||
case "graphics":
|
||||
h.setGraphicsMode(value.toUpperCase());
|
||||
break;
|
||||
case "codepage":
|
||||
case "code_page":
|
||||
case "charset":
|
||||
h.setCodePage(value);
|
||||
break;
|
||||
case "proxytype":
|
||||
case "proxy_type":
|
||||
h.setProxyType(value.toUpperCase());
|
||||
break;
|
||||
case "proxyhost":
|
||||
case "proxy_host":
|
||||
h.setProxyHost(value);
|
||||
break;
|
||||
case "proxyport":
|
||||
case "proxy_port":
|
||||
h.setProxyPort(parseInt(value, 0));
|
||||
break;
|
||||
case "proxyusername":
|
||||
case "proxy_username":
|
||||
case "proxyuser":
|
||||
h.setProxyUsername(value);
|
||||
break;
|
||||
case "proxypassword":
|
||||
case "proxy_password":
|
||||
case "proxypass":
|
||||
h.setProxyPassword(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ================= JSON Serialization =================
|
||||
|
||||
public static String serializeHostsJson(List<SavedHost> hosts) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[\n");
|
||||
for (int i = 0; i < hosts.size(); i++) {
|
||||
SavedHost h = hosts.get(i);
|
||||
sb.append(" ").append(serializeHostJson(h));
|
||||
if (i < hosts.size() - 1) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String serializeHostJson(SavedHost h) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{");
|
||||
appendField(sb, "id", h.getId(), true);
|
||||
appendField(sb, "name", h.getName(), true);
|
||||
appendField(sb, "host", h.getHost(), true);
|
||||
appendField(sb, "port", h.getPort(), true);
|
||||
appendField(sb, "model", h.getModel(), true);
|
||||
appendField(sb, "dynamicRows", h.getDynamicRows(), true);
|
||||
appendField(sb, "dynamicCols", h.getDynamicCols(), true);
|
||||
appendField(sb, "luName", h.getLuName(), true);
|
||||
appendField(sb, "autoConnect", h.isAutoConnect(), true);
|
||||
appendField(sb, "hostType", h.getHostType(), true);
|
||||
appendField(sb, "useTls", h.isUseTls(), true);
|
||||
appendField(sb, "tlsVerifyCert", h.isTlsVerifyCert(), true);
|
||||
appendField(sb, "tn3270e", h.isTn3270e(), true);
|
||||
appendField(sb, "graphicsMode", h.getGraphicsMode(), true);
|
||||
appendField(sb, "codePage", h.getCodePage(), true);
|
||||
appendField(sb, "proxyType", h.getProxyType(), true);
|
||||
appendField(sb, "proxyHost", h.getProxyHost(), true);
|
||||
appendField(sb, "proxyPort", h.getProxyPort(), true);
|
||||
appendField(sb, "proxyUsername", h.getProxyUsername(), true);
|
||||
appendField(sb, "proxyPassword", h.getProxyPassword(), false);
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, String val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":\"").append(escapeJson(val != null ? val : "")).append("\"");
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, int val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":").append(val);
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static void appendField(StringBuilder sb, String key, boolean val, boolean comma) {
|
||||
sb.append("\"").append(key).append("\":").append(val);
|
||||
if (comma) sb.append(",");
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '"': sb.append("\\\""); break;
|
||||
case '\\': sb.append("\\\\"); break;
|
||||
case '\b': sb.append("\\b"); break;
|
||||
case '\f': sb.append("\\f"); break;
|
||||
case '\n': sb.append("\\n"); break;
|
||||
case '\r': sb.append("\\r"); break;
|
||||
case '\t': sb.append("\\t"); break;
|
||||
default:
|
||||
if (c < 32) {
|
||||
sb.append(String.format("\\u%04x", (int) c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ================= JSON Deserialization =================
|
||||
|
||||
public static List<SavedHost> parseHostsJson(String json) {
|
||||
List<SavedHost> list = new ArrayList<>();
|
||||
if (json == null || json.trim().isEmpty()) return list;
|
||||
|
||||
SimpleJsonParser parser = new SimpleJsonParser(json);
|
||||
Object root = parser.parseValue();
|
||||
if (root instanceof List) {
|
||||
for (Object item : (List<?>) root) {
|
||||
if (item instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) item;
|
||||
list.add(fromMap(map));
|
||||
}
|
||||
}
|
||||
} else if (root instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) root;
|
||||
list.add(fromMap(map));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static SavedHost fromMap(Map<String, Object> map) {
|
||||
SavedHost h = new SavedHost();
|
||||
if (map.containsKey("id")) h.setId(String.valueOf(map.get("id")));
|
||||
if (map.containsKey("name")) h.setName(String.valueOf(map.get("name")));
|
||||
if (map.containsKey("host")) h.setHost(String.valueOf(map.get("host")));
|
||||
if (map.containsKey("port")) h.setPort(parseInt(map.get("port"), 23));
|
||||
if (map.containsKey("model")) h.setModel(parseInt(map.get("model"), 4));
|
||||
if (map.containsKey("dynamicRows")) h.setDynamicRows(parseInt(map.get("dynamicRows"), 62));
|
||||
if (map.containsKey("dynamicCols")) h.setDynamicCols(parseInt(map.get("dynamicCols"), 160));
|
||||
if (map.containsKey("luName")) h.setLuName(String.valueOf(map.get("luName")));
|
||||
if (map.containsKey("autoConnect")) h.setAutoConnect(parseBool(map.get("autoConnect"), false));
|
||||
if (map.containsKey("hostType")) h.setHostType(String.valueOf(map.get("hostType")));
|
||||
if (map.containsKey("useTls")) h.setUseTls(parseBool(map.get("useTls"), false));
|
||||
if (map.containsKey("tlsVerifyCert")) h.setTlsVerifyCert(parseBool(map.get("tlsVerifyCert"), true));
|
||||
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
|
||||
if (map.containsKey("graphicsMode")) {
|
||||
String gm = String.valueOf(map.get("graphicsMode"));
|
||||
h.setGraphicsMode(gm);
|
||||
}
|
||||
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
|
||||
if (map.containsKey("proxyType")) h.setProxyType(String.valueOf(map.get("proxyType")));
|
||||
if (map.containsKey("proxyHost")) h.setProxyHost(String.valueOf(map.get("proxyHost")));
|
||||
if (map.containsKey("proxyPort")) h.setProxyPort(parseInt(map.get("proxyPort"), 0));
|
||||
if (map.containsKey("proxyUsername")) h.setProxyUsername(String.valueOf(map.get("proxyUsername")));
|
||||
if (map.containsKey("proxyPassword")) h.setProxyPassword(String.valueOf(map.get("proxyPassword")));
|
||||
return h;
|
||||
}
|
||||
|
||||
private static int parseInt(Object obj, int def) {
|
||||
if (obj == null) return def;
|
||||
if (obj instanceof Number) return ((Number) obj).intValue();
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(obj).trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean parseBool(Object obj, boolean def) {
|
||||
if (obj == null) return def;
|
||||
if (obj instanceof Boolean) return (Boolean) obj;
|
||||
return Boolean.parseBoolean(String.valueOf(obj).trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal pure-Java JSON parser for nested Maps, Lists, Strings, Numbers, and Booleans.
|
||||
*/
|
||||
static class SimpleJsonParser {
|
||||
private final String src;
|
||||
private int pos = 0;
|
||||
|
||||
SimpleJsonParser(String src) {
|
||||
this.src = src;
|
||||
}
|
||||
|
||||
Object parseValue() {
|
||||
skipWhitespace();
|
||||
if (pos >= src.length()) return null;
|
||||
char c = src.charAt(pos);
|
||||
if (c == '{') return parseObject();
|
||||
if (c == '[') return parseArray();
|
||||
if (c == '"') return parseString();
|
||||
if (c == 't' || c == 'f') return parseBoolean();
|
||||
if (c == 'n') return parseNull();
|
||||
if (c == '-' || Character.isDigit(c)) return parseNumber();
|
||||
throw new IllegalArgumentException("Unexpected char '" + c + "' at position " + pos);
|
||||
}
|
||||
|
||||
private Map<String, Object> parseObject() {
|
||||
match('{');
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
skipWhitespace();
|
||||
if (peek() == '}') {
|
||||
pos++;
|
||||
return map;
|
||||
}
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
String key = parseString();
|
||||
skipWhitespace();
|
||||
match(':');
|
||||
Object val = parseValue();
|
||||
map.put(key, val);
|
||||
skipWhitespace();
|
||||
char c = peek();
|
||||
if (c == ',') {
|
||||
pos++;
|
||||
} else if (c == '}') {
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<Object> parseArray() {
|
||||
match('[');
|
||||
List<Object> list = new ArrayList<>();
|
||||
skipWhitespace();
|
||||
if (peek() == ']') {
|
||||
pos++;
|
||||
return list;
|
||||
}
|
||||
while (true) {
|
||||
Object val = parseValue();
|
||||
list.add(val);
|
||||
skipWhitespace();
|
||||
char c = peek();
|
||||
if (c == ',') {
|
||||
pos++;
|
||||
} else if (c == ']') {
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String parseString() {
|
||||
match('"');
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (pos < src.length()) {
|
||||
char c = src.charAt(pos++);
|
||||
if (c == '"') {
|
||||
return sb.toString();
|
||||
} else if (c == '\\') {
|
||||
if (pos >= src.length()) break;
|
||||
char esc = src.charAt(pos++);
|
||||
switch (esc) {
|
||||
case '"': sb.append('"'); break;
|
||||
case '\\': sb.append('\\'); break;
|
||||
case '/': sb.append('/'); break;
|
||||
case 'b': sb.append('\b'); break;
|
||||
case 'f': sb.append('\f'); break;
|
||||
case 'n': sb.append('\n'); break;
|
||||
case 'r': sb.append('\r'); break;
|
||||
case 't': sb.append('\t'); break;
|
||||
case 'u':
|
||||
if (pos + 4 <= src.length()) {
|
||||
String hex = src.substring(pos, pos + 4);
|
||||
pos += 4;
|
||||
try {
|
||||
sb.append((char) Integer.parseInt(hex, 16));
|
||||
} catch (NumberFormatException e) {
|
||||
sb.append("?");
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sb.append(esc);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private Boolean parseBoolean() {
|
||||
if (src.startsWith("true", pos)) {
|
||||
pos += 4;
|
||||
return true;
|
||||
} else if (src.startsWith("false", pos)) {
|
||||
pos += 5;
|
||||
return false;
|
||||
}
|
||||
throw new IllegalArgumentException("Expected boolean at " + pos);
|
||||
}
|
||||
|
||||
private Object parseNull() {
|
||||
if (src.startsWith("null", pos)) {
|
||||
pos += 4;
|
||||
return null;
|
||||
}
|
||||
throw new IllegalArgumentException("Expected null at " + pos);
|
||||
}
|
||||
|
||||
private Number parseNumber() {
|
||||
int start = pos;
|
||||
if (src.charAt(pos) == '-') pos++;
|
||||
while (pos < src.length() && (Character.isDigit(src.charAt(pos)) || src.charAt(pos) == '.' || src.charAt(pos) == 'e' || src.charAt(pos) == 'E' || src.charAt(pos) == '+' || src.charAt(pos) == '-')) {
|
||||
pos++;
|
||||
}
|
||||
String numStr = src.substring(start, pos);
|
||||
if (numStr.contains(".")) {
|
||||
return Double.parseDouble(numStr);
|
||||
}
|
||||
return Long.parseLong(numStr);
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
private char peek() {
|
||||
return pos < src.length() ? src.charAt(pos) : '\0';
|
||||
}
|
||||
|
||||
private void match(char expected) {
|
||||
skipWhitespace();
|
||||
if (pos >= src.length() || src.charAt(pos) != expected) {
|
||||
throw new IllegalArgumentException("Expected '" + expected + "' at " + pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.graphics.GraphicsMode;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Data model for a saved 3270 connection profile, matching the schema of a3270 SavedHost.
|
||||
* Includes per-connection host, model, protocol, and proxy configuration.
|
||||
*/
|
||||
public class SavedHost {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String host;
|
||||
private int port;
|
||||
private int model;
|
||||
private int dynamicRows;
|
||||
private int dynamicCols;
|
||||
private String luName;
|
||||
private boolean autoConnect;
|
||||
private String hostType;
|
||||
private boolean useTls;
|
||||
private boolean tlsVerifyCert;
|
||||
private boolean tn3270e;
|
||||
private String graphicsMode;
|
||||
private String codePage;
|
||||
|
||||
// Per-connection proxy configuration
|
||||
private String proxyType;
|
||||
private String proxyHost;
|
||||
private int proxyPort;
|
||||
private String proxyUsername;
|
||||
private String proxyPassword;
|
||||
|
||||
public SavedHost() {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.name = "Mainframe";
|
||||
this.host = "127.0.0.1";
|
||||
this.port = 23;
|
||||
this.model = 4;
|
||||
this.dynamicRows = 62;
|
||||
this.dynamicCols = 160;
|
||||
this.luName = "";
|
||||
this.autoConnect = false;
|
||||
this.hostType = "TSO";
|
||||
this.useTls = false;
|
||||
this.tlsVerifyCert = true;
|
||||
this.tn3270e = true;
|
||||
this.graphicsMode = "BOTH";
|
||||
this.codePage = "037";
|
||||
this.proxyType = "NONE";
|
||||
this.proxyHost = "";
|
||||
this.proxyPort = 0;
|
||||
this.proxyUsername = "";
|
||||
this.proxyPassword = "";
|
||||
}
|
||||
|
||||
public SavedHost(String id, String name, String host, int port, int model,
|
||||
int dynamicRows, int dynamicCols, String luName, boolean autoConnect,
|
||||
String hostType, boolean useTls, boolean tlsVerifyCert, boolean tn3270e,
|
||||
String graphicsMode, String codePage, String proxyType, String proxyHost,
|
||||
int proxyPort, String proxyUsername, String proxyPassword) {
|
||||
this.id = (id != null && !id.trim().isEmpty()) ? id : UUID.randomUUID().toString();
|
||||
this.name = name != null ? name : "";
|
||||
this.host = host != null ? host : "127.0.0.1";
|
||||
this.port = port > 0 ? port : 23;
|
||||
this.model = model;
|
||||
this.dynamicRows = dynamicRows > 0 ? dynamicRows : 62;
|
||||
this.dynamicCols = dynamicCols > 0 ? dynamicCols : 160;
|
||||
this.luName = luName != null ? luName : "";
|
||||
this.autoConnect = autoConnect;
|
||||
this.hostType = hostType != null ? hostType : "TSO";
|
||||
this.useTls = useTls;
|
||||
this.tlsVerifyCert = tlsVerifyCert;
|
||||
this.tn3270e = tn3270e;
|
||||
this.graphicsMode = graphicsMode != null ? graphicsMode : "BOTH";
|
||||
this.codePage = codePage != null ? codePage : "037";
|
||||
this.proxyType = (proxyType != null && !proxyType.trim().isEmpty()) ? proxyType.toUpperCase() : "NONE";
|
||||
this.proxyHost = proxyHost != null ? proxyHost : "";
|
||||
this.proxyPort = proxyPort;
|
||||
this.proxyUsername = proxyUsername != null ? proxyUsername : "";
|
||||
this.proxyPassword = proxyPassword != null ? proxyPassword : "";
|
||||
}
|
||||
|
||||
public SavedHost copy() {
|
||||
return new SavedHost(id, name, host, port, model, dynamicRows, dynamicCols, luName,
|
||||
autoConnect, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, codePage,
|
||||
proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this saved host profile into an active ConnectionConfig.
|
||||
*/
|
||||
public ConnectionConfig toConnectionConfig() {
|
||||
TerminalModel tm;
|
||||
if (model == 0) {
|
||||
tm = TerminalModel.IBM_DYNAMIC;
|
||||
} else {
|
||||
try {
|
||||
tm = TerminalModel.forModel(model, true);
|
||||
} catch (Exception e) {
|
||||
tm = TerminalModel.IBM_3279_4;
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId(this.id);
|
||||
config.setSavedProfileName(this.name);
|
||||
config.setHostType(this.hostType != null ? this.hostType : "TSO");
|
||||
if (model == 0 || tm.isDynamic()) {
|
||||
config.setDynamicDimensions(dynamicRows, dynamicCols);
|
||||
}
|
||||
if (luName != null && !luName.trim().isEmpty()) {
|
||||
config.setLuName(luName.trim());
|
||||
}
|
||||
config.setTlsVerifyCert(tlsVerifyCert);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
if (graphicsMode != null && !graphicsMode.isEmpty()) {
|
||||
config.setGraphicsMode(GraphicsMode.fromString(graphicsMode));
|
||||
}
|
||||
if (codePage != null && !codePage.isEmpty()) {
|
||||
config.setCodePage(codePage);
|
||||
}
|
||||
|
||||
// Apply per-connection proxy if configured
|
||||
if (proxyType != null && !proxyType.equalsIgnoreCase("NONE") && proxyHost != null && !proxyHost.trim().isEmpty()) {
|
||||
try {
|
||||
ConnectionConfig.ProxyType pt = ConnectionConfig.ProxyType.valueOf(proxyType.toUpperCase());
|
||||
int pPort = proxyPort > 0 ? proxyPort : (pt == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080);
|
||||
config.setProxy(pt, proxyHost.trim(), pPort,
|
||||
(proxyUsername != null && !proxyUsername.trim().isEmpty()) ? proxyUsername.trim() : null,
|
||||
(proxyPassword != null && !proxyPassword.isEmpty()) ? proxyPassword : null);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
|
||||
public int getModel() { return model; }
|
||||
public void setModel(int model) { this.model = model; }
|
||||
|
||||
public int getDynamicRows() { return dynamicRows; }
|
||||
public void setDynamicRows(int dynamicRows) { this.dynamicRows = dynamicRows; }
|
||||
|
||||
public int getDynamicCols() { return dynamicCols; }
|
||||
public void setDynamicCols(int dynamicCols) { this.dynamicCols = dynamicCols; }
|
||||
|
||||
public String getLuName() { return luName; }
|
||||
public void setLuName(String luName) { this.luName = luName; }
|
||||
|
||||
public boolean isAutoConnect() { return autoConnect; }
|
||||
public void setAutoConnect(boolean autoConnect) { this.autoConnect = autoConnect; }
|
||||
|
||||
public String getHostType() { return hostType; }
|
||||
public void setHostType(String hostType) { this.hostType = hostType; }
|
||||
|
||||
public boolean isUseTls() { return useTls; }
|
||||
public void setUseTls(boolean useTls) { this.useTls = useTls; }
|
||||
|
||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
|
||||
|
||||
public boolean isTn3270e() { return tn3270e; }
|
||||
public void setTn3270e(boolean tn3270e) { this.tn3270e = tn3270e; }
|
||||
|
||||
public String getGraphicsMode() { return graphicsMode; }
|
||||
public void setGraphicsMode(String graphicsMode) { this.graphicsMode = graphicsMode; }
|
||||
|
||||
public String getCodePage() { return codePage; }
|
||||
public void setCodePage(String codePage) { this.codePage = codePage; }
|
||||
|
||||
public String getProxyType() { return proxyType != null ? proxyType : "NONE"; }
|
||||
public void setProxyType(String proxyType) { this.proxyType = (proxyType != null) ? proxyType.toUpperCase() : "NONE"; }
|
||||
|
||||
public String getProxyHost() { return proxyHost != null ? proxyHost : ""; }
|
||||
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
|
||||
|
||||
public int getProxyPort() { return proxyPort; }
|
||||
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
|
||||
|
||||
public String getProxyUsername() { return proxyUsername != null ? proxyUsername : ""; }
|
||||
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
|
||||
|
||||
public String getProxyPassword() { return proxyPassword != null ? proxyPassword : ""; }
|
||||
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (name != null && !name.trim().isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
return host + (port > 0 ? ":" + port : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SavedHost savedHost = (SavedHost) o;
|
||||
return Objects.equals(id, savedHost.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Utility for loading and applying application window and taskbar icons.
|
||||
* Loads multi-resolution lib3270j icons from resources.
|
||||
*/
|
||||
public final class AppIcon {
|
||||
|
||||
private static final Logger log = Logger.getLogger(AppIcon.class.getName());
|
||||
|
||||
private static final int[] ICON_SIZES = {16, 24, 32, 48, 64, 128, 256, 512};
|
||||
private static List<Image> cachedIcons = null;
|
||||
private static Image defaultIcon = null;
|
||||
|
||||
private AppIcon() {}
|
||||
|
||||
/**
|
||||
* Retrieves the list of available application icon images in various resolutions.
|
||||
*
|
||||
* @return an unmodifiable list of Image objects, or empty list if none could be loaded.
|
||||
*/
|
||||
public static synchronized List<Image> getAppIcons() {
|
||||
if (cachedIcons == null) {
|
||||
loadIcons();
|
||||
}
|
||||
return cachedIcons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default application icon image (256x256 or best available).
|
||||
*
|
||||
* @return the default Image, or null if no icon is available.
|
||||
*/
|
||||
public static synchronized Image getDefaultIcon() {
|
||||
if (cachedIcons == null) {
|
||||
loadIcons();
|
||||
}
|
||||
return defaultIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the application icons to a target Window (JFrame, JDialog, etc.)
|
||||
* and updates the desktop Taskbar icon if supported.
|
||||
*
|
||||
* @param window the Window to set icon images for.
|
||||
*/
|
||||
public static void applyTo(Window window) {
|
||||
if (GraphicsEnvironment.isHeadless()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Image> icons = getAppIcons();
|
||||
if (icons.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window != null) {
|
||||
window.setIconImages(icons);
|
||||
}
|
||||
|
||||
// Apply to OS Taskbar / Dock if supported (Java 9+)
|
||||
try {
|
||||
if (Taskbar.isTaskbarSupported()) {
|
||||
Taskbar taskbar = Taskbar.getTaskbar();
|
||||
if (taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) {
|
||||
Image bestIcon = defaultIcon != null ? defaultIcon : icons.get(icons.size() - 1);
|
||||
taskbar.setIconImage(bestIcon);
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
log.log(Level.FINE, "Taskbar icon could not be set: {0}", t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadIcons() {
|
||||
List<Image> icons = new ArrayList<>();
|
||||
|
||||
for (int size : ICON_SIZES) {
|
||||
String path = "/icons/lib3270j_" + size + ".png";
|
||||
Image img = loadImage(path);
|
||||
if (img != null) {
|
||||
icons.add(img);
|
||||
}
|
||||
}
|
||||
|
||||
if (icons.isEmpty()) {
|
||||
Image img = loadImage("/icons/lib3270j.png");
|
||||
if (img != null) {
|
||||
icons.add(img);
|
||||
}
|
||||
}
|
||||
|
||||
if (!icons.isEmpty()) {
|
||||
for (Image img : icons) {
|
||||
if (img.getWidth(null) == 256) {
|
||||
defaultIcon = img;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultIcon == null) {
|
||||
defaultIcon = icons.get(icons.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
cachedIcons = Collections.unmodifiableList(icons);
|
||||
}
|
||||
|
||||
private static Image loadImage(String resourcePath) {
|
||||
try {
|
||||
URL url = AppIcon.class.getResource(resourcePath);
|
||||
if (url != null) {
|
||||
BufferedImage img = ImageIO.read(url);
|
||||
if (img != null) {
|
||||
return img;
|
||||
}
|
||||
}
|
||||
try (InputStream in = AppIcon.class.getResourceAsStream(resourcePath)) {
|
||||
if (in != null) {
|
||||
return ImageIO.read(in);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Failed to load icon from " + resourcePath, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
|
||||
/**
|
||||
* Dialogue shown when a connection is initiated while j3270 is already connected.
|
||||
* Asks the user whether the connection should be opened in a new window or
|
||||
* if the current connection should be aborted.
|
||||
*/
|
||||
public class ConnectChoiceDialog extends JDialog {
|
||||
|
||||
public enum Choice {
|
||||
NEW_WINDOW,
|
||||
ABORT_CURRENT,
|
||||
CANCEL
|
||||
}
|
||||
|
||||
private Choice result = Choice.CANCEL;
|
||||
private final JButton newWindowBtn;
|
||||
private final JButton abortBtn;
|
||||
private final JButton cancelBtn;
|
||||
private final JLabel titleLabel;
|
||||
private final JLabel line1;
|
||||
private final JLabel line2;
|
||||
|
||||
public ConnectChoiceDialog(Window owner, String currentTarget) {
|
||||
super(owner, "Active Connection", ModalityType.APPLICATION_MODAL);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
setResizable(false);
|
||||
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(16, 16));
|
||||
mainPanel.setBorder(new EmptyBorder(20, 24, 20, 24));
|
||||
|
||||
// Message Panel
|
||||
JPanel messagePanel = new JPanel();
|
||||
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
|
||||
messagePanel.setOpaque(false);
|
||||
|
||||
titleLabel = new JLabel("Connection Already Active");
|
||||
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 2));
|
||||
messagePanel.add(titleLabel);
|
||||
messagePanel.add(Box.createVerticalStrut(10));
|
||||
|
||||
String targetText = (currentTarget != null && !currentTarget.trim().isEmpty())
|
||||
? currentTarget.trim()
|
||||
: "the host";
|
||||
line1 = new JLabel("j3270 is currently connected to " + targetText + ".");
|
||||
line1.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
messagePanel.add(line1);
|
||||
|
||||
messagePanel.add(Box.createVerticalStrut(6));
|
||||
line2 = new JLabel("Would you like to open the connection in a new window or abort the current connection?");
|
||||
line2.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
messagePanel.add(line2);
|
||||
|
||||
mainPanel.add(messagePanel, BorderLayout.CENTER);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
cancelBtn = new JButton("Cancel");
|
||||
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
cancelBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
cancelBtn.addActionListener(e -> {
|
||||
result = Choice.CANCEL;
|
||||
dispose();
|
||||
});
|
||||
|
||||
abortBtn = new JButton("Abort Current Connection");
|
||||
ThemeManager.styleButton(abortBtn, ThemeManager.ButtonVariant.DANGER);
|
||||
abortBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
|
||||
abortBtn.addActionListener(e -> {
|
||||
result = Choice.ABORT_CURRENT;
|
||||
dispose();
|
||||
});
|
||||
|
||||
newWindowBtn = new JButton("New Window");
|
||||
ThemeManager.styleButton(newWindowBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
newWindowBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
newWindowBtn.addActionListener(e -> {
|
||||
result = Choice.NEW_WINDOW;
|
||||
dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(abortBtn);
|
||||
buttonPanel.add(newWindowBtn);
|
||||
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// ESC key cancels
|
||||
getRootPane().registerKeyboardAction(e -> {
|
||||
result = Choice.CANCEL;
|
||||
dispose();
|
||||
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
|
||||
getRootPane().setDefaultButton(newWindowBtn);
|
||||
|
||||
pack();
|
||||
if (owner != null) {
|
||||
setLocationRelativeTo(owner);
|
||||
} else {
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
}
|
||||
|
||||
public Choice getChoice() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public JButton getNewWindowButton() {
|
||||
return newWindowBtn;
|
||||
}
|
||||
|
||||
public JButton getAbortButton() {
|
||||
return abortBtn;
|
||||
}
|
||||
|
||||
public JButton getCancelButton() {
|
||||
return cancelBtn;
|
||||
}
|
||||
|
||||
public JLabel getTitleLabel() {
|
||||
return titleLabel;
|
||||
}
|
||||
|
||||
public JLabel getLine1() {
|
||||
return line1;
|
||||
}
|
||||
|
||||
public JLabel getLine2() {
|
||||
return line2;
|
||||
}
|
||||
|
||||
public static Choice showPrompt(Window owner, String currentTarget) {
|
||||
ConnectChoiceDialog dialog = new ConnectChoiceDialog(owner, currentTarget);
|
||||
dialog.setVisible(true);
|
||||
return dialog.getChoice();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,31 @@
|
||||
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.TerminalModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
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 {
|
||||
|
||||
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 portField;
|
||||
private JComboBox<TerminalModel> modelCombo;
|
||||
@@ -21,77 +36,137 @@ public class ConnectDialog extends JDialog {
|
||||
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
|
||||
private JComboBox<String> codePageCombo;
|
||||
private JTextField luField;
|
||||
private JComboBox<String> hostTypeCombo;
|
||||
private JCheckBox tlsCheckBox;
|
||||
private JCheckBox verifyCertCheckBox;
|
||||
private JCheckBox tn3270eCheckBox;
|
||||
private JCheckBox keepAliveCheckBox;
|
||||
private JCheckBox autoReconnectCheckBox;
|
||||
|
||||
// Per-connection proxy configuration
|
||||
private JComboBox<ConnectionConfig.ProxyType> proxyTypeCombo;
|
||||
private JTextField proxyHostField;
|
||||
private JTextField proxyPortField;
|
||||
private JTextField proxyUserField;
|
||||
private JPasswordField proxyPassField;
|
||||
|
||||
private boolean confirmed;
|
||||
private ConnectionConfig result;
|
||||
|
||||
public ConnectDialog(Frame parent) {
|
||||
super(parent, "Connect to Host", true);
|
||||
AppIcon.applyTo(this);
|
||||
buildUI();
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
setResizable(false);
|
||||
setResizable(true);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new GridBagLayout());
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
||||
JPanel contentPanel = new JPanel(new GridBagLayout());
|
||||
contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(5, 6, 5, 6);
|
||||
gbc.insets = new Insets(4, 5, 4, 5);
|
||||
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.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:");
|
||||
hostLabel.setFont(labelFont);
|
||||
mainPanel.add(hostLabel, gbc);
|
||||
contentPanel.add(hostLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
hostField = createField(20);
|
||||
mainPanel.add(hostField, gbc);
|
||||
contentPanel.add(hostField, gbc);
|
||||
|
||||
// Port
|
||||
gridy++;
|
||||
|
||||
// 2. Port
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel portLabel = new JLabel("Port:");
|
||||
portLabel.setFont(labelFont);
|
||||
mainPanel.add(portLabel, gbc);
|
||||
contentPanel.add(portLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
portField = createField(6);
|
||||
portField.setText("23");
|
||||
mainPanel.add(portField, gbc);
|
||||
contentPanel.add(portField, gbc);
|
||||
|
||||
// Model
|
||||
gridy++;
|
||||
|
||||
// 3. Model
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel modelLabel = new JLabel("Model:");
|
||||
modelLabel.setFont(labelFont);
|
||||
mainPanel.add(modelLabel, gbc);
|
||||
contentPanel.add(modelLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
modelCombo = new JComboBox<>(TerminalModel.values());
|
||||
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
|
||||
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
modelCombo.setFont(ThemeManager.getMonospacedUiFont());
|
||||
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.gridy = 3;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
dynamicDimLabel = new JLabel("Screen Size:");
|
||||
dynamicDimLabel.setFont(labelFont);
|
||||
mainPanel.add(dynamicDimLabel, gbc);
|
||||
contentPanel.add(dynamicDimLabel, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
@@ -112,7 +187,7 @@ public class ConnectDialog extends JDialog {
|
||||
dynamicDimPanel.add(dynamicRowsSpinner);
|
||||
dynamicDimPanel.add(colsLabel);
|
||||
dynamicDimPanel.add(dynamicColsSpinner);
|
||||
mainPanel.add(dynamicDimPanel, gbc);
|
||||
contentPanel.add(dynamicDimPanel, gbc);
|
||||
|
||||
Runnable updateDynamicVisibility = () -> {
|
||||
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
|
||||
@@ -124,40 +199,83 @@ public class ConnectDialog extends JDialog {
|
||||
modelCombo.addActionListener(e -> updateDynamicVisibility.run());
|
||||
updateDynamicVisibility.run();
|
||||
|
||||
// LU Name
|
||||
gridy++;
|
||||
|
||||
// 5. LU Name
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel luLabel = new JLabel("LU Name:");
|
||||
luLabel.setFont(labelFont);
|
||||
mainPanel.add(luLabel, gbc);
|
||||
contentPanel.add(luLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
luField = createField(12);
|
||||
mainPanel.add(luField, gbc);
|
||||
contentPanel.add(luField, gbc);
|
||||
|
||||
// Graphics Mode
|
||||
gridy++;
|
||||
|
||||
// Host Type (CMS/TSO/CICS)
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 5;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel hostTypeLabel = new JLabel("Host Type:");
|
||||
hostTypeLabel.setFont(labelFont);
|
||||
contentPanel.add(hostTypeLabel, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
String[] hostTypes = {"TSO", "CMS", "CICS"};
|
||||
hostTypeCombo = new JComboBox<>(hostTypes);
|
||||
hostTypeCombo.setFont(ThemeManager.getUiFont());
|
||||
ThemeManager.styleComboBox(hostTypeCombo);
|
||||
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
|
||||
contentPanel.add(hostTypeCombo, gbc);
|
||||
|
||||
gridy++;
|
||||
|
||||
// 6. Graphics Mode
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel graphicsLabel = new JLabel("Graphics:");
|
||||
graphicsLabel.setFont(labelFont);
|
||||
mainPanel.add(graphicsLabel, gbc);
|
||||
contentPanel.add(graphicsLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
|
||||
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
graphicsCombo.setFont(ThemeManager.getUiFont());
|
||||
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.gridy = 6;
|
||||
gbc.gridy = gridy;
|
||||
gbc.weightx = 0;
|
||||
JLabel cpLabel = new JLabel("Code Page:");
|
||||
cpLabel.setFont(labelFont);
|
||||
mainPanel.add(cpLabel, gbc);
|
||||
contentPanel.add(cpLabel, gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1.0;
|
||||
String[] commonCodePages = {
|
||||
@@ -178,43 +296,44 @@ public class ConnectDialog extends JDialog {
|
||||
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
|
||||
"905 - Turkey (Turkish Latin-3)",
|
||||
"420 - Arabic Bilingual",
|
||||
"424 - Hebrew (with Lowercase)",
|
||||
"803 - Hebrew Old / Standard",
|
||||
"838 - Thai",
|
||||
"1160 - Thai (Euro \u20AC)",
|
||||
"424 - Hebrew (with Latin)",
|
||||
"803 - Hebrew Character Set",
|
||||
"838 - Thai Extended",
|
||||
"1160 - Thai Euro \u20AC",
|
||||
"1025 - Cyrillic Multilingual",
|
||||
"1123 - Cyrillic Ukraine",
|
||||
"1154 - Cyrillic Multilingual (Euro \u20AC)",
|
||||
"880 - Cyrillic Russian",
|
||||
"1154 - Cyrillic Euro \u20AC",
|
||||
"880 - Cyrillic (Russian)",
|
||||
"1140 - US / Canada (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)",
|
||||
"1149 - Iceland (Euro \u20AC)",
|
||||
"930 - Japanese Katakana Mixed DBCS",
|
||||
"939 - Japanese Latin Mixed DBCS",
|
||||
"935 - Simplified Chinese Mixed DBCS",
|
||||
"1388 - Simplified Chinese Extended Mixed DBCS",
|
||||
"937 - Traditional Chinese Mixed DBCS",
|
||||
"1371 - Traditional Chinese Extended Mixed DBCS",
|
||||
"1388 - Simplified Chinese Extended DBCS",
|
||||
"1371 - Traditional Chinese Extended DBCS",
|
||||
"933 - Korean Mixed DBCS"
|
||||
};
|
||||
codePageCombo = new JComboBox<>(commonCodePages);
|
||||
codePageCombo.setEditable(true);
|
||||
String currentCp = haus.nightmare.j3270.config.Settings.getCodePage();
|
||||
for (String item : commonCodePages) {
|
||||
if (item.startsWith(currentCp + " ") || item.equals(currentCp)) {
|
||||
codePageCombo.setSelectedItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
|
||||
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.gridy = 7;
|
||||
gbc.weightx = 1.0;
|
||||
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
|
||||
gbc.gridy = gridy;
|
||||
tlsCheckBox = new JCheckBox("Use TLS / SSL Encryption");
|
||||
ThemeManager.styleCheckBox(tlsCheckBox);
|
||||
tlsCheckBox.setFont(labelFont);
|
||||
tlsCheckBox.addActionListener(e -> {
|
||||
@@ -227,46 +346,169 @@ public class ConnectDialog extends JDialog {
|
||||
portField.setText("23");
|
||||
}
|
||||
});
|
||||
mainPanel.add(tlsCheckBox, gbc);
|
||||
contentPanel.add(tlsCheckBox, gbc);
|
||||
|
||||
// Verify Certificate Checkbox
|
||||
gridy++;
|
||||
|
||||
// 9. Verify Cert Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 8;
|
||||
gbc.gridy = gridy;
|
||||
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
|
||||
ThemeManager.styleCheckBox(verifyCertCheckBox);
|
||||
verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
verifyCertCheckBox.setFont(ThemeManager.getUiFont());
|
||||
verifyCertCheckBox.setSelected(true);
|
||||
verifyCertCheckBox.setEnabled(false);
|
||||
mainPanel.add(verifyCertCheckBox, gbc);
|
||||
contentPanel.add(verifyCertCheckBox, gbc);
|
||||
|
||||
// TN3270E Checkbox
|
||||
gridy++;
|
||||
|
||||
// 10. TN3270E Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 9;
|
||||
gbc.gridy = gridy;
|
||||
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
|
||||
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());
|
||||
mainPanel.add(tn3270eCheckBox, gbc);
|
||||
contentPanel.add(tn3270eCheckBox, gbc);
|
||||
|
||||
// Keep-Alive Checkbox
|
||||
gridy++;
|
||||
|
||||
// 11. Keep-Alive Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 10;
|
||||
gbc.gridy = gridy;
|
||||
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
|
||||
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());
|
||||
mainPanel.add(keepAliveCheckBox, gbc);
|
||||
contentPanel.add(keepAliveCheckBox, gbc);
|
||||
|
||||
// Auto-Reconnect Checkbox
|
||||
gridy++;
|
||||
|
||||
// 12. Auto-Reconnect Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 11;
|
||||
gbc.gridy = gridy;
|
||||
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
|
||||
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());
|
||||
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));
|
||||
|
||||
JButton connectBtn = new JButton("Connect");
|
||||
@@ -284,13 +526,14 @@ public class ConnectDialog extends JDialog {
|
||||
|
||||
buttonPanel.add(cancelBtn);
|
||||
buttonPanel.add(connectBtn);
|
||||
contentPanel.add(buttonPanel, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 12;
|
||||
gbc.gridwidth = 2;
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
JScrollPane scrollPane = new JScrollPane(contentPanel);
|
||||
scrollPane.setBorder(BorderFactory.createEmptyBorder());
|
||||
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
scrollPane.getVerticalScrollBar().setUnitIncrement(14);
|
||||
setContentPane(scrollPane);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// Enter key triggers connect
|
||||
@@ -299,11 +542,197 @@ public class ConnectDialog extends JDialog {
|
||||
|
||||
private JTextField createField(int cols) {
|
||||
JTextField field = new JTextField(cols);
|
||||
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
|
||||
field.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextField(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);
|
||||
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
|
||||
}
|
||||
}
|
||||
|
||||
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() : "");
|
||||
setInitialHostType(h.getHostType() != null ? h.getHostType() : "TSO");
|
||||
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();
|
||||
String hostType = getSelectedHostType();
|
||||
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,
|
||||
hostType, tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
|
||||
}
|
||||
|
||||
// ================= Connect Action =================
|
||||
|
||||
private void onConnect() {
|
||||
String host = hostField.getText().trim();
|
||||
if (host.isEmpty()) {
|
||||
@@ -320,20 +749,35 @@ public class ConnectDialog extends JDialog {
|
||||
}
|
||||
|
||||
TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
|
||||
result = new ConnectionConfig(host, port, selectedModel);
|
||||
if (selectedModel != null && selectedModel.isDynamic()) {
|
||||
if (selectedModel == null) {
|
||||
selectedModel = TerminalModel.IBM_3279_4;
|
||||
}
|
||||
|
||||
// Support full host string parsing if the user pasted flags or prefixes into host field
|
||||
if (host.contains(" ") || host.startsWith("L:") || host.startsWith("P:") || host.startsWith("--proxy=") || host.startsWith("-proxy=")) {
|
||||
result = ConnectionConfig.parseHostString(host, port, selectedModel);
|
||||
} else {
|
||||
result = new ConnectionConfig(host, port, selectedModel);
|
||||
}
|
||||
|
||||
if (selectedModel.isDynamic()) {
|
||||
int dRows = (Integer) dynamicRowsSpinner.getValue();
|
||||
int dCols = (Integer) dynamicColsSpinner.getValue();
|
||||
result.setDynamicDimensions(dRows, dCols);
|
||||
haus.nightmare.j3270.config.Settings.setDynamicRows(dRows);
|
||||
haus.nightmare.j3270.config.Settings.setDynamicCols(dCols);
|
||||
}
|
||||
|
||||
String lu = luField.getText().trim();
|
||||
if (!lu.isEmpty()) {
|
||||
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();
|
||||
if (cpSelection != null) {
|
||||
String cpStr = cpSelection.toString().trim();
|
||||
@@ -360,6 +804,34 @@ public class ConnectDialog extends JDialog {
|
||||
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
|
||||
|
||||
// Apply per-connection proxy parameters strictly to this connection
|
||||
ConnectionConfig.ProxyType pType = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
|
||||
String pHost = proxyHostField.getText().trim();
|
||||
if (pType != null && pType != ConnectionConfig.ProxyType.NONE && !pHost.isEmpty()) {
|
||||
int pPort = 0;
|
||||
try {
|
||||
pPort = Integer.parseInt(proxyPortField.getText().trim());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
if (pPort <= 0) {
|
||||
pPort = (pType == ConnectionConfig.ProxyType.HTTP) ? 8080 : 1080;
|
||||
}
|
||||
String pUser = proxyUserField.getText().trim();
|
||||
String pPass = new String(proxyPassField.getPassword());
|
||||
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
|
||||
}
|
||||
|
||||
result.setHostType(getSelectedHostType());
|
||||
if (selectedProfileId != null) {
|
||||
result.setSavedConnection(true);
|
||||
result.setSavedProfileId(selectedProfileId);
|
||||
SavedHost existing = HostStorage.findById(selectedProfileId);
|
||||
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
|
||||
? existing.getName() : host;
|
||||
result.setSavedProfileName(pName);
|
||||
SavedHost updated = buildCurrentSavedHost(pName);
|
||||
HostStorage.saveHost(updated);
|
||||
}
|
||||
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
@@ -394,6 +866,12 @@ public class ConnectDialog extends JDialog {
|
||||
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) {
|
||||
if (cp != null && codePageCombo != null) {
|
||||
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
|
||||
@@ -406,4 +884,89 @@ public class ConnectDialog extends JDialog {
|
||||
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());
|
||||
}
|
||||
|
||||
public void setInitialHostType(String hostType) {
|
||||
if (hostType != null && hostTypeCombo != null) {
|
||||
for (int i = 0; i < hostTypeCombo.getItemCount(); i++) {
|
||||
if (hostTypeCombo.getItemAt(i).equalsIgnoreCase(hostType.trim())) {
|
||||
hostTypeCombo.setSelectedIndex(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setInitialHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType hostType) {
|
||||
if (hostType != null) {
|
||||
setInitialHostType(hostType.name());
|
||||
}
|
||||
}
|
||||
|
||||
public String getSelectedHostType() {
|
||||
if (hostTypeCombo != null && hostTypeCombo.getSelectedItem() != null) {
|
||||
return hostTypeCombo.getSelectedItem().toString().trim().toUpperCase();
|
||||
}
|
||||
return "TSO";
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ft.FTConfig.HostType getSelectedFtHostType() {
|
||||
try {
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(getSelectedHostType());
|
||||
} catch (Exception e) {
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
|
||||
}
|
||||
}
|
||||
|
||||
public JComboBox<String> getHostTypeCombo() {
|
||||
return hostTypeCombo;
|
||||
}
|
||||
|
||||
public String getSelectedProfileId() {
|
||||
return selectedProfileId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
super(parent, "3270 Presentation Space Field Inspector", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI();
|
||||
setSize(850, 420);
|
||||
@@ -60,7 +61,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
JPanel topPanel = new JPanel(new BorderLayout());
|
||||
topPanel.setOpaque(false);
|
||||
countLabel = new JLabel("0 fields detected on screen");
|
||||
countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13));
|
||||
countLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
topPanel.add(countLabel, BorderLayout.WEST);
|
||||
|
||||
JButton refreshBtn = new JButton("Refresh");
|
||||
@@ -83,12 +84,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene
|
||||
|
||||
table = new JTable(tableModel);
|
||||
ThemeManager.styleTable(table);
|
||||
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
table.setFont(ThemeManager.getMonospacedUiFont());
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(20);
|
||||
table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
|
||||
header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
|
||||
// Column widths
|
||||
int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260};
|
||||
|
||||
@@ -32,6 +32,7 @@ public class FindDialog extends JDialog {
|
||||
super(parent, "Find on Screen", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI();
|
||||
pack();
|
||||
@@ -50,7 +51,7 @@ public class FindDialog extends JDialog {
|
||||
gbc.insets = new Insets(4, 4, 4, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Search text
|
||||
gbc.gridx = 0;
|
||||
@@ -63,7 +64,7 @@ public class FindDialog extends JDialog {
|
||||
gbc.weightx = 1.0;
|
||||
searchField = new JTextField(20);
|
||||
searchField.setText(lastSearchText);
|
||||
searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
searchField.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextField(searchField);
|
||||
formPanel.add(searchField, gbc);
|
||||
|
||||
@@ -102,7 +103,7 @@ public class FindDialog extends JDialog {
|
||||
gbc.gridwidth = 2;
|
||||
statusLabel = new JLabel(" ");
|
||||
statusLabel.setForeground(ThemeManager.getOiaFgAlert());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
statusLabel.setFont(labelFont);
|
||||
formPanel.add(statusLabel, gbc);
|
||||
|
||||
mainPanel.add(formPanel, BorderLayout.CENTER);
|
||||
|
||||
@@ -3,15 +3,19 @@ package haus.nightmare.j3270.ui;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryParser;
|
||||
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import javax.swing.table.JTableHeader;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
@@ -19,6 +23,7 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
|
||||
* Aligned with IBM Host On-Demand presentation space queries and clipboard parsing.
|
||||
*/
|
||||
public class HostDirectoryDialog extends JDialog {
|
||||
|
||||
@@ -27,22 +32,26 @@ public class HostDirectoryDialog extends JDialog {
|
||||
|
||||
private JComboBox<FTConfig.HostType> hostTypeCombo;
|
||||
private JTextField queryField;
|
||||
private JButton queryHostBtn;
|
||||
private JTable table;
|
||||
private DefaultTableModel tableModel;
|
||||
private JLabel statusLabel;
|
||||
|
||||
private String selectedHostFile = null;
|
||||
private HostDirectoryEntry selectedEntry = null;
|
||||
private boolean confirmed = false;
|
||||
|
||||
private final List<HostDirectoryEntry> allEntries = new ArrayList<>();
|
||||
private final List<HostDirectoryEntry> currentEntries = new ArrayList<>();
|
||||
|
||||
public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) {
|
||||
super(parent, "Host Catalog & Directory Browser", true);
|
||||
this.client = client;
|
||||
this.initialHostType = hostType;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI(initialQuery);
|
||||
setSize(700, 420);
|
||||
setSize(840, 480);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
@@ -50,7 +59,7 @@ public class HostDirectoryDialog extends JDialog {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12));
|
||||
|
||||
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
Font font = ThemeManager.getUiFont();
|
||||
|
||||
// Top Query bar
|
||||
JPanel topPanel = new JPanel(new GridBagLayout());
|
||||
@@ -68,40 +77,75 @@ public class HostDirectoryDialog extends JDialog {
|
||||
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
|
||||
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
|
||||
ThemeManager.styleComboBox(hostTypeCombo);
|
||||
hostTypeCombo.addActionListener(e -> {
|
||||
setupTableColumns();
|
||||
FTConfig.HostType ht = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
if (ht == FTConfig.HostType.CMS) {
|
||||
String cur = queryField.getText().trim();
|
||||
if (cur.isEmpty() || cur.equals("* * *") || cur.equals("*")) {
|
||||
queryField.setText("* * A");
|
||||
}
|
||||
} else if (ht == FTConfig.HostType.TSO) {
|
||||
String cur = queryField.getText().trim();
|
||||
if (cur.equals("* * A") || cur.equals("* * *")) {
|
||||
queryField.setText("");
|
||||
}
|
||||
}
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
queryHost();
|
||||
} else {
|
||||
parseCurrentScreen();
|
||||
}
|
||||
});
|
||||
topPanel.add(hostTypeCombo, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
JLabel queryLabel = new JLabel("Query Pattern / Text:");
|
||||
JLabel queryLabel = new JLabel("Filter / Pattern:");
|
||||
queryLabel.setFont(font);
|
||||
topPanel.add(queryLabel, gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.weightx = 1.0;
|
||||
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16);
|
||||
queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
ThemeManager.styleTextField(queryField);
|
||||
String defaultQuery = resolveDefaultQuery(initialHostType, initialQuery);
|
||||
queryField = new JTextField(defaultQuery, 16);
|
||||
queryField.setFont(ThemeManager.getMonospacedUiFont());
|
||||
queryField.addActionListener(e -> onFilterOrParse());
|
||||
topPanel.add(queryField, gbc);
|
||||
|
||||
gbc.gridx = 4;
|
||||
gbc.weightx = 0;
|
||||
JButton parseBtn = new JButton("Query / Parse");
|
||||
ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
parseBtn.setFont(font);
|
||||
parseBtn.addActionListener(e -> runQuery());
|
||||
topPanel.add(parseBtn, gbc);
|
||||
JButton filterBtn = new JButton("Filter / Parse");
|
||||
ThemeManager.styleButton(filterBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
filterBtn.setFont(font);
|
||||
filterBtn.addActionListener(e -> onFilterOrParse());
|
||||
topPanel.add(filterBtn, gbc);
|
||||
|
||||
gbc.gridx = 5;
|
||||
queryHostBtn = new JButton("Query Host");
|
||||
ThemeManager.styleButton(queryHostBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
queryHostBtn.setFont(font);
|
||||
queryHostBtn.setToolTipText("Send LISTCAT or LISTFILE query to active host session");
|
||||
queryHostBtn.addActionListener(e -> queryHost());
|
||||
topPanel.add(queryHostBtn, gbc);
|
||||
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Table
|
||||
tableModel = new DefaultTableModel();
|
||||
tableModel = new DefaultTableModel() {
|
||||
@Override
|
||||
public boolean isCellEditable(int row, int column) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
table = new JTable(tableModel);
|
||||
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
|
||||
ThemeManager.styleTable(table);
|
||||
table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
table.setFont(ThemeManager.getMonospacedUiFont());
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
table.setRowHeight(20);
|
||||
table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8));
|
||||
|
||||
JTableHeader header = table.getTableHeader();
|
||||
header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
|
||||
header.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
|
||||
|
||||
table.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
@@ -113,26 +157,42 @@ public class HostDirectoryDialog extends JDialog {
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(table);
|
||||
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
|
||||
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
|
||||
ThemeManager.styleScrollPane(scrollPane);
|
||||
mainPanel.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 6));
|
||||
bottomPanel.setOpaque(false);
|
||||
bottomPanel.setBorder(BorderFactory.createEmptyBorder(6, 4, 4, 4));
|
||||
|
||||
statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
statusLabel.setFont(font);
|
||||
bottomPanel.add(statusLabel, BorderLayout.NORTH);
|
||||
|
||||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
btnPanel.setOpaque(false);
|
||||
|
||||
JButton pasteScreenBtn = new JButton("Parse Current Screen");
|
||||
JButton inspectBtn = new JButton("Inspect (LISTDS)");
|
||||
ThemeManager.styleButton(inspectBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
inspectBtn.setFont(font);
|
||||
inspectBtn.setToolTipText("Retrieve VTOC attributes (RECFM, LRECL, BLKSIZE) for the selected dataset via TSO LISTDS");
|
||||
inspectBtn.addActionListener(e -> inspectSelectedDataset());
|
||||
|
||||
JButton pasteScreenBtn = new JButton("Parse Screen");
|
||||
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pasteScreenBtn.setFont(font);
|
||||
pasteScreenBtn.setToolTipText("Parse items currently visible on the 3270 presentation space");
|
||||
pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
|
||||
|
||||
JButton pasteClipBtn = new JButton("Paste Clipboard");
|
||||
ThemeManager.styleButton(pasteClipBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pasteClipBtn.setFont(font);
|
||||
pasteClipBtn.setToolTipText("Parse host filenames copied to the system clipboard (HoD compatible)");
|
||||
pasteClipBtn.addActionListener(e -> parseClipboard());
|
||||
|
||||
JButton selectBtn = new JButton("Select Dataset");
|
||||
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
selectBtn.setFont(font);
|
||||
@@ -146,23 +206,28 @@ public class HostDirectoryDialog extends JDialog {
|
||||
dispose();
|
||||
});
|
||||
|
||||
btnPanel.add(inspectBtn);
|
||||
btnPanel.add(pasteScreenBtn);
|
||||
btnPanel.add(pasteClipBtn);
|
||||
btnPanel.add(Box.createHorizontalStrut(10));
|
||||
btnPanel.add(selectBtn);
|
||||
btnPanel.add(cancelBtn);
|
||||
|
||||
bottomPanel.add(btnPanel, BorderLayout.EAST);
|
||||
bottomPanel.add(btnPanel, BorderLayout.SOUTH);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
|
||||
// Initial setup
|
||||
// Initial setup: columns and screen parse or host query
|
||||
setupTableColumns();
|
||||
if (initialQuery != null && !initialQuery.isEmpty()) {
|
||||
runQuery();
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
queryHost();
|
||||
} else {
|
||||
parseCurrentScreen();
|
||||
if (!queryField.getText().trim().isEmpty()) {
|
||||
applyFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,76 +238,665 @@ public class HostDirectoryDialog extends JDialog {
|
||||
tableModel.setColumnIdentifiers(new Object[]{
|
||||
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
|
||||
});
|
||||
int[] widths = {120, 100, 80, 70, 70, 90, 170};
|
||||
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
|
||||
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
|
||||
}
|
||||
} else {
|
||||
tableModel.setColumnIdentifiers(new Object[]{
|
||||
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents"
|
||||
"Dataset Name", "Volume"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void runQuery() {
|
||||
setupTableColumns();
|
||||
currentEntries.clear();
|
||||
String query = queryField.getText().trim();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
|
||||
if (client != null && client.getXfer() != null) {
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(query);
|
||||
currentEntries.addAll(cmsEntries);
|
||||
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
||||
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(query);
|
||||
currentEntries.addAll(tsoEntries);
|
||||
for (TSODirectoryEntry entry : tsoEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
||||
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
||||
});
|
||||
int[] widths = {620, 160};
|
||||
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
|
||||
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
|
||||
if (i == 0) {
|
||||
table.getColumnModel().getColumn(0).setMinWidth(250);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusLabel.setText(currentEntries.size() + " entries found.");
|
||||
private void onFilterOrParse() {
|
||||
String query = queryField.getText().trim();
|
||||
// If text contains multiple lines, treat it as pasted raw text to parse
|
||||
if (query.contains("\n") || query.contains("\r") || query.toUpperCase().contains("NONVSAM")
|
||||
|| query.toUpperCase().contains("IN-CAT") || query.toUpperCase().contains("FILENAME")) {
|
||||
parseText(query, "query text");
|
||||
} else if (allEntries.isEmpty() && client != null && client.getConnectionState().isFullSession()) {
|
||||
queryHost();
|
||||
} else {
|
||||
applyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
private void queryHost() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
statusLabel.setText("No active session to query.");
|
||||
return;
|
||||
}
|
||||
|
||||
String pattern = queryField.getText().trim();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
statusLabel.setText("Querying host (" + hostType + ")...");
|
||||
if (queryHostBtn != null) queryHostBtn.setEnabled(false);
|
||||
|
||||
SwingWorker<String, Void> worker = new SwingWorker<>() {
|
||||
@Override
|
||||
protected String doInBackground() throws Exception {
|
||||
if (client == null || client.getPS() == null) return "";
|
||||
|
||||
StringBuilder allPages = new StringBuilder();
|
||||
int maxPages = 500;
|
||||
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
// CMS Query:
|
||||
// 1. Clear presentation space first to ensure clean state
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
|
||||
// 2. Format CMS LISTFILE query command (default to minidisk A unless specified)
|
||||
String cmsFilter = formatCMSFilter(pattern);
|
||||
client.getPS().SendKeys("LISTFILE " + cmsFilter + " ( nohead alloc date[enter]");
|
||||
Thread.sleep(100);
|
||||
|
||||
// 3. Multi-page collection loop:
|
||||
// CMS pagination displays MORE... or HOLDING when a screen is full.
|
||||
// When advanced with [clear], CMS flashes RUNNING while generating the next screen.
|
||||
// We poll up to 10 seconds per page for MORE... / HOLDING or Ready;
|
||||
for (int page = 0; page < maxPages; page++) {
|
||||
long startWait = System.currentTimeMillis();
|
||||
long timeoutMs = 10000; // 10 seconds per page
|
||||
String screen = "";
|
||||
boolean isMore = false;
|
||||
|
||||
while (System.currentTimeMillis() - startWait < timeoutMs) {
|
||||
waitForKeyboardUnlock(client, 500);
|
||||
int rows = client.getPS().getRows();
|
||||
int cols = client.getPS().getCols();
|
||||
screen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
|
||||
isMore = true;
|
||||
break;
|
||||
}
|
||||
if (upper.contains("READY;") || upper.contains("READY(")) {
|
||||
break;
|
||||
}
|
||||
// Still RUNNING or transitioning, wait and re-check
|
||||
Thread.sleep(150);
|
||||
}
|
||||
|
||||
allPages.append(screen).append("\n");
|
||||
|
||||
if (isMore) {
|
||||
// In CMS, sending [clear] advances to the next page of output
|
||||
client.getPS().SendKeys("[clear]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
} else {
|
||||
// Ready; encountered or timed out -> listing complete
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically clear the listing from the terminal presentation space
|
||||
// so the terminal screen is left clean for the user while keeping the dialog populated
|
||||
if (client.getPS() != null) {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
}
|
||||
|
||||
} else {
|
||||
// TSO Query:
|
||||
// Check if current screen is in ISPF (formatted screen or containing ===> or ISPF)
|
||||
int rows = client.getPS().getRows();
|
||||
int cols = client.getPS().getCols();
|
||||
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
|
||||
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
|
||||
boolean hasCmdArrow = initialScreen.contains("===>");
|
||||
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
|
||||
|
||||
String tsoCmd = "TSO LISTCAT ALL";
|
||||
if (!pattern.isEmpty()) {
|
||||
String dsnPattern = pattern.trim();
|
||||
if (dsnPattern.startsWith("'") && dsnPattern.endsWith("'") && dsnPattern.length() > 2) {
|
||||
dsnPattern = dsnPattern.substring(1, dsnPattern.length() - 1);
|
||||
}
|
||||
int paren = dsnPattern.indexOf('(');
|
||||
if (paren > 0) {
|
||||
dsnPattern = dsnPattern.substring(0, paren);
|
||||
}
|
||||
if (dsnPattern.equals("*") || dsnPattern.equals("* * *") || dsnPattern.equals("*.*")) {
|
||||
tsoCmd = "TSO LISTCAT ALL";
|
||||
} else {
|
||||
String level = dsnPattern;
|
||||
if (level.endsWith(".*")) {
|
||||
level = level.substring(0, level.length() - 2);
|
||||
} else if (level.endsWith("*")) {
|
||||
level = level.substring(0, level.length() - 1);
|
||||
}
|
||||
if (level.isEmpty()) {
|
||||
tsoCmd = "TSO LISTCAT ALL";
|
||||
} else {
|
||||
tsoCmd = "TSO LISTCAT LEVEL(" + level + ") ALL";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isIspf) {
|
||||
// In ISPF, do NOT send [clear] because [clear] destroys the ISPF panel.
|
||||
// Locate the command/option line (indicated by ===>) or home to first input field.
|
||||
int arrowPos = initialScreen.indexOf("===>");
|
||||
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
|
||||
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
|
||||
if (inputAddr > 0) {
|
||||
client.getScreenBuffer().setCursorAddress(inputAddr);
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
} else {
|
||||
client.getPS().SendKeys("[home]");
|
||||
}
|
||||
} else {
|
||||
client.getPS().SendKeys("[home]");
|
||||
}
|
||||
client.getPS().SendKeys("[erase_eof]");
|
||||
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||
} else {
|
||||
// In native TSO (ISPF closed, line mode READY prompt)
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||
}
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
// Multi-page collection loop for TSO:
|
||||
// 1. Wait for output to start appearing after sending command
|
||||
long startCmd = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - startCmd < 4000) {
|
||||
waitForKeyboardUnlock(client, 200);
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
String u = s.toUpperCase();
|
||||
if (u.contains("NONVSAM") || u.contains("CLUSTER") || u.contains("GDG")
|
||||
|| u.contains("ALIAS") || u.contains("IN-CAT") || u.contains("IDCAMS")
|
||||
|| u.contains("***") || u.contains("READY") || u.contains("NOT FOUND")
|
||||
|| u.contains("NO ENTRY") || s.contains(" --- ")
|
||||
|| u.contains("--RECFM-") || u.contains("--VOLUMES--")) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(80);
|
||||
}
|
||||
|
||||
// 2. Multi-page collection
|
||||
String prevScreen = "";
|
||||
for (int page = 0; page < maxPages; page++) {
|
||||
waitForKeyboardUnlock(client, 300);
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
prevScreen = screen;
|
||||
allPages.append(screen).append("\n");
|
||||
|
||||
if (upper.contains("***")) {
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
} else if (upper.contains("NONVSAM") || upper.contains("CLUSTER") || upper.contains("GDG")
|
||||
|| upper.contains("ALIAS") || upper.contains("IDCAMS") || screen.contains(" --- ")
|
||||
|| upper.contains("--RECFM-") || upper.contains("--VOLUMES--")) {
|
||||
// If screen shows dataset data without ***, send [enter] once to see if *** or more output is triggered
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 1000);
|
||||
|
||||
int r2 = client.getPS().getRows();
|
||||
int c2 = client.getPS().getCols();
|
||||
String s2 = client.getPS().copyString(0, 0, r2 - 1, c2 - 1);
|
||||
if (s2.toUpperCase().contains("***")) {
|
||||
prevScreen = s2;
|
||||
allPages.append(s2).append("\n");
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
continue;
|
||||
} else {
|
||||
if (!s2.equals(screen) && !s2.trim().isEmpty()) {
|
||||
allPages.append(s2).append("\n");
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Terminal clean-up: clear the list from the terminal presentation space
|
||||
if (client.getPS() != null) {
|
||||
if (isIspf) {
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(150);
|
||||
waitForKeyboardUnlock(client, 1500);
|
||||
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
if (currentScr.toUpperCase().contains("NONVSAM") || currentScr.toUpperCase().contains("IDCAMS")
|
||||
|| currentScr.contains(" --- ") || currentScr.toUpperCase().contains("--RECFM-")
|
||||
|| (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
}
|
||||
} else {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allPages.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
if (queryHostBtn != null) queryHostBtn.setEnabled(true);
|
||||
try {
|
||||
String screenText = get();
|
||||
parseText(screenText, "host query");
|
||||
if (!pattern.isEmpty() && !allEntries.isEmpty()) {
|
||||
applyFilter();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
statusLabel.setText("Host query failed: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
}
|
||||
|
||||
private void inspectSelectedDataset() {
|
||||
int row = table.getSelectedRow();
|
||||
if (row < 0 || row >= currentEntries.size()) {
|
||||
JOptionPane.showMessageDialog(this, "Please select an entry from the table to inspect.",
|
||||
"No Selection", JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
HostDirectoryEntry entry = currentEntries.get(row);
|
||||
if (!(entry instanceof TSODirectoryEntry)) {
|
||||
JOptionPane.showMessageDialog(this, "Inspection is only supported for TSO datasets.",
|
||||
"Not Supported", JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
JOptionPane.showMessageDialog(this, "No active full 3270 session to query host.",
|
||||
"Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
|
||||
String dsn = tso.getDatasetName();
|
||||
if (dsn == null || dsn.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
statusLabel.setText("Inspecting " + dsn + " via TSO LISTDS...");
|
||||
|
||||
SwingWorker<String, Void> worker = new SwingWorker<String, Void>() {
|
||||
@Override
|
||||
protected String doInBackground() throws Exception {
|
||||
StringBuilder output = new StringBuilder();
|
||||
int rows = client.getPS().getRows();
|
||||
int cols = client.getPS().getCols();
|
||||
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
|
||||
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
|
||||
boolean hasCmdArrow = initialScreen.contains("===>");
|
||||
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
|
||||
|
||||
String tsoCmd = "TSO LISTDS '" + dsn + "' STATUS";
|
||||
|
||||
if (isIspf) {
|
||||
int arrowPos = initialScreen.indexOf("===>");
|
||||
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
|
||||
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
|
||||
if (inputAddr > 0) {
|
||||
client.getScreenBuffer().setCursorAddress(inputAddr);
|
||||
client.getScreenBuffer().updateDisplaySnapshot();
|
||||
} else {
|
||||
client.getPS().SendKeys("[home]");
|
||||
}
|
||||
} else {
|
||||
client.getPS().SendKeys("[home]");
|
||||
}
|
||||
client.getPS().SendKeys("[erase_eof]");
|
||||
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||
} else {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 3000);
|
||||
client.getPS().SendKeys(tsoCmd + "[enter]");
|
||||
}
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
// Wait for output to start appearing
|
||||
long startCmd = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - startCmd < 4000) {
|
||||
waitForKeyboardUnlock(client, 200);
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
String u = s.toUpperCase();
|
||||
if (u.contains("--RECFM-") || u.contains("--VOLUMES--") || u.contains("***")
|
||||
|| u.contains("READY") || u.contains("NOT IN CATALOG") || u.contains("NOT FOUND")) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(80);
|
||||
}
|
||||
|
||||
String prevScreen = "";
|
||||
for (int page = 0; page < 5; page++) {
|
||||
waitForKeyboardUnlock(client, 300);
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
String upper = screen.toUpperCase();
|
||||
|
||||
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
|
||||
break;
|
||||
}
|
||||
prevScreen = screen;
|
||||
output.append(screen).append("\n");
|
||||
|
||||
if (upper.contains("***")) {
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(100);
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal cleanup
|
||||
if (client.getPS() != null) {
|
||||
if (isIspf) {
|
||||
client.getPS().SendKeys("[enter]");
|
||||
Thread.sleep(150);
|
||||
waitForKeyboardUnlock(client, 1500);
|
||||
|
||||
int r = client.getPS().getRows();
|
||||
int c = client.getPS().getCols();
|
||||
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
|
||||
if (currentScr.toUpperCase().contains("--RECFM-") || (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
}
|
||||
} else {
|
||||
client.getPS().SendKeys("[clear]");
|
||||
waitForKeyboardUnlock(client, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
try {
|
||||
String outText = get();
|
||||
List<TSODirectoryEntry> parsed = TSODirectoryParser.parse(outText);
|
||||
if (!parsed.isEmpty()) {
|
||||
TSODirectoryEntry updated = parsed.get(0);
|
||||
if (updated.getRecfm() != null) tso.setRecfm(updated.getRecfm());
|
||||
if (updated.getLrecl() > 0) tso.setLrecl(updated.getLrecl());
|
||||
if (updated.getBlksize() > 0) tso.setBlksize(updated.getBlksize());
|
||||
if (updated.getDsorg() != null) tso.setDsorg(updated.getDsorg());
|
||||
if (updated.getVolume() != null && (tso.getVolume() == null || tso.getVolume().equals("******") || tso.getVolume().isEmpty())) {
|
||||
tso.setVolume(updated.getVolume());
|
||||
}
|
||||
|
||||
// Update table row in view
|
||||
int modelRow = table.convertRowIndexToModel(row);
|
||||
if (modelRow >= 0 && modelRow < tableModel.getRowCount()) {
|
||||
tableModel.setValueAt(tso.getVolume() != null ? tso.getVolume() : "", modelRow, 1);
|
||||
}
|
||||
|
||||
statusLabel.setText("Attributes for " + dsn + ": RECFM=" + tso.getRecfm() + ", LRECL=" + tso.getLrecl() + ", BLKSIZE=" + tso.getBlksize() + ", VOL=" + tso.getVolume());
|
||||
|
||||
StringBuilder msg = new StringBuilder();
|
||||
msg.append("Dataset: ").append(dsn).append("\n\n");
|
||||
msg.append("Volume: ").append(tso.getVolume() != null ? tso.getVolume() : "N/A").append("\n");
|
||||
msg.append("Record Format (RECFM): ").append(tso.getRecfm() != null ? tso.getRecfm() : "N/A").append("\n");
|
||||
msg.append("Logical Record Length (LRECL): ").append(tso.getLrecl() > 0 ? String.valueOf(tso.getLrecl()) : "N/A").append("\n");
|
||||
msg.append("Block Size (BLKSIZE): ").append(tso.getBlksize() > 0 ? String.valueOf(tso.getBlksize()) : "N/A").append("\n");
|
||||
msg.append("Dataset Organization (DSORG): ").append(tso.getDsorg() != null ? tso.getDsorg() : "N/A").append("\n");
|
||||
if (tso.getTracksAllocated() > 0) {
|
||||
msg.append("Allocated Tracks: ").append(tso.getTracksAllocated()).append("\n");
|
||||
}
|
||||
if (tso.getExtents() > 0) {
|
||||
msg.append("Extents: ").append(tso.getExtents()).append("\n");
|
||||
}
|
||||
JOptionPane.showMessageDialog(HostDirectoryDialog.this, msg.toString(), "Dataset Attributes (LISTDS)", JOptionPane.INFORMATION_MESSAGE);
|
||||
} else {
|
||||
statusLabel.setText("No VTOC attributes found for " + dsn);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
statusLabel.setText("Inspection failed: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
}
|
||||
|
||||
private static boolean waitForKeyboardUnlock(Telnet3270Client client, long timeoutMs) {
|
||||
if (client == null) return false;
|
||||
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
boolean locked = false;
|
||||
if (client.getInputProcessor() != null && client.getInputProcessor().isKeyboardLocked()) {
|
||||
locked = true;
|
||||
}
|
||||
if (client.getOIA() != null && (client.getOIA().isKeyLocked() || client.getOIA().isXSystem())) {
|
||||
locked = true;
|
||||
}
|
||||
if (!locked) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(40);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void parseCurrentScreen() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
if (client == null || !client.getConnectionState().isFullSession() || client.getPS() == null) {
|
||||
statusLabel.setText("No active session to parse.");
|
||||
return;
|
||||
}
|
||||
|
||||
int rows = client.getPS().getRows();
|
||||
int cols = client.getPS().getCols();
|
||||
String screenText = client.getPS().copyString(0, 0, rows - 1, cols - 1);
|
||||
parseText(screenText, "presentation space");
|
||||
}
|
||||
|
||||
private void parseClipboard() {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
Transferable contents = clipboard.getContents(null);
|
||||
if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
|
||||
String clipText = (String) contents.getTransferData(DataFlavor.stringFlavor);
|
||||
if (clipText != null && !clipText.trim().isEmpty()) {
|
||||
parseText(clipText, "clipboard");
|
||||
return;
|
||||
}
|
||||
}
|
||||
statusLabel.setText("Clipboard contains no text.");
|
||||
} catch (Exception ex) {
|
||||
statusLabel.setText("Error reading clipboard: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void parseText(String text, String source) {
|
||||
setupTableColumns();
|
||||
currentEntries.clear();
|
||||
allEntries.clear();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
String screenText = client.getPS().getString(0, client.getPS().getSize());
|
||||
|
||||
if (hostType == FTConfig.HostType.CMS) {
|
||||
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(screenText);
|
||||
currentEntries.addAll(cmsEntries);
|
||||
for (CMSDirectoryEntry entry : cmsEntries) {
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
|
||||
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
|
||||
});
|
||||
}
|
||||
List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(text);
|
||||
allEntries.addAll(cmsEntries);
|
||||
} else {
|
||||
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText);
|
||||
currentEntries.addAll(tsoEntries);
|
||||
for (TSODirectoryEntry entry : tsoEntries) {
|
||||
List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(text);
|
||||
allEntries.addAll(tsoEntries);
|
||||
}
|
||||
|
||||
applyFilter();
|
||||
if (allEntries.isEmpty()) {
|
||||
statusLabel.setText("0 entries found from " + source + ". Use 'Query Host' or 'Paste Clipboard'.");
|
||||
} else {
|
||||
statusLabel.setText("Parsed " + allEntries.size() + " entries from " + source + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFilter() {
|
||||
tableModel.setRowCount(0);
|
||||
currentEntries.clear();
|
||||
String filter = queryField.getText().trim();
|
||||
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
|
||||
|
||||
for (HostDirectoryEntry entry : allEntries) {
|
||||
if (!matchesFilter(entry, filter)) {
|
||||
continue;
|
||||
}
|
||||
currentEntries.add(entry);
|
||||
if (hostType == FTConfig.HostType.CMS && entry instanceof CMSDirectoryEntry) {
|
||||
CMSDirectoryEntry cms = (CMSDirectoryEntry) entry;
|
||||
tableModel.addRow(new Object[]{
|
||||
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
|
||||
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
|
||||
cms.getFilename() != null ? cms.getFilename() : "",
|
||||
cms.getFiletype() != null ? cms.getFiletype() : "",
|
||||
cms.getFilemode() != null ? cms.getFilemode() : "",
|
||||
cms.getRecfm() != null ? cms.getRecfm() : "",
|
||||
cms.getLrecl() > 0 ? String.valueOf(cms.getLrecl()) : "",
|
||||
cms.getNumRecords() > 0 ? String.valueOf(cms.getNumRecords()) : "",
|
||||
(cms.getDate() != null ? cms.getDate() : "") + " " + (cms.getTime() != null ? cms.getTime() : "")
|
||||
});
|
||||
} else if (entry instanceof TSODirectoryEntry) {
|
||||
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
|
||||
tableModel.addRow(new Object[]{
|
||||
tso.getDatasetName() != null ? tso.getDatasetName() : "",
|
||||
tso.getVolume() != null ? tso.getVolume() : ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space.");
|
||||
if (!allEntries.isEmpty()) {
|
||||
statusLabel.setText(currentEntries.size() + " entries shown (" + allEntries.size() + " total).");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchesFilter(HostDirectoryEntry entry, String filter) {
|
||||
if (filter == null || filter.trim().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String cleanFilter = filter.replace("'", "").replace("\"", "").trim();
|
||||
if (cleanFilter.isEmpty() || cleanFilter.equals("*") || cleanFilter.equals("* * *") || cleanFilter.equals("*.*")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry instanceof CMSDirectoryEntry) {
|
||||
return matchesCmsFilter((CMSDirectoryEntry) entry, cleanFilter);
|
||||
} else if (entry instanceof TSODirectoryEntry) {
|
||||
return matchesTsoFilter((TSODirectoryEntry) entry, cleanFilter);
|
||||
} else {
|
||||
return matchesWildcard(entry.getName(), cleanFilter);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchesCmsFilter(CMSDirectoryEntry cms, String filter) {
|
||||
String fn = cms.getFilename() != null ? cms.getFilename().trim() : "";
|
||||
String ft = cms.getFiletype() != null ? cms.getFiletype().trim() : "";
|
||||
String fm = cms.getFilemode() != null ? cms.getFilemode().trim() : "";
|
||||
|
||||
String[] tokens = filter.split("\\s+");
|
||||
if (tokens.length >= 3) {
|
||||
String fnPat = tokens[0];
|
||||
String ftPat = tokens[1];
|
||||
String fmPat = tokens[2];
|
||||
return matchesWildcard(fn, fnPat)
|
||||
&& matchesWildcard(ft, ftPat)
|
||||
&& matchesCmsFilemode(fm, fmPat);
|
||||
} else if (tokens.length == 2) {
|
||||
String fnPat = tokens[0];
|
||||
String ftPat = tokens[1];
|
||||
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
|
||||
} else {
|
||||
String tok = tokens[0];
|
||||
if (tok.contains(".")) {
|
||||
int dot = tok.indexOf('.');
|
||||
String fnPat = tok.substring(0, dot);
|
||||
String ftPat = tok.substring(dot + 1);
|
||||
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
|
||||
}
|
||||
if (matchesWildcard(fn, tok) || matchesWildcard(ft, tok) || matchesCmsFilemode(fm, tok)) {
|
||||
return true;
|
||||
}
|
||||
return matchesWildcard(cms.getName(), tok);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchesCmsFilemode(String actualFm, String fmPattern) {
|
||||
if (fmPattern == null || fmPattern.isEmpty() || fmPattern.equals("*")) {
|
||||
return true;
|
||||
}
|
||||
if (actualFm == null || actualFm.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (fmPattern.length() == 1 && Character.isLetter(fmPattern.charAt(0))) {
|
||||
return actualFm.toUpperCase().startsWith(fmPattern.toUpperCase());
|
||||
}
|
||||
return matchesWildcard(actualFm, fmPattern);
|
||||
}
|
||||
|
||||
public static boolean matchesTsoFilter(TSODirectoryEntry tso, String filter) {
|
||||
String dsn = tso.getDatasetName() != null ? tso.getDatasetName().trim() : "";
|
||||
if (filter.equals("*") || filter.equals("* * *") || filter.equals("*.*")) {
|
||||
return true;
|
||||
}
|
||||
if (filter.contains("*") || filter.contains("?")) {
|
||||
return matchesWildcard(dsn, filter);
|
||||
}
|
||||
return dsn.toUpperCase().contains(filter.toUpperCase());
|
||||
}
|
||||
|
||||
public static boolean matchesWildcard(String text, String pattern) {
|
||||
if (text == null) text = "";
|
||||
if (pattern == null || pattern.isEmpty() || pattern.equals("*")) return true;
|
||||
|
||||
StringBuilder regex = new StringBuilder("^");
|
||||
for (int i = 0; i < pattern.length(); i++) {
|
||||
char c = pattern.charAt(i);
|
||||
if (c == '*') {
|
||||
regex.append(".*");
|
||||
} else if (c == '?') {
|
||||
regex.append(".");
|
||||
} else if ("\\.[]{}()+^$|".indexOf(c) >= 0) {
|
||||
regex.append("\\").append(c);
|
||||
} else {
|
||||
regex.append(Character.toUpperCase(c));
|
||||
}
|
||||
}
|
||||
regex.append("$");
|
||||
return text.toUpperCase().matches(regex.toString());
|
||||
}
|
||||
|
||||
private void onConfirmSelection() {
|
||||
@@ -255,6 +909,7 @@ public class HostDirectoryDialog extends JDialog {
|
||||
|
||||
HostDirectoryEntry entry = currentEntries.get(row);
|
||||
selectedHostFile = entry.getName();
|
||||
selectedEntry = entry;
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
@@ -266,4 +921,50 @@ public class HostDirectoryDialog extends JDialog {
|
||||
public String getSelectedHostFile() {
|
||||
return selectedHostFile;
|
||||
}
|
||||
|
||||
public HostDirectoryEntry getSelectedEntry() {
|
||||
return selectedEntry;
|
||||
}
|
||||
|
||||
public List<HostDirectoryEntry> getAllEntries() {
|
||||
return allEntries;
|
||||
}
|
||||
|
||||
public List<HostDirectoryEntry> getCurrentEntries() {
|
||||
return currentEntries;
|
||||
}
|
||||
|
||||
public JTextField getQueryField() {
|
||||
return queryField;
|
||||
}
|
||||
|
||||
public static String resolveDefaultQuery(FTConfig.HostType hostType, String initialQuery) {
|
||||
String defaultQuery = initialQuery != null ? initialQuery.trim() : "";
|
||||
if (hostType == FTConfig.HostType.CMS && (defaultQuery.isEmpty() || defaultQuery.equals("* * *") || defaultQuery.equals("*"))) {
|
||||
defaultQuery = "* * A";
|
||||
}
|
||||
return defaultQuery;
|
||||
}
|
||||
|
||||
public static String formatCMSFilter(String pattern) {
|
||||
if (pattern == null || pattern.trim().isEmpty()) {
|
||||
return "* * A";
|
||||
}
|
||||
String clean = pattern.replace("'", "").replace("\"", "").trim();
|
||||
if (clean.equals("* * *") || clean.equals("*")) {
|
||||
return "* * A";
|
||||
} else if (clean.contains(".")) {
|
||||
int dot = clean.indexOf('.');
|
||||
return clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A";
|
||||
} else {
|
||||
String[] parts = clean.split("\\s+");
|
||||
if (parts.length == 1) {
|
||||
return parts[0] + " * A";
|
||||
} else if (parts.length == 2) {
|
||||
return parts[0] + " " + parts[1] + " A";
|
||||
} else {
|
||||
return parts[0] + " " + parts[1] + " " + parts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* 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
|
||||
* @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 {
|
||||
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;
|
||||
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
|
||||
int commentIdx = line.indexOf(';');
|
||||
@@ -262,6 +290,22 @@ public class KeyBindings {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export key bindings to a .kmp file using IBM HoD format.
|
||||
*/
|
||||
public static void exportKmp(File file, Map<String, String> keyBindings) throws IOException {
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||
exportKmp(bw, keyBindings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export key bindings to a Writer using IBM HoD format.
|
||||
*/
|
||||
public static void exportKmp(Writer writer, Map<String, String> keyBindings) throws IOException {
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.exportHodKmp(writer, keyBindings);
|
||||
}
|
||||
|
||||
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
|
||||
String[] parts = stmt.split("\\s+", 2);
|
||||
if (parts.length < 2) return;
|
||||
@@ -288,6 +332,19 @@ public class KeyBindings {
|
||||
}
|
||||
|
||||
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
|
||||
if (left.matches("^[A-Za-z][0-9]+$")) {
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.KeyEntry entry =
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.parseHashKey(left, right);
|
||||
if (entry != null) {
|
||||
String stroke = entry.toKeyStrokeString();
|
||||
String act = haus.nightmare.lib3270j.input.HodKeyMap.resolveMnemonicToAction(right);
|
||||
if (act != null && stroke != null && !stroke.isEmpty()) {
|
||||
addBinding(actionToBindings, act, stroke);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
|
||||
String action = resolveMnemonic(right);
|
||||
String keyStroke = null;
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.MacroScreen;
|
||||
import haus.nightmare.lib3270j.macro.MacroScreens;
|
||||
import haus.nightmare.lib3270j.macro.MacroXmlParser;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroAction;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Interactive Swing dialog for loading, editing, running, and debugging
|
||||
* IBM Host On-Demand XML Macros (<HASCRIPT> / .mac files).
|
||||
*/
|
||||
public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private JTextArea xmlArea;
|
||||
private JTextArea logArea;
|
||||
private JLabel statusLabel;
|
||||
private JButton playBtn;
|
||||
private JButton pauseBtn;
|
||||
private JButton stopBtn;
|
||||
|
||||
private Macro macro;
|
||||
|
||||
public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "IBM HoD XML Macro Runtime (<HASCRIPT>)", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
this.macro = new Macro(client);
|
||||
this.macro.addMacroListener(this);
|
||||
this.macro.setPromptHandler(this);
|
||||
|
||||
buildUI();
|
||||
setSize(780, 580);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Top info label
|
||||
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
|
||||
topPanel.setOpaque(false);
|
||||
JLabel descLabel = new JLabel("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code><HASCRIPT></code>).<br>"
|
||||
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
|
||||
descLabel.setFont(labelFont);
|
||||
topPanel.add(descLabel, BorderLayout.CENTER);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Center split: XML editor and Execution Log
|
||||
xmlArea = new JTextArea();
|
||||
xmlArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
xmlArea.setLineWrap(false);
|
||||
xmlArea.setText(getDefaultMacroXml());
|
||||
ThemeManager.styleTextArea(xmlArea);
|
||||
|
||||
JScrollPane xmlScroll = new JScrollPane(xmlArea);
|
||||
ThemeManager.styleScrollPane(xmlScroll);
|
||||
xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML (<HASCRIPT>)"));
|
||||
|
||||
logArea = new JTextArea();
|
||||
logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1));
|
||||
logArea.setEditable(false);
|
||||
logArea.setLineWrap(true);
|
||||
ThemeManager.styleTextArea(logArea);
|
||||
|
||||
JScrollPane logScroll = new JScrollPane(logArea);
|
||||
ThemeManager.styleScrollPane(logScroll);
|
||||
logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log"));
|
||||
|
||||
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll);
|
||||
splitPane.setResizeWeight(0.65);
|
||||
splitPane.setOpaque(false);
|
||||
mainPanel.add(splitPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom control panel
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Status: Stopped");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(labelFont);
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton loadBtn = new JButton("Load .mac...");
|
||||
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
loadBtn.setFont(labelFont);
|
||||
loadBtn.addActionListener(e -> loadMacroFile());
|
||||
|
||||
JButton saveBtn = new JButton("Save .mac...");
|
||||
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveBtn.setFont(labelFont);
|
||||
saveBtn.addActionListener(e -> saveMacroFile());
|
||||
|
||||
playBtn = new JButton("Play");
|
||||
ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
playBtn.setFont(labelFont);
|
||||
playBtn.addActionListener(e -> playMacro());
|
||||
|
||||
pauseBtn = new JButton("Pause");
|
||||
ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pauseBtn.setFont(labelFont);
|
||||
pauseBtn.setEnabled(false);
|
||||
pauseBtn.addActionListener(e -> togglePause());
|
||||
|
||||
stopBtn = new JButton("Stop");
|
||||
ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
stopBtn.setFont(labelFont);
|
||||
stopBtn.setEnabled(false);
|
||||
stopBtn.addActionListener(e -> stopMacro());
|
||||
|
||||
JButton clearLogBtn = new JButton("Clear Log");
|
||||
ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
clearLogBtn.setFont(labelFont);
|
||||
clearLogBtn.addActionListener(e -> logArea.setText(""));
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(loadBtn);
|
||||
buttonPanel.add(saveBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(8));
|
||||
buttonPanel.add(playBtn);
|
||||
buttonPanel.add(pauseBtn);
|
||||
buttonPanel.add(stopBtn);
|
||||
buttonPanel.add(clearLogBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(8));
|
||||
buttonPanel.add(closeBtn);
|
||||
|
||||
bottomPanel.add(buttonPanel, BorderLayout.EAST);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private void loadMacroFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Open IBM HoD Macro");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
|
||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
xmlArea.setText(sb.toString());
|
||||
log("Loaded macro file: " + f.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveMacroFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Save IBM HoD Macro");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
|
||||
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
if (!f.getName().contains(".")) {
|
||||
f = new File(f.getParentFile(), f.getName() + ".mac");
|
||||
}
|
||||
try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) {
|
||||
w.write(xmlArea.getText());
|
||||
log("Saved macro file: " + f.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void playMacro() {
|
||||
String xml = xmlArea.getText();
|
||||
if (xml == null || xml.trim().isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
int opt = JOptionPane.showConfirmDialog(this,
|
||||
"Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?",
|
||||
"Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
if (opt != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
MacroScreens screens = MacroXmlParser.parse(xml);
|
||||
macro.setScreens(screens);
|
||||
macro.setClient(client);
|
||||
|
||||
log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName()));
|
||||
macro.play();
|
||||
} catch (Exception ex) {
|
||||
log("Parse / Launch Error: " + ex.getMessage());
|
||||
JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
public void togglePause() {
|
||||
if (macro.getState() == Macro.STATE_PLAYING) {
|
||||
macro.pause();
|
||||
pauseBtn.setText("Resume");
|
||||
log("Macro paused.");
|
||||
} else if (macro.getState() == Macro.STATE_PAUSED) {
|
||||
macro.resume();
|
||||
pauseBtn.setText("Pause");
|
||||
log("Macro resumed.");
|
||||
}
|
||||
}
|
||||
|
||||
public void stopMacro() {
|
||||
macro.stop();
|
||||
log("Macro stopped by user.");
|
||||
}
|
||||
|
||||
private void log(String msg) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
logArea.append(msg + "\n");
|
||||
logArea.setCaretPosition(logArea.getDocument().getLength());
|
||||
});
|
||||
}
|
||||
|
||||
// ========== MacroListener Callbacks ==========
|
||||
|
||||
@Override
|
||||
public void macroStateChanged(Macro macro, int oldState, int newState) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
switch (newState) {
|
||||
case Macro.STATE_PLAYING:
|
||||
statusLabel.setText("Status: Playing");
|
||||
statusLabel.setForeground(ThemeManager.getFgMain());
|
||||
playBtn.setEnabled(false);
|
||||
pauseBtn.setEnabled(true);
|
||||
pauseBtn.setText("Pause");
|
||||
stopBtn.setEnabled(true);
|
||||
break;
|
||||
case Macro.STATE_PAUSED:
|
||||
statusLabel.setText("Status: Paused");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
pauseBtn.setText("Resume");
|
||||
break;
|
||||
case Macro.STATE_ERROR:
|
||||
statusLabel.setText("Status: Error");
|
||||
statusLabel.setForeground(Color.RED);
|
||||
playBtn.setEnabled(true);
|
||||
pauseBtn.setEnabled(false);
|
||||
stopBtn.setEnabled(false);
|
||||
break;
|
||||
case Macro.STATE_STOPPED:
|
||||
default:
|
||||
statusLabel.setText("Status: Stopped");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
playBtn.setEnabled(true);
|
||||
pauseBtn.setEnabled(false);
|
||||
pauseBtn.setText("Pause");
|
||||
stopBtn.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroScreenMatching(MacroScreen screen) {
|
||||
log("Matched screen: [" + screen.getName() + "]");
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroActionExecuting(MacroAction action) {
|
||||
log(" -> Executing: " + action.getClass().getSimpleName());
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroMessage(String title, String message) {
|
||||
log("Message [" + title + "]: " + message);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroError(String error) {
|
||||
log("ERROR: " + error);
|
||||
}
|
||||
|
||||
// ========== MacroPromptHandler Callback ==========
|
||||
|
||||
@Override
|
||||
public String prompt(MacroActionPrompt prompt) {
|
||||
AtomicReference<String> result = new AtomicReference<>(null);
|
||||
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
result.set(showPromptModal(prompt));
|
||||
} else {
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
result.set(showPromptModal(prompt));
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result.get();
|
||||
}
|
||||
|
||||
private String showPromptModal(MacroActionPrompt prompt) {
|
||||
JPanel panel = new JPanel(new BorderLayout(5, 5));
|
||||
JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription());
|
||||
panel.add(msgLabel, BorderLayout.NORTH);
|
||||
|
||||
JTextField textField;
|
||||
if (prompt.isEncrypted()) {
|
||||
textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
|
||||
} else {
|
||||
textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
|
||||
}
|
||||
panel.add(textField, BorderLayout.CENTER);
|
||||
|
||||
int opt = JOptionPane.showConfirmDialog(this, panel,
|
||||
prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(),
|
||||
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||
|
||||
if (opt == JOptionPane.OK_OPTION) {
|
||||
if (textField instanceof JPasswordField) {
|
||||
return new String(((JPasswordField) textField).getPassword());
|
||||
}
|
||||
return textField.getText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getDefaultMacroXml() {
|
||||
return "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
|
||||
+ " <vars>\n"
|
||||
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
|
||||
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
|
||||
+ " </vars>\n\n"
|
||||
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
|
||||
+ " <description>\n"
|
||||
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||
+ " </description>\n"
|
||||
+ " <actions>\n"
|
||||
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
|
||||
+ " </actions>\n"
|
||||
+ " <nextscreens timeout=\"10000\">\n"
|
||||
+ " <nextscreen name=\"ReadyScreen\" />\n"
|
||||
+ " </nextscreens>\n"
|
||||
+ " </screen>\n\n"
|
||||
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
|
||||
+ " <description>\n"
|
||||
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||
+ " </description>\n"
|
||||
+ " <actions>\n"
|
||||
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
|
||||
+ " </actions>\n"
|
||||
+ " </screen>\n"
|
||||
+ "</HASCRIPT>";
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) {
|
||||
super(parent, "IBM 3287 Printer Session Manager", false);
|
||||
this.parent = parent;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
config = new PrinterConfig(defaultHost, defaultPort);
|
||||
config.setUseTls(defaultTls);
|
||||
@@ -71,7 +72,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
gbc.insets = new Insets(3, 4, 3, 4);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Host / Port
|
||||
gbc.gridx = 0; gbc.gridy = 0;
|
||||
@@ -205,7 +206,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
|
||||
// Spool text area
|
||||
spoolArea = new JTextArea();
|
||||
spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
spoolArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
spoolArea.setEditable(false);
|
||||
ThemeManager.styleTextArea(spoolArea);
|
||||
|
||||
@@ -256,7 +257,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene
|
||||
|
||||
private JTextField createField(String text, int cols) {
|
||||
JTextField tf = new JTextField(text, cols);
|
||||
tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
tf.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextField(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);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
buildUI();
|
||||
setSize(600, 480);
|
||||
@@ -35,7 +36,7 @@ public class ScriptDialog extends JDialog {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Header
|
||||
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
|
||||
@@ -48,7 +49,7 @@ public class ScriptDialog extends JDialog {
|
||||
|
||||
// Script area
|
||||
scriptArea = new JTextArea();
|
||||
scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
|
||||
scriptArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
scriptArea.setLineWrap(true);
|
||||
scriptArea.setWrapStyleWord(false);
|
||||
ThemeManager.styleTextArea(scriptArea);
|
||||
@@ -74,7 +75,7 @@ public class ScriptDialog extends JDialog {
|
||||
|
||||
for (String token : tokens) {
|
||||
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);
|
||||
btn.setFocusable(false);
|
||||
btn.setMargin(new Insets(2, 4, 2, 4));
|
||||
@@ -94,7 +95,7 @@ public class ScriptDialog extends JDialog {
|
||||
|
||||
statusLabel = new JLabel("Ready");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
statusLabel.setFont(labelFont);
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
|
||||
@@ -29,14 +29,15 @@ public class StatusBar extends JPanel {
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener;
|
||||
|
||||
public StatusBar() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
setBackground(ThemeManager.getStatusBarBg());
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder()));
|
||||
setPreferredSize(new Dimension(800, 22));
|
||||
|
||||
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
Font oiaFont = ThemeManager.getMonospacedUiFont();
|
||||
updateBarHeight(oiaFont);
|
||||
|
||||
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
|
||||
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
|
||||
@@ -131,8 +132,15 @@ public class StatusBar extends JPanel {
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
if (this.client != null && this.client.getOIA() != null && this.oiaNotifyListener != null) {
|
||||
this.client.getOIA().unregisterOIAEvent(this.oiaNotifyListener);
|
||||
}
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
if (this.client != null && this.client.getOIA() != null) {
|
||||
this.oiaNotifyListener = event -> updateStatus();
|
||||
this.client.getOIA().registerOIAEvent(this.oiaNotifyListener);
|
||||
}
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
@@ -143,9 +151,36 @@ public class StatusBar extends JPanel {
|
||||
}
|
||||
setBackground(ThemeManager.getStatusBarBg(theme));
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
|
||||
Font oiaFont = ThemeManager.getMonospacedUiFont();
|
||||
updateBarHeight(oiaFont);
|
||||
applyFontRecursively(this, oiaFont);
|
||||
revalidate();
|
||||
repaint();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
private void updateBarHeight(Font font) {
|
||||
int fontHeight = 16;
|
||||
try {
|
||||
FontMetrics fm = getFontMetrics(font);
|
||||
if (fm != null) fontHeight = fm.getHeight();
|
||||
} catch (Exception ignored) {
|
||||
fontHeight = font.getSize() + 4;
|
||||
}
|
||||
int barHeight = Math.max(22, fontHeight + 6);
|
||||
setPreferredSize(new Dimension(800, barHeight));
|
||||
}
|
||||
|
||||
private void applyFontRecursively(Component comp, Font font) {
|
||||
if (comp instanceof JLabel) {
|
||||
comp.setFont(font);
|
||||
} else if (comp instanceof Container) {
|
||||
for (Component child : ((Container) comp).getComponents()) {
|
||||
applyFontRecursively(child, font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(this::updateStatus);
|
||||
@@ -322,9 +357,12 @@ public class StatusBar extends JPanel {
|
||||
wordWrapStatus.setText("");
|
||||
}
|
||||
|
||||
// Field status: Numeric vs Alphanumeric
|
||||
// Field status: Numeric vs Alphanumeric vs DBCS
|
||||
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
|
||||
if (client.getOIA().isNumeric()) {
|
||||
if (client.getOIA().isDBCS()) {
|
||||
fieldTypeStatus.setText("DBCS");
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
|
||||
} else if (client.getOIA().isNumeric()) {
|
||||
fieldTypeStatus.setText("NUM");
|
||||
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
|
||||
} else {
|
||||
@@ -361,4 +399,8 @@ public class StatusBar extends JPanel {
|
||||
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
|
||||
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
}
|
||||
|
||||
public String getLockStatusText() {
|
||||
return lockStatus != null ? lockStatus.getText() : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.awt.event.MouseEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
|
||||
/**
|
||||
* Centralized theme manager for j3270 Java desktop UI.
|
||||
@@ -33,6 +34,25 @@ public final class 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() {
|
||||
return currentTheme;
|
||||
}
|
||||
@@ -433,6 +453,47 @@ public final class ThemeManager {
|
||||
UIManager.put("TitledBorder.titleColor", fgMain);
|
||||
UIManager.put("OptionPane.background", bgPanel);
|
||||
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) {
|
||||
if (button == null) return null;
|
||||
button.setUI(new StyledButtonUI(variant));
|
||||
button.setFont(getUiFont());
|
||||
button.setFocusPainted(false);
|
||||
button.setOpaque(false);
|
||||
button.setContentAreaFilled(false);
|
||||
@@ -465,6 +527,7 @@ public final class ThemeManager {
|
||||
|
||||
public static JTextField styleTextField(JTextField field) {
|
||||
if (field == null) return null;
|
||||
field.setFont(getUiFont());
|
||||
field.setBackground(getBgComponent());
|
||||
field.setForeground(getFgMain());
|
||||
field.setCaretColor(getFgMain());
|
||||
@@ -478,6 +541,7 @@ public final class ThemeManager {
|
||||
|
||||
public static JTextArea styleTextArea(JTextArea area) {
|
||||
if (area == null) return null;
|
||||
area.setFont(getMonospacedUiFont());
|
||||
area.setBackground(getCodeAreaBg());
|
||||
area.setForeground(getCodeAreaFg());
|
||||
area.setCaretColor(getFgMain());
|
||||
@@ -488,6 +552,7 @@ public final class ThemeManager {
|
||||
|
||||
public static JComboBox<?> styleComboBox(JComboBox<?> box) {
|
||||
if (box == null) return null;
|
||||
box.setFont(getUiFont());
|
||||
box.setBackground(getBgComponent());
|
||||
box.setForeground(getFgMain());
|
||||
box.setRenderer(new DefaultListCellRenderer() {
|
||||
@@ -510,12 +575,14 @@ public final class ThemeManager {
|
||||
|
||||
public static JTable styleTable(JTable table) {
|
||||
if (table == null) return null;
|
||||
table.setFont(getUiFont());
|
||||
table.setBackground(getBgComponent());
|
||||
table.setForeground(getFgMain());
|
||||
table.setSelectionBackground(getSelectionBg());
|
||||
table.setSelectionForeground(getSelectionFg());
|
||||
table.setGridColor(getTableGrid());
|
||||
table.setRowHeight(24);
|
||||
int rowHeight = Math.max(24, getUiFont().getSize() + 10);
|
||||
table.setRowHeight(rowHeight);
|
||||
|
||||
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
@@ -537,14 +604,14 @@ public final class ThemeManager {
|
||||
if (header != null) {
|
||||
header.setBackground(getTableHeaderBg());
|
||||
header.setForeground(getTableHeaderFg());
|
||||
header.setFont(header.getFont().deriveFont(Font.BOLD));
|
||||
header.setFont(getUiFont(Font.BOLD, 0));
|
||||
header.setDefaultRenderer(new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) {
|
||||
super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col);
|
||||
setBackground(getTableHeaderBg());
|
||||
setForeground(getTableHeaderFg());
|
||||
setFont(getFont().deriveFont(Font.BOLD));
|
||||
setFont(getUiFont(Font.BOLD, 0));
|
||||
setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()),
|
||||
new EmptyBorder(4, 6, 4, 6)));
|
||||
@@ -558,6 +625,7 @@ public final class ThemeManager {
|
||||
public static JTabbedPane styleTabbedPane(JTabbedPane tp) {
|
||||
if (tp == null) return null;
|
||||
tp.setUI(new StyledTabbedPaneUI());
|
||||
tp.setFont(getUiFont());
|
||||
tp.setBackground(getBgMain());
|
||||
tp.setForeground(getFgMain());
|
||||
return tp;
|
||||
@@ -566,6 +634,7 @@ public final class ThemeManager {
|
||||
public static JMenuBar styleMenuBar(JMenuBar bar) {
|
||||
if (bar == null) return null;
|
||||
bar.setUI(new StyledMenuBarUI());
|
||||
bar.setFont(getUiFont());
|
||||
bar.setBackground(getMenuBarBg());
|
||||
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder()));
|
||||
return bar;
|
||||
@@ -574,6 +643,7 @@ public final class ThemeManager {
|
||||
public static JMenu styleMenu(JMenu menu) {
|
||||
if (menu == null) return null;
|
||||
menu.setUI(new StyledMenuUI());
|
||||
menu.setFont(getUiFont());
|
||||
menu.setForeground(getMenuBarFg());
|
||||
menu.setBackground(getMenuBarBg());
|
||||
menu.setOpaque(false);
|
||||
@@ -583,6 +653,7 @@ public final class ThemeManager {
|
||||
public static JMenuItem styleMenuItem(JMenuItem item) {
|
||||
if (item == null) return null;
|
||||
item.setUI(new StyledMenuItemUI());
|
||||
item.setFont(getUiFont());
|
||||
item.setBackground(getMenuPopupBg());
|
||||
item.setForeground(getMenuItemFg());
|
||||
return item;
|
||||
@@ -591,6 +662,7 @@ public final class ThemeManager {
|
||||
public static JPopupMenu stylePopupMenu(JPopupMenu popup) {
|
||||
if (popup == null) return null;
|
||||
popup.setUI(new StyledPopupMenuUI());
|
||||
popup.setFont(getUiFont());
|
||||
popup.setBackground(getMenuPopupBg());
|
||||
popup.setBorder(BorderFactory.createCompoundBorder(
|
||||
new LineBorder(getBorder(), 1),
|
||||
@@ -610,12 +682,14 @@ public final class ThemeManager {
|
||||
|
||||
public static JSpinner styleSpinner(JSpinner sp) {
|
||||
if (sp == null) return null;
|
||||
sp.setFont(getUiFont());
|
||||
sp.setBackground(getBgComponent());
|
||||
sp.setForeground(getFgMain());
|
||||
sp.setBorder(new LineBorder(getBorder(), 1));
|
||||
JComponent editor = sp.getEditor();
|
||||
if (editor instanceof JSpinner.DefaultEditor) {
|
||||
JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
|
||||
tf.setFont(getUiFont());
|
||||
tf.setBackground(getBgComponent());
|
||||
tf.setForeground(getFgMain());
|
||||
tf.setCaretColor(getFgMain());
|
||||
@@ -626,6 +700,7 @@ public final class ThemeManager {
|
||||
|
||||
public static JCheckBox styleCheckBox(JCheckBox cb) {
|
||||
if (cb == null) return null;
|
||||
cb.setFont(getUiFont());
|
||||
cb.setOpaque(false);
|
||||
cb.setForeground(getFgMain());
|
||||
cb.setFocusPainted(false);
|
||||
@@ -634,6 +709,7 @@ public final class ThemeManager {
|
||||
|
||||
public static JRadioButton styleRadioButton(JRadioButton rb) {
|
||||
if (rb == null) return null;
|
||||
rb.setFont(getUiFont());
|
||||
rb.setOpaque(false);
|
||||
rb.setForeground(getFgMain());
|
||||
rb.setFocusPainted(false);
|
||||
@@ -646,7 +722,7 @@ public final class ThemeManager {
|
||||
title,
|
||||
TitledBorder.DEFAULT_JUSTIFICATION,
|
||||
TitledBorder.DEFAULT_POSITION,
|
||||
new Font(Font.SANS_SERIF, Font.BOLD, 12),
|
||||
getUiFont(Font.BOLD, 0),
|
||||
getFgMain());
|
||||
}
|
||||
|
||||
@@ -732,6 +808,7 @@ public final class ThemeManager {
|
||||
|
||||
if (comp instanceof JButton) {
|
||||
JButton btn = (JButton) comp;
|
||||
btn.setFont(getUiFont());
|
||||
if (!(btn.getUI() instanceof StyledButtonUI)) {
|
||||
styleButton(btn, ButtonVariant.DEFAULT);
|
||||
}
|
||||
@@ -770,6 +847,21 @@ public final class ThemeManager {
|
||||
|
||||
if (comp instanceof JLabel) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -783,6 +875,7 @@ public final class ThemeManager {
|
||||
if (b instanceof TitledBorder) {
|
||||
TitledBorder tb = (TitledBorder) b;
|
||||
tb.setTitleColor(getFgMain(theme));
|
||||
tb.setTitleFont(getUiFont(Font.BOLD, 0));
|
||||
tb.setBorder(new LineBorder(getBorder(theme), 1));
|
||||
}
|
||||
} 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) {
|
||||
super(parent, "Untrusted SSL/TLS Certificate", true);
|
||||
AppIcon.applyTo(this);
|
||||
buildUI(host, port, chain, exception);
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
@@ -31,18 +32,20 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
|
||||
headerPanel.setOpaque(false);
|
||||
JLabel iconLabel = new JLabel("⚠️");
|
||||
iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28));
|
||||
iconLabel.putClientProperty("uiFontSizeDelta", 15);
|
||||
iconLabel.setFont(ThemeManager.getUiFont(Font.PLAIN, 15));
|
||||
headerPanel.add(iconLabel);
|
||||
|
||||
JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2));
|
||||
titleBox.setOpaque(false);
|
||||
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());
|
||||
titleBox.add(titleLabel);
|
||||
|
||||
JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified.");
|
||||
subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
subtitleLabel.setFont(ThemeManager.getUiFont());
|
||||
subtitleLabel.setForeground(ThemeManager.getFgMuted());
|
||||
titleBox.add(subtitleLabel);
|
||||
headerPanel.add(titleBox);
|
||||
@@ -69,7 +72,7 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
|
||||
JTextArea detailsArea = new JTextArea(sb.toString());
|
||||
detailsArea.setEditable(false);
|
||||
detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
detailsArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
ThemeManager.styleTextArea(detailsArea);
|
||||
detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
|
||||
@@ -91,7 +94,7 @@ public class UntrustedCertificateDialog extends JDialog {
|
||||
|
||||
JButton trustBtn = new JButton("Connect Anyway");
|
||||
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 -> {
|
||||
accepted = true;
|
||||
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 |
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="144 814 280 169"
|
||||
width="280"
|
||||
height="169"
|
||||
xml:space="preserve"
|
||||
id="svg_lib3270j_cropped"
|
||||
version="1.1">
|
||||
<metadata id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<!-- Pure black background (#000000) for classic 3270 CRT terminal look -->
|
||||
<rect x="144" y="814" width="280" height="169" fill="#000000" />
|
||||
<g transform="matrix(1.3333333,0,0,-1.3333333,0,1122.6667)" id="g10">
|
||||
<g transform="scale(0.1)" id="g12">
|
||||
<g id="g14">
|
||||
<!-- Green Terminal Screen Frame -->
|
||||
<path
|
||||
id="path28"
|
||||
style="fill:none;stroke:#00ff00;stroke-width:3.75079012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
|
||||
<!-- Blue Wave / Exponential Curve -->
|
||||
<path
|
||||
id="path30"
|
||||
style="fill:none;stroke:#0000ff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1172.41,1137.58 79.1,-1.22 -0.21,-0.38 c 1.08,0 2.18,0 3.27,0 762.95,0 1464.86,417.88 1829.53,1089.21" />
|
||||
<!-- Red Circular Arc -->
|
||||
<path
|
||||
id="path32"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2593.86,1620.73 -1.22,-28 -0.08,-0.08 c 0.43,-250.15 202.81,-452.72 452.3,-452.72 0.14,0 0.28,0 0.42,0 l 43.9,2.52" />
|
||||
<!-- Yellow Smiley Head -->
|
||||
<path
|
||||
id="path34"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2976,1363.94 v 0.08 c 44.73,34.29 71.21,87.25 71.8,143.61 l 0.01,-0.15 c -0.06,97.48 -79.11,176.47 -176.59,176.47 -0.36,0 -0.73,0 -1.09,0 l -0.07,0.03 c -0.25,0 -0.51,0 -0.75,0 -37.38,0 -73.79,-11.94 -103.92,-34.08 l 0.02,-0.03 c -44.38,-34.04 -70.82,-86.47 -71.8,-142.39 l 0.06,0.04 c 0.54,-97.43 79.04,-176.47 176.46,-177.69 l 0.08,0.02 c 0.64,-0.01 1.29,-0.01 1.92,-0.01 37.4,0 73.82,11.94 103.96,34.08" />
|
||||
<!-- 'l' -->
|
||||
<path
|
||||
id="path36"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<path
|
||||
id="path38"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1502.21,1785.02 h 23.13 v 32.86 h -23.13 l -4.87,8.52 v 154.56 h -18.25 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52" />
|
||||
<!-- 'i' -->
|
||||
<path
|
||||
id="path40"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<path
|
||||
id="path42"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1566.71,1785.02 h 13.39 v 32.86 h -13.39 l -4.86,8.52 v 105.88 h -18.26 V 1826.4 l 4.87,-25.56 4.87,-7.3 13.38,-8.52 m -4.86,163.08 v 32.86 h -18.26 v -32.86 h 18.26" />
|
||||
<!-- 'b' -->
|
||||
<path
|
||||
id="path44"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<path
|
||||
id="path46"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1598.36,1785.02 v 195.94 h 18.25 v -48.68 h 41.38 l 13.39,-8.52 4.87,-8.52 4.86,-24.34 v -64.5 l -4.86,-25.56 -4.87,-7.3 -13.39,-8.52 h -59.63 m 18.25,32.86 h 41.38 l 4.87,8.52 v 64.5 l -4.87,8.52 h -41.38 v -81.54" />
|
||||
<!-- '3' -->
|
||||
<path
|
||||
id="path48"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<path
|
||||
id="path50"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1722.49,1817.88 h -23.12 v -32.86 h 69.36 l 13.4,8.52 4.86,7.3 3.65,25.56 v 40.16 l -8.51,15.82 8.51,17.04 v 40.16 l -3.65,24.34 -4.86,8.52 -13.4,8.52 h -69.36 v -32.86 h 69.36 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24 v -32.86 h 46.24 l 3.66,-8.52 v -31.64 l -3.66,-8.52 h -46.24" />
|
||||
<!-- '2' -->
|
||||
<path
|
||||
id="path52"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<path
|
||||
id="path54"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1808.9,1785.02 h 92.49 v 32.86 h -73.02 v 32.86 l 3.65,7.3 h 46.25 l 13.38,8.52 4.87,8.52 4.87,24.34 v 40.16 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -46.25 l -13.39,-8.52 -4.86,-8.52 -4.87,-24.34 v -24.34 h 19.47 v 24.34 l 3.65,8.52 h 46.25 l 4.86,-8.52 v -40.16 l -4.86,-8.52 h -46.25 l -13.39,-8.52 -4.86,-7.3 -4.87,-24.34 v -65.72" />
|
||||
<!-- '7' -->
|
||||
<path
|
||||
id="path56"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<path
|
||||
id="path58"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 1919.64,1980.96 h 91.28 v -65.72 l -49.9,-130.22 h -23.12 l 54.76,138.74 v 24.34 h -73.02 v 32.86" />
|
||||
<!-- '0' -->
|
||||
<path
|
||||
id="path60"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<path
|
||||
id="path62"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2047.43,1972.44 -4.87,-8.52 -3.65,-24.34 V 1826.4 l 3.65,-25.56 4.87,-7.3 14.61,-8.52 h 31.64 l 13.38,8.52 4.87,7.3 4.87,25.56 v 113.18 l -4.87,24.34 -4.87,8.52 -13.38,8.52 h -31.64 l -14.61,-8.52 m 14.61,-24.34 h 31.64 l 4.86,-8.52 V 1826.4 l -4.86,-8.52 h -31.64 l -4.88,8.52 v 113.18 l 4.88,8.52" />
|
||||
<!-- 'j' -->
|
||||
<path
|
||||
id="path64"
|
||||
style="fill:#ff00ff;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<path
|
||||
id="path66"
|
||||
style="fill:none;stroke:#ff00ff;stroke-width:1.21700001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2176.43,1948.1 v 32.86 h -18.25 v -32.86 h 18.25 m 0,-15.82 V 1769.2 l -4.87,-25.56 -4.86,-7.3 -13.39,-8.52 h -18.26 v 32.86 h 18.26 l 4.87,8.52 v 163.08 h 18.25" />
|
||||
<!-- Smile -->
|
||||
<path
|
||||
id="path68"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2778.85,1478.34 0.04,-0.02 c 21.03,-31.91 56.69,-51.12 94.91,-51.12 0.41,0 0.83,0 1.24,0.01 l -0.02,0.07 c 43.88,0.19 84.11,24.51 104.66,63.28" />
|
||||
<!-- Left Eye (cyan) -->
|
||||
<path
|
||||
id="path70"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2820.22,1595.17 0.01,0.01 c -14.62,-1.91 -25.56,-14.37 -25.56,-29.11 0,-0.03 0,-0.07 0,-0.1 l -0.02,-0.01 c 0,-0.03 0,-0.07 0,-0.1 0,-16.09 13.04,-29.14 29.13,-29.14 0.43,0 0.87,0.01 1.29,0.03 h 3.67 0.01 c 14.61,1.91 25.55,14.37 25.55,29.11 0,0.03 0,0.07 0,0.1 l 0.02,0.01 c 0,0.04 0,0.08 0,0.12 0,16.07 -13.03,29.09 -29.09,29.09 -0.03,0 -0.08,0 -0.12,0 l -4.89,-0.01" />
|
||||
<!-- Right Eye (cyan) -->
|
||||
<path
|
||||
id="path72"
|
||||
style="fill:none;stroke:#00ffff;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2921.23,1569.61 v -3.65 l -0.01,-0.01 c 0,-0.27 -0.01,-0.54 -0.01,-0.81 0,-18.22 14.66,-33.05 32.87,-33.26 l 0.03,0.01 c 0.28,0 0.57,-0.01 0.84,-0.01 17.29,0 31.72,13.22 33.24,30.44 l -0.02,3.64 0.02,0.02 c 0,0.27 0.01,0.54 0.01,0.81 0,18.37 -14.9,33.28 -33.28,33.28 -0.26,0 -0.54,-0.01 -0.81,-0.02 l -0.02,-0.03 c -17.14,-0.18 -31.36,-13.34 -32.86,-30.42" />
|
||||
<!-- Nose -->
|
||||
<path
|
||||
id="path74"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2892.03,1528.23 -29.21,-20.68" />
|
||||
<path
|
||||
id="path76"
|
||||
style="fill:none;stroke:#ffff00;stroke-width:7.50158978;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 2862.82,1507.55 h 32.86" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 311 B |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 452 B |
|
After Width: | Height: | Size: 714 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,57 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.AppIcon;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class AppIconTest {
|
||||
|
||||
@Test
|
||||
public void testIconsAreLoaded() {
|
||||
List<Image> icons = AppIcon.getAppIcons();
|
||||
assertNotNull(icons, "Icons list must not be null");
|
||||
assertFalse(icons.isEmpty(), "At least one icon image must be loaded");
|
||||
|
||||
// Verify standard sizes are present
|
||||
int[] expectedSizes = {16, 24, 32, 48, 64, 128, 256, 512};
|
||||
for (int expectedSize : expectedSizes) {
|
||||
boolean found = icons.stream().anyMatch(img -> img.getWidth(null) == expectedSize && img.getHeight(null) == expectedSize);
|
||||
assertTrue(found, "Expected icon of size " + expectedSize + "x" + expectedSize + " was not found");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultIcon() {
|
||||
Image defaultIcon = AppIcon.getDefaultIcon();
|
||||
assertNotNull(defaultIcon, "Default icon must not be null");
|
||||
assertEquals(256, defaultIcon.getWidth(null), "Default icon width should be 256");
|
||||
assertEquals(256, defaultIcon.getHeight(null), "Default icon height should be 256");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplyToInHeadless() {
|
||||
// Must execute without exception even in headless environment
|
||||
assertDoesNotThrow(() -> AppIcon.applyTo(null));
|
||||
|
||||
if (!GraphicsEnvironment.isHeadless()) {
|
||||
JFrame frame = new JFrame();
|
||||
assertDoesNotThrow(() -> AppIcon.applyTo(frame));
|
||||
frame.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSvgResourcesExist() {
|
||||
URL svgUrl = getClass().getResource("/icons/lib3270j.svg");
|
||||
assertNotNull(svgUrl, "lib3270j.svg must exist in /icons/");
|
||||
|
||||
URL iconSvgUrl = getClass().getResource("/icons/j3270_icon.svg");
|
||||
assertNotNull(iconSvgUrl, "j3270_icon.svg must exist in /icons/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import haus.nightmare.j3270.ui.ConnectChoiceDialog;
|
||||
import haus.nightmare.j3270.ui.ThemeManager;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionConflictTest {
|
||||
|
||||
private final List<J3270App> createdApps = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
for (J3270App app : createdApps) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
|
||||
try {
|
||||
app.dispose();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private J3270App createApp() {
|
||||
try {
|
||||
J3270App app = new J3270App();
|
||||
createdApps.add(app);
|
||||
return app;
|
||||
} catch (HeadlessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectChoiceDialog components, styling, and action results")
|
||||
public void testConnectChoiceDialogComponents() {
|
||||
try {
|
||||
ConnectChoiceDialog dialog = new ConnectChoiceDialog(null, "mvs.corp.local:23");
|
||||
assertEquals("Active Connection", dialog.getTitle());
|
||||
assertNotNull(dialog.getNewWindowButton());
|
||||
assertNotNull(dialog.getAbortButton());
|
||||
assertNotNull(dialog.getCancelButton());
|
||||
assertEquals("New Window", dialog.getNewWindowButton().getText());
|
||||
assertEquals("Abort Current Connection", dialog.getAbortButton().getText());
|
||||
assertEquals("Cancel", dialog.getCancelButton().getText());
|
||||
|
||||
assertTrue(dialog.getLine1().getText().contains("mvs.corp.local:23"));
|
||||
assertTrue(dialog.getLine2().getText().contains("new window"));
|
||||
assertTrue(dialog.getLine2().getText().contains("abort"));
|
||||
|
||||
// Test button click choices
|
||||
dialog.getNewWindowButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.NEW_WINDOW, dialog.getChoice());
|
||||
|
||||
ConnectChoiceDialog dialog2 = new ConnectChoiceDialog(null, "cms.test:992");
|
||||
dialog2.getAbortButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.ABORT_CURRENT, dialog2.getChoice());
|
||||
|
||||
ConnectChoiceDialog dialog3 = new ConnectChoiceDialog(null, "cics.test:23");
|
||||
dialog3.getCancelButton().doClick();
|
||||
assertEquals(ConnectChoiceDialog.Choice.CANCEL, dialog3.getChoice());
|
||||
} catch (HeadlessException ignored) {
|
||||
// Expected in headless CI environments without X11
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with NEW_WINDOW choice opens in a new window and preserves current connection")
|
||||
public void testConnectChoiceNewWindow() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
config1.setSavedProfileName("Host 1");
|
||||
app.connect(config1, false);
|
||||
|
||||
assertTrue(app.getConnectedTarget().contains("host1.test") || app.getConnectedTarget().contains("Host 1"));
|
||||
|
||||
// Configure handler to return NEW_WINDOW
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
assertTrue(target.contains("host1.test") || target.contains("Host 1"));
|
||||
return ConnectChoiceDialog.Choice.NEW_WINDOW;
|
||||
});
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
config2.setSavedProfileName("Host 2");
|
||||
app.connect(config2, true);
|
||||
|
||||
// Handler was invoked
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called when already connected");
|
||||
|
||||
// A new window was opened
|
||||
assertEquals(initialWindows + 1, J3270App.getOpenWindows().size(),
|
||||
"A new window should have been added to openWindows");
|
||||
|
||||
// Original app remains on config1
|
||||
assertEquals("host1.test", app.getClient().getConfig().getHost());
|
||||
|
||||
// Find the new window
|
||||
J3270App newWindow = null;
|
||||
for (J3270App w : J3270App.getOpenWindows()) {
|
||||
if (w != app) {
|
||||
newWindow = w;
|
||||
createdApps.add(w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(newWindow, "New window must exist");
|
||||
assertNotNull(newWindow.getClient(), "New window should have a client");
|
||||
assertEquals("host2.test", newWindow.getClient().getConfig().getHost(),
|
||||
"New window must be connected to host2.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with ABORT_CURRENT choice aborts existing connection and connects in same window")
|
||||
public void testConnectChoiceAbortCurrent() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
|
||||
});
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called");
|
||||
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
|
||||
"No new window should be created when choosing ABORT_CURRENT");
|
||||
assertEquals("host2.test", app.getClient().getConfig().getHost(),
|
||||
"Same window should now be connected to host2.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("When connected, Connect with CANCEL choice keeps existing connection and creates no new window")
|
||||
public void testConnectChoiceCancel() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
int initialWindows = J3270App.getOpenWindows().size();
|
||||
|
||||
boolean[] handlerCalled = new boolean[]{false};
|
||||
app.setConnectChoiceHandler((owner, target) -> {
|
||||
handlerCalled[0] = true;
|
||||
return ConnectChoiceDialog.Choice.CANCEL;
|
||||
});
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
assertTrue(handlerCalled[0], "Connect choice handler must be called");
|
||||
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
|
||||
"No new window should be created when choosing CANCEL");
|
||||
assertEquals("host1.test", app.getClient().getConfig().getHost(),
|
||||
"Existing window must remain connected to host1.test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("File menu contains Close Window item with VK_W accelerator")
|
||||
public void testCloseWindowMenuItem() {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb);
|
||||
JMenu fileMenu = null;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
if ("File".equals(mb.getMenu(i).getText())) {
|
||||
fileMenu = mb.getMenu(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(fileMenu, "File menu must exist");
|
||||
|
||||
JMenuItem closeWindowItem = null;
|
||||
for (int i = 0; i < fileMenu.getItemCount(); i++) {
|
||||
JMenuItem item = fileMenu.getItem(i);
|
||||
if (item != null && "Close Window".equals(item.getText())) {
|
||||
closeWindowItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(closeWindowItem, "File menu must contain Close Window item");
|
||||
KeyStroke ks = closeWindowItem.getAccelerator();
|
||||
assertNotNull(ks, "Close Window item must have an accelerator");
|
||||
assertEquals(KeyEvent.VK_W, ks.getKeyCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Closing one window when multiple windows are open leaves remaining window open")
|
||||
public void testMultiWindowCloseIndependent() {
|
||||
J3270App app1 = createApp();
|
||||
if (app1 == null) return;
|
||||
|
||||
J3270App app2 = createApp();
|
||||
if (app2 == null) return;
|
||||
|
||||
assertTrue(J3270App.getOpenWindows().contains(app1));
|
||||
assertTrue(J3270App.getOpenWindows().contains(app2));
|
||||
assertEquals(2, J3270App.getOpenWindows().size());
|
||||
|
||||
// Close app1
|
||||
app1.closeWindow();
|
||||
|
||||
assertFalse(J3270App.getOpenWindows().contains(app1), "app1 should be removed from open windows");
|
||||
assertTrue(J3270App.getOpenWindows().contains(app2), "app2 should remain open");
|
||||
assertEquals(1, J3270App.getOpenWindows().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Abort current connection cleanly tears down old client and connects new client in same window")
|
||||
public void testAbortCurrentConnectionGracefullyDisconnects() throws Exception {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config1, false);
|
||||
|
||||
Telnet3270Client oldClient = app.getClient();
|
||||
assertNotNull(oldClient);
|
||||
|
||||
app.setConnectChoiceHandler((owner, target) -> ConnectChoiceDialog.Choice.ABORT_CURRENT);
|
||||
|
||||
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config2, true);
|
||||
|
||||
// Client on app is immediately replaced with new client
|
||||
Telnet3270Client newClient = app.getClient();
|
||||
assertNotNull(newClient);
|
||||
assertNotSame(oldClient, newClient);
|
||||
assertEquals("host2.test", newClient.getConfig().getHost());
|
||||
|
||||
// Old client should be disconnected
|
||||
Thread.sleep(100);
|
||||
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, oldClient.getConnectionState());
|
||||
assertFalse(oldClient.isConnected());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Explicit app.disconnect() tears down client and clears UI state")
|
||||
public void testExplicitDisconnectResetsUI() throws Exception {
|
||||
J3270App app = createApp();
|
||||
if (app == null) return;
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
|
||||
app.connect(config, false);
|
||||
|
||||
Telnet3270Client client = app.getClient();
|
||||
assertNotNull(client);
|
||||
|
||||
app.disconnect();
|
||||
|
||||
assertNull(app.getClient());
|
||||
assertFalse(app.isConnected());
|
||||
Thread.sleep(100);
|
||||
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
assertFalse(client.isConnected());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package haus.nightmare.j3270;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class LoggingConfigurationTest {
|
||||
|
||||
private PrintStream originalOut;
|
||||
private PrintStream originalErr;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalOut = System.out;
|
||||
originalErr = System.err;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
System.setOut(originalOut);
|
||||
System.setErr(originalErr);
|
||||
// Ensure all handlers are closed and logging reset
|
||||
J3270App.configureLogging(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingDisabledWhenDebugFalse() {
|
||||
File logFile = new File("j3270.log");
|
||||
if (logFile.exists()) {
|
||||
logFile.delete();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
|
||||
System.setOut(new PrintStream(outContent));
|
||||
System.setErr(new PrintStream(errContent));
|
||||
|
||||
J3270App.configureLogging(false);
|
||||
|
||||
// Root logger should have no handlers attached
|
||||
Logger rootLogger = Logger.getLogger("");
|
||||
assertEquals(0, rootLogger.getHandlers().length, "Root logger should have no handlers when debug is disabled");
|
||||
assertEquals(Level.OFF, rootLogger.getLevel(), "Root logger level should be OFF when debug is disabled");
|
||||
|
||||
// Application loggers should be OFF
|
||||
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare").getLevel());
|
||||
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.j3270").getLevel());
|
||||
assertEquals(Level.OFF, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
|
||||
|
||||
// Emit log records at all levels
|
||||
Logger appLogger = Logger.getLogger("haus.nightmare.j3270.J3270App");
|
||||
appLogger.severe("Test SEVERE message");
|
||||
appLogger.warning("Test WARNING message");
|
||||
appLogger.info("Test INFO message");
|
||||
appLogger.fine("Test FINE message");
|
||||
|
||||
// Verify nothing was written to stdout or stderr
|
||||
assertEquals(0, outContent.size(), "Standard output should be empty when debug is disabled");
|
||||
assertEquals(0, errContent.size(), "Standard error should be empty when debug is disabled");
|
||||
|
||||
// Verify j3270.log was NOT created
|
||||
assertFalse(logFile.exists(), "j3270.log should NOT be created when debug is disabled");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingEnabledWhenDebugTrue() {
|
||||
J3270App.configureLogging(true);
|
||||
|
||||
Logger rootLogger = Logger.getLogger("");
|
||||
Handler[] handlers = rootLogger.getHandlers();
|
||||
assertTrue(handlers.length >= 2, "Root logger should have at least ConsoleHandler and FileHandler when debug is enabled");
|
||||
|
||||
assertEquals(Level.ALL, rootLogger.getLevel());
|
||||
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare").getLevel());
|
||||
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.j3270").getLevel());
|
||||
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
|
||||
|
||||
File logFile = new File("j3270.log");
|
||||
File logFile1 = new File("j3270.log.1");
|
||||
assertTrue(logFile.exists() || logFile1.exists(), "j3270.log should be created when debug is enabled");
|
||||
|
||||
// Clean up
|
||||
J3270App.configureLogging(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.Frame;
|
||||
import java.awt.HeadlessException;
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionHostTypeConfigTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private String originalDefaultHostType;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
FileTransferDialog.resetSessionState();
|
||||
originalSavedHosts = HostStorage.getSavedHosts();
|
||||
originalDefaultHostType = Settings.getDefaultHostType();
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
FileTransferDialog.resetSessionState();
|
||||
HostStorage.clearAll();
|
||||
if (originalSavedHosts != null) {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
}
|
||||
Settings.setDefaultHostType(originalDefaultHostType);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost converts to ConnectionConfig preserving hostType and saved connection metadata")
|
||||
public void testSavedHostToConnectionConfigPreservesHostType() {
|
||||
SavedHost cmsHost = new SavedHost(
|
||||
"cms-uuid-1", "VM/CMS System", "vm370.corp.local", 23, 4, 62, 160, "",
|
||||
false, "CMS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgCms = cmsHost.toConnectionConfig();
|
||||
assertTrue(cfgCms.isSavedConnection(), "Config from SavedHost must be marked as saved connection");
|
||||
assertEquals("cms-uuid-1", cfgCms.getSavedProfileId());
|
||||
assertEquals("VM/CMS System", cfgCms.getSavedProfileName());
|
||||
assertEquals("CMS", cfgCms.getHostType());
|
||||
assertEquals(FTConfig.HostType.CMS, cfgCms.getFtHostType());
|
||||
|
||||
SavedHost cicsHost = new SavedHost(
|
||||
"cics-uuid-2", "CICS Gateway", "cics.corp.local", 23, 2, 24, 80, "LU1",
|
||||
false, "CICS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgCics = cicsHost.toConnectionConfig();
|
||||
assertTrue(cfgCics.isSavedConnection());
|
||||
assertEquals("CICS", cfgCics.getHostType());
|
||||
assertEquals(FTConfig.HostType.CICS, cfgCics.getFtHostType());
|
||||
|
||||
SavedHost tsoHost = new SavedHost(
|
||||
"tso-uuid-3", "MVS TSO", "tso.corp.local", 23, 4, 43, 80, "",
|
||||
false, "TSO", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
|
||||
ConnectionConfig cfgTso = tsoHost.toConnectionConfig();
|
||||
assertTrue(cfgTso.isSavedConnection());
|
||||
assertEquals("TSO", cfgTso.getHostType());
|
||||
assertEquals(FTConfig.HostType.TSO, cfgTso.getFtHostType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog preselects CMS when using a saved connection configured as CMS")
|
||||
public void testFileTransferDialogPrefillsFromSavedConnectionCms() {
|
||||
ConnectionConfig config = new ConnectionConfig("vm.example.com", 23);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId("vm-1");
|
||||
config.setSavedProfileName("VM370");
|
||||
config.setHostType("CMS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must preselect CMS when saved connection is configured as CMS");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog preselects CICS when using a saved connection configured as CICS")
|
||||
public void testFileTransferDialogPrefillsFromSavedConnectionCics() {
|
||||
ConnectionConfig config = new ConnectionConfig("cics.example.com", 23);
|
||||
config.setSavedConnection(true);
|
||||
config.setSavedProfileId("cics-1");
|
||||
config.setSavedProfileName("CICS Region");
|
||||
config.setHostType("CICS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CICS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must preselect CICS when saved connection is configured as CICS");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FileTransferDialog overrides stale prior transfer state with configured host type from saved connection")
|
||||
public void testFileTransferDialogOverridesOldTransferStateWhenSavedConnectionUsed() {
|
||||
// Simulate a prior transfer performed on a TSO system
|
||||
FileTransferDialog.TransferSessionState oldState = new FileTransferDialog.TransferSessionState();
|
||||
oldState.hostType = FTConfig.HostType.TSO;
|
||||
oldState.localFile = "test.txt";
|
||||
oldState.hostFile = "USER.DATA";
|
||||
FileTransferDialog.setLastTransferState(oldState);
|
||||
|
||||
// Now connect to a CMS saved connection
|
||||
ConnectionConfig cmsConfig = new ConnectionConfig("vm.corp.internal", 23);
|
||||
cmsConfig.setSavedConnection(true);
|
||||
cmsConfig.setSavedProfileName("VM Prod");
|
||||
cmsConfig.setHostType("CMS");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(cmsConfig);
|
||||
FileTransfer ft = new FileTransfer(client, null);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, ft);
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
|
||||
"FileTransferDialog must use the configured host type (CMS) of the saved connection rather than prior session's TSO");
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectDialog allows host type configuration and saves it to profile")
|
||||
public void testConnectDialogHostTypeConfigurationAndProfile() {
|
||||
try {
|
||||
ConnectDialog dialog = new ConnectDialog((Frame) null);
|
||||
assertNotNull(dialog.getHostTypeCombo(), "ConnectDialog must provide a Host Type combo box");
|
||||
assertEquals("TSO", dialog.getSelectedHostType());
|
||||
|
||||
// Switch to CMS
|
||||
dialog.setInitialHostType("CMS");
|
||||
assertEquals("CMS", dialog.getSelectedHostType());
|
||||
assertEquals(FTConfig.HostType.CMS, dialog.getSelectedFtHostType());
|
||||
|
||||
// Switch to CICS
|
||||
dialog.setInitialHostType("CICS");
|
||||
assertEquals("CICS", dialog.getSelectedHostType());
|
||||
assertEquals(FTConfig.HostType.CICS, dialog.getSelectedFtHostType());
|
||||
|
||||
// Loading a SavedHost profile updates host type in ConnectDialog
|
||||
SavedHost cmsProfile = new SavedHost(
|
||||
"cms-prof-1", "CMS Dev", "cms.dev.local", 23, 4, 62, 160, "",
|
||||
false, "CMS", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
dialog.loadProfile(cmsProfile);
|
||||
assertEquals("CMS", dialog.getSelectedHostType());
|
||||
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Settings persists and exports defaultHostType")
|
||||
public void testSettingsDefaultHostTypePersistenceAndIni() throws Exception {
|
||||
Settings.setDefaultHostType("CMS");
|
||||
assertEquals("CMS", Settings.getDefaultHostType());
|
||||
|
||||
File tmpIni = File.createTempFile("j3270_test_cfg", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try (PrintWriter writer = new PrintWriter(tmpIni)) {
|
||||
writer.println("[connection]");
|
||||
writer.println("hostType = CICS");
|
||||
}
|
||||
|
||||
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
|
||||
assertEquals("CICS", Settings.getDefaultHostType());
|
||||
|
||||
// Test export
|
||||
File exportFile = File.createTempFile("j3270_export", ".ini");
|
||||
exportFile.deleteOnExit();
|
||||
Settings.exportToIniFile(exportFile.getAbsolutePath());
|
||||
|
||||
String content = new String(java.nio.file.Files.readAllBytes(exportFile.toPath()));
|
||||
assertTrue(content.contains("hostType = CICS"), "Exported INI must contain hostType = CICS");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("J3270App setConnectionHostType updates connection config and syncs with profile in HostStorage")
|
||||
public void testSetConnectionHostTypeUpdatesActiveConnectionAndStorage() {
|
||||
SavedHost saved = new SavedHost(
|
||||
"saved-prof-1", "Prod CMS", "vm.prod", 23, 4, 62, 160, "",
|
||||
false, "TSO", false, true, true, "BOTH", "037",
|
||||
"NONE", "", 0, "", ""
|
||||
);
|
||||
HostStorage.saveHost(saved);
|
||||
|
||||
ConnectionConfig config = saved.toConnectionConfig();
|
||||
assertEquals("TSO", config.getHostType());
|
||||
|
||||
try {
|
||||
J3270App app = new J3270App();
|
||||
try {
|
||||
// Verify Connection menu exists in JMenuBar
|
||||
JMenuBar mb = app.getJMenuBar();
|
||||
assertNotNull(mb);
|
||||
boolean hasConnectionMenu = false;
|
||||
for (int i = 0; i < mb.getMenuCount(); i++) {
|
||||
JMenu m = mb.getMenu(i);
|
||||
if (m != null && "Connection".equals(m.getText())) {
|
||||
hasConnectionMenu = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(hasConnectionMenu, "JMenuBar must include a Connection menu");
|
||||
|
||||
// Simulate connecting with this config
|
||||
app.connect(config);
|
||||
|
||||
// Change host type via setConnectionHostType
|
||||
app.setConnectionHostType(FTConfig.HostType.CMS);
|
||||
assertEquals(FTConfig.HostType.CMS, app.getClient().getConfig().getFtHostType());
|
||||
|
||||
// Profile in storage should also be updated
|
||||
SavedHost reloaded = HostStorage.findById("saved-prof-1");
|
||||
assertNotNull(reloaded);
|
||||
assertEquals("CMS", reloaded.getHostType());
|
||||
} finally {
|
||||
app.dispose();
|
||||
}
|
||||
} catch (HeadlessException ignored) {
|
||||
// In headless environments where JFrame cannot be initialized, test model updates directly
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getConfig().setFtHostType(FTConfig.HostType.CMS);
|
||||
saved.setHostType(FTConfig.HostType.CMS.name());
|
||||
HostStorage.saveHost(saved);
|
||||
|
||||
SavedHost reloaded = HostStorage.findById("saved-prof-1");
|
||||
assertNotNull(reloaded);
|
||||
assertEquals("CMS", reloaded.getHostType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package haus.nightmare.j3270.ft;
|
||||
|
||||
import haus.nightmare.j3270.ui.HostDirectoryDialog;
|
||||
import haus.nightmare.lib3270j.ft.FTConfig;
|
||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
|
||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -99,4 +102,114 @@ public class FileTransferSessionAndCmsTest {
|
||||
// Headless environment; verified through state retention
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPdsSessionStatePersistence() {
|
||||
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
|
||||
state.hostType = FTConfig.HostType.TSO;
|
||||
state.isSend = true;
|
||||
state.hostFile = "'USER.PDS.DATA'";
|
||||
state.units = FTConfig.AllocationUnit.CYLINDERS;
|
||||
state.space = "10,5";
|
||||
state.dirBlocks = "20";
|
||||
|
||||
FileTransferDialog.setLastTransferState(state);
|
||||
|
||||
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
|
||||
assertNotNull(retrieved);
|
||||
assertEquals(FTConfig.AllocationUnit.CYLINDERS, retrieved.units);
|
||||
assertEquals("10,5", retrieved.space);
|
||||
assertEquals("20", retrieved.dirBlocks);
|
||||
|
||||
try {
|
||||
FileTransferDialog dialog = new FileTransferDialog(null, null);
|
||||
assertEquals(FTConfig.AllocationUnit.CYLINDERS, dialog.getUnitsCombo().getSelectedItem());
|
||||
assertEquals("10,5", dialog.getSpaceField().getText());
|
||||
assertEquals("20", dialog.getDirField().getText());
|
||||
dialog.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostDirectoryDialogStructureAndFilter() {
|
||||
// Test default query resolution
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, null));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, ""));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, " "));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "* * *"));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "*"));
|
||||
assertEquals("TEST EXEC", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "TEST EXEC"));
|
||||
|
||||
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, null));
|
||||
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, ""));
|
||||
assertEquals("'MY.DATASET'", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, "'MY.DATASET'"));
|
||||
|
||||
// Test CMS filter formatting
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(null));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(""));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("* * *"));
|
||||
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("*"));
|
||||
assertEquals("TEST * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST"));
|
||||
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC"));
|
||||
assertEquals("TEST EXEC B", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC B"));
|
||||
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST.EXEC"));
|
||||
|
||||
try {
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "");
|
||||
assertNotNull(dialog);
|
||||
assertFalse(dialog.isConfirmed());
|
||||
assertNull(dialog.getSelectedHostFile());
|
||||
assertEquals("* * A", dialog.getQueryField().getText());
|
||||
dialog.dispose();
|
||||
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog2 =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "* * *");
|
||||
assertEquals("* * A", dialog2.getQueryField().getText());
|
||||
dialog2.dispose();
|
||||
|
||||
haus.nightmare.j3270.ui.HostDirectoryDialog dialog3 =
|
||||
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "TEST EXEC");
|
||||
assertEquals("TEST EXEC", dialog3.getQueryField().getText());
|
||||
dialog3.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesFilterWildcards() {
|
||||
CMSDirectoryEntry cmsA = new CMSDirectoryEntry("PROFILE", "EXEC", "A1");
|
||||
CMSDirectoryEntry cmsB = new CMSDirectoryEntry("PROFILE", "EXEC", "B1");
|
||||
CMSDirectoryEntry cmsSys = new CMSDirectoryEntry("SYSTEM", "CONFIG", "S2");
|
||||
|
||||
// * * A matches A disk files (A1, A0, etc.), excludes B and S
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* * A"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsB, "* * A"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* * A"));
|
||||
|
||||
// Blank, *, * * * matches all
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, ""));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsB, "* * *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsSys, "* * *"));
|
||||
|
||||
// Two tokens (fn ft) and dot tokens
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* EXEC"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*.EXEC"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROFILE *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROF*.*"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* EXEC"));
|
||||
|
||||
// TSO Wildcards
|
||||
TSODirectoryEntry tso = new TSODirectoryEntry("TSOUSER.TEST.DATA");
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, ""));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "* * *"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*.DATA"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TEST"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TSOUSER.*"));
|
||||
assertTrue(HostDirectoryDialog.matchesFilter(tso, "'TSOUSER.TEST.*'"));
|
||||
assertFalse(HostDirectoryDialog.matchesFilter(tso, "*.CNTL"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.ui.SettingsDialog;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ConnectionIniStorageTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private String origHost;
|
||||
private int origPort;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalSavedHosts = HostStorage.getSavedHosts();
|
||||
origHost = Settings.getAutoConnectHost();
|
||||
origPort = Settings.getAutoConnectPort();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
if (originalSavedHosts != null) {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
} else {
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
Settings.setAutoConnectHost(origHost);
|
||||
Settings.setAutoConnectPort(origPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage exports to and loads from connections.ini with full fidelity")
|
||||
public void testExportAndLoadConnectionsIniRoundtrip() throws IOException {
|
||||
List<SavedHost> hosts = new ArrayList<>();
|
||||
SavedHost h1 = new SavedHost("id-mainframe-1", "Production TSO", "mainframe.corp.com", 23, 4, 62, 160,
|
||||
"LU01", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
SavedHost h2 = new SavedHost("id-vm-2", "VM CMS Test", "vmcms.lab.net", 992, 2, 24, 80,
|
||||
"LUCMS", false, "CMS", true, false, true, "GDDM", "1047", "SOCKS5", "10.0.0.1", 1080, "proxyuser", "proxypass");
|
||||
hosts.add(h1);
|
||||
hosts.add(h2);
|
||||
|
||||
File tmpIni = File.createTempFile("connections_test", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
HostStorage.exportToIniFile(tmpIni, hosts);
|
||||
|
||||
assertTrue(tmpIni.exists());
|
||||
String content = new String(Files.readAllBytes(tmpIni.toPath()));
|
||||
assertTrue(content.contains("[Production TSO]"), "Must contain section header [Production TSO]");
|
||||
assertTrue(content.contains("[VM CMS Test]"), "Must contain section header [VM CMS Test]");
|
||||
assertTrue(content.contains("host = mainframe.corp.com"));
|
||||
assertTrue(content.contains("proxyHost = 10.0.0.1"));
|
||||
assertTrue(content.contains("proxyType = SOCKS5"));
|
||||
|
||||
List<SavedHost> loaded = HostStorage.loadFromIniFile(tmpIni);
|
||||
assertEquals(2, loaded.size(), "Should load 2 connections from INI");
|
||||
|
||||
SavedHost loaded1 = loaded.get(0);
|
||||
assertEquals("id-mainframe-1", loaded1.getId());
|
||||
assertEquals("Production TSO", loaded1.getName());
|
||||
assertEquals("mainframe.corp.com", loaded1.getHost());
|
||||
assertEquals(23, loaded1.getPort());
|
||||
assertEquals(4, loaded1.getModel());
|
||||
assertEquals(62, loaded1.getDynamicRows());
|
||||
assertEquals(160, loaded1.getDynamicCols());
|
||||
assertEquals("LU01", loaded1.getLuName());
|
||||
assertTrue(loaded1.isAutoConnect());
|
||||
assertEquals("TSO", loaded1.getHostType());
|
||||
assertFalse(loaded1.isUseTls());
|
||||
assertTrue(loaded1.isTlsVerifyCert());
|
||||
|
||||
SavedHost loaded2 = loaded.get(1);
|
||||
assertEquals("id-vm-2", loaded2.getId());
|
||||
assertEquals("VM CMS Test", loaded2.getName());
|
||||
assertEquals("vmcms.lab.net", loaded2.getHost());
|
||||
assertEquals(992, loaded2.getPort());
|
||||
assertEquals(2, loaded2.getModel());
|
||||
assertEquals(24, loaded2.getDynamicRows());
|
||||
assertEquals(80, loaded2.getDynamicCols());
|
||||
assertEquals("LUCMS", loaded2.getLuName());
|
||||
assertFalse(loaded2.isAutoConnect());
|
||||
assertEquals("CMS", loaded2.getHostType());
|
||||
assertTrue(loaded2.isUseTls());
|
||||
assertFalse(loaded2.isTlsVerifyCert());
|
||||
assertEquals("SOCKS5", loaded2.getProxyType());
|
||||
assertEquals("10.0.0.1", loaded2.getProxyHost());
|
||||
assertEquals(1080, loaded2.getProxyPort());
|
||||
assertEquals("proxyuser", loaded2.getProxyUsername());
|
||||
assertEquals("proxypass", loaded2.getProxyPassword());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage importFromIniFile merges and updates existing hosts by name or id")
|
||||
public void testImportFromIniFileMergesAndUpdates() throws IOException {
|
||||
SavedHost initial = new SavedHost("id-100", "Mainframe Prod", "old.host.org", 23, 4, 62, 160,
|
||||
"", false, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
List<SavedHost> initList = new ArrayList<>();
|
||||
initList.add(initial);
|
||||
HostStorage.saveAll(initList);
|
||||
|
||||
File tmpIni = File.createTempFile("connections_merge", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
// Update Mainframe Prod with new host address
|
||||
w.write("[Mainframe Prod]\n");
|
||||
w.write("id = id-100\n");
|
||||
w.write("host = new.host.org\n");
|
||||
w.write("port = 2323\n");
|
||||
w.write("\n");
|
||||
// Add a second connection
|
||||
w.write("[Development CICS]\n");
|
||||
w.write("host = cics.dev.org\n");
|
||||
w.write("port = 992\n");
|
||||
w.write("hostType = CICS\n");
|
||||
w.write("useTls = true\n");
|
||||
}
|
||||
|
||||
List<SavedHost> result = HostStorage.importFromIniFile(tmpIni);
|
||||
assertEquals(2, result.size(), "Import should result in 2 saved hosts (1 updated, 1 added)");
|
||||
|
||||
SavedHost updated = HostStorage.findById("id-100");
|
||||
assertNotNull(updated);
|
||||
assertEquals("new.host.org", updated.getHost());
|
||||
assertEquals(2323, updated.getPort());
|
||||
|
||||
SavedHost added = HostStorage.findByName("Development CICS");
|
||||
assertNotNull(added);
|
||||
assertEquals("cics.dev.org", added.getHost());
|
||||
assertEquals(992, added.getPort());
|
||||
assertEquals("CICS", added.getHostType());
|
||||
assertTrue(added.isUseTls());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage parses INI section prefixes and aliases properly")
|
||||
public void testImportHandlesMissingFieldsAndAliases() throws IOException {
|
||||
File tmpIni = File.createTempFile("connections_alias", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
w.write("[connection:Aliased Host]\n");
|
||||
w.write("hostname = test.alias.net\n");
|
||||
w.write("port = 1023\n");
|
||||
w.write("terminalModel = 2\n");
|
||||
w.write("type = CMS\n");
|
||||
w.write("tls = true\n");
|
||||
w.write("verifyCert = false\n");
|
||||
w.write("charset = 1047\n");
|
||||
w.write("rows = 24\n");
|
||||
w.write("cols = 80\n");
|
||||
w.write("lu = LUVM\n");
|
||||
}
|
||||
|
||||
List<SavedHost> parsed = HostStorage.loadFromIniFile(tmpIni);
|
||||
assertEquals(1, parsed.size());
|
||||
|
||||
SavedHost h = parsed.get(0);
|
||||
assertEquals("Aliased Host", h.getName());
|
||||
assertEquals("test.alias.net", h.getHost());
|
||||
assertEquals(1023, h.getPort());
|
||||
assertEquals(2, h.getModel());
|
||||
assertEquals("CMS", h.getHostType());
|
||||
assertTrue(h.isUseTls());
|
||||
assertFalse(h.isTlsVerifyCert());
|
||||
assertEquals("1047", h.getCodePage());
|
||||
assertEquals(24, h.getDynamicRows());
|
||||
assertEquals(80, h.getDynamicCols());
|
||||
assertEquals("LUVM", h.getLuName());
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog contains Import Connections and Export Connections buttons and is taller")
|
||||
public void testSettingsDialogConnectionButtonsAndHeight() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
JButton btnImportConn = dialog.getBtnImportConnections();
|
||||
JButton btnExportConn = dialog.getBtnExportConnections();
|
||||
|
||||
assertNotNull(btnImportConn, "Import Connections button must exist in SettingsDialog");
|
||||
assertNotNull(btnExportConn, "Export Connections button must exist in SettingsDialog");
|
||||
|
||||
assertEquals("Import Connections...", btnImportConn.getText());
|
||||
assertEquals("Export Connections...", btnExportConn.getText());
|
||||
|
||||
// Check window height: must be taller than previous 540 to accommodate extra buttons
|
||||
assertTrue(dialog.getHeight() > 540,
|
||||
"Settings window height (" + dialog.getHeight() + ") must be taller than 540 to accommodate the extra buttons");
|
||||
|
||||
// Verify config buttons share container row1, and connection buttons share container row2
|
||||
JButton btnImport = dialog.getBtnImport();
|
||||
JButton btnExport = dialog.getBtnExport();
|
||||
assertNotNull(btnImport);
|
||||
assertNotNull(btnExport);
|
||||
|
||||
Container configRow = btnImport.getParent();
|
||||
assertNotNull(configRow);
|
||||
assertSame(configRow, btnExport.getParent(), "btnImport and btnExport must share the same parent row");
|
||||
|
||||
Container connRow = btnImportConn.getParent();
|
||||
assertNotNull(connRow);
|
||||
assertSame(connRow, btnExportConn.getParent(), "btnImportConn and btnExportConn must share the same parent row");
|
||||
|
||||
assertNotSame(configRow, connRow, "Connection buttons must be below and NOT in line with config buttons");
|
||||
|
||||
// Verify connRow is positioned below configRow in buttonPanel
|
||||
Container buttonPanel = configRow.getParent();
|
||||
assertNotNull(buttonPanel);
|
||||
assertSame(buttonPanel, connRow.getParent(), "Both rows must be in buttonPanel");
|
||||
|
||||
int configRowIdx = -1;
|
||||
int connRowIdx = -1;
|
||||
Component[] rows = buttonPanel.getComponents();
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
if (rows[i] == configRow) configRowIdx = i;
|
||||
if (rows[i] == connRow) connRowIdx = i;
|
||||
}
|
||||
assertTrue(configRowIdx >= 0, "configRow must be in buttonPanel");
|
||||
assertTrue(connRowIdx >= 0, "connRow must be in buttonPanel");
|
||||
assertTrue(connRowIdx > configRowIdx, "connRow must be below configRow in buttonPanel (" + connRowIdx + " > " + configRowIdx + ")");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog importConnectionsFile updates savedHostBox and Settings")
|
||||
public void testSettingsDialogImportConnectionsFileUpdatesDropdownAndSettings() throws IOException {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
File tmpIni = File.createTempFile("settings_dialog_import_test", ".ini");
|
||||
tmpIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (FileWriter w = new FileWriter(tmpIni)) {
|
||||
w.write("[Auto Mainframe]\n");
|
||||
w.write("host = auto.mainframe.org\n");
|
||||
w.write("port = 992\n");
|
||||
w.write("autoConnect = true\n");
|
||||
w.write("useTls = true\n");
|
||||
}
|
||||
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
dialog.importConnectionsFile(tmpIni);
|
||||
|
||||
// Check savedHostBox updated
|
||||
JComboBox<SavedHost> box = dialog.getSavedHostBox();
|
||||
assertNotNull(box);
|
||||
assertTrue(box.getItemCount() >= 1);
|
||||
|
||||
SavedHost sel = (SavedHost) box.getSelectedItem();
|
||||
assertNotNull(sel);
|
||||
assertEquals("auto.mainframe.org", sel.getHost());
|
||||
assertTrue(sel.isAutoConnect());
|
||||
|
||||
// Check Settings updated for autoConnect host
|
||||
assertEquals("auto.mainframe.org", Settings.getAutoConnectHost());
|
||||
assertEquals(992, Settings.getAutoConnectPort());
|
||||
assertTrue(Settings.getAutoConnectTls());
|
||||
|
||||
// Test exportConnectionsFile
|
||||
File exportTarget = File.createTempFile("settings_dialog_export_test", ".ini");
|
||||
exportTarget.deleteOnExit();
|
||||
dialog.exportConnectionsFile(exportTarget);
|
||||
assertTrue(exportTarget.exists());
|
||||
String exported = new String(Files.readAllBytes(exportTarget.toPath()));
|
||||
assertTrue(exported.contains("auto.mainframe.org"));
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
} finally {
|
||||
if (tmpIni.exists()) tmpIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog importConnectionsFile throws FileNotFoundException for nonexistent file")
|
||||
public void testImportNonExistentConnectionsFileThrows() {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
File nonExistent = new File("/path/to/nonexistent/connections_" + System.currentTimeMillis() + ".ini");
|
||||
assertThrows(FileNotFoundException.class, () -> {
|
||||
dialog.importConnectionsFile(nonExistent);
|
||||
});
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package haus.nightmare.j3270.storage;
|
||||
|
||||
import haus.nightmare.j3270.ui.ConnectDialog;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Frame;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class HostStorageAndProxyTest {
|
||||
|
||||
private List<SavedHost> originalHosts;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalHosts = HostStorage.getSavedHosts();
|
||||
HostStorage.clearAll();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
HostStorage.clearAll();
|
||||
HostStorage.saveAll(originalHosts);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost correctly models per-connection proxy and converts to ConnectionConfig")
|
||||
public void testSavedHostProxyToConnectionConfig() {
|
||||
// 1. Host with SOCKS5 proxy and auth
|
||||
SavedHost host1 = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Prod Mainframe",
|
||||
"mvs.corp.local",
|
||||
2323,
|
||||
4,
|
||||
43,
|
||||
80,
|
||||
"TSO01",
|
||||
false,
|
||||
"TSO",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"037",
|
||||
"SOCKS5",
|
||||
"proxy.internal.corp",
|
||||
1080,
|
||||
"admin",
|
||||
"secret123"
|
||||
);
|
||||
|
||||
ConnectionConfig cfg1 = host1.toConnectionConfig();
|
||||
assertEquals("mvs.corp.local", cfg1.getHost());
|
||||
assertEquals(2323, cfg1.getPort());
|
||||
assertTrue(cfg1.isUseTls());
|
||||
assertEquals("TSO01", cfg1.getLuName());
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS5, cfg1.getProxyType());
|
||||
assertEquals("proxy.internal.corp", cfg1.getProxyHost());
|
||||
assertEquals(1080, cfg1.getProxyPort());
|
||||
assertEquals("admin", cfg1.getProxyUsername());
|
||||
assertEquals("secret123", cfg1.getProxyPassword());
|
||||
|
||||
// 2. Host with direct connection (NONE proxy)
|
||||
SavedHost host2 = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Local VM370",
|
||||
"127.0.0.1",
|
||||
23,
|
||||
2,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
false,
|
||||
"CMS",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
"BOTH",
|
||||
"037",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
|
||||
ConnectionConfig cfg2 = host2.toConnectionConfig();
|
||||
assertEquals("127.0.0.1", cfg2.getHost());
|
||||
assertEquals(23, cfg2.getPort());
|
||||
assertFalse(cfg2.isUseTls());
|
||||
assertEquals(ConnectionConfig.ProxyType.NONE, cfg2.getProxyType());
|
||||
assertNull(cfg2.getProxyHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage persists and restores multiple host profiles with isolated per-connection proxies")
|
||||
public void testHostStoragePersistenceAndIsolation() {
|
||||
SavedHost h1 = new SavedHost(
|
||||
"id-1",
|
||||
"System A (SOCKS5)",
|
||||
"sysa.corp",
|
||||
23,
|
||||
4,
|
||||
62,
|
||||
160,
|
||||
"",
|
||||
false,
|
||||
"TSO",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"037",
|
||||
"SOCKS5",
|
||||
"socks.gateway.net",
|
||||
1080,
|
||||
"alice",
|
||||
"passA"
|
||||
);
|
||||
|
||||
SavedHost h2 = new SavedHost(
|
||||
"id-2",
|
||||
"System B (HTTP CONNECT)",
|
||||
"sysb.dmz",
|
||||
992,
|
||||
5,
|
||||
27,
|
||||
132,
|
||||
"LU99",
|
||||
false,
|
||||
"TSO",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"1047",
|
||||
"HTTP",
|
||||
"squid.proxy.org",
|
||||
3128,
|
||||
"bob",
|
||||
"passB"
|
||||
);
|
||||
|
||||
SavedHost h3 = new SavedHost(
|
||||
"id-3",
|
||||
"System C (Direct)",
|
||||
"sysc.lan",
|
||||
23,
|
||||
2,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
true, // autoConnect
|
||||
"CMS",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"BOTH",
|
||||
"500",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
|
||||
HostStorage.saveHost(h1);
|
||||
HostStorage.saveHost(h2);
|
||||
HostStorage.saveHost(h3);
|
||||
|
||||
List<SavedHost> loaded = HostStorage.getSavedHosts();
|
||||
assertEquals(3, loaded.size());
|
||||
|
||||
SavedHost loadedA = HostStorage.findById("id-1");
|
||||
assertNotNull(loadedA);
|
||||
assertEquals("System A (SOCKS5)", loadedA.getName());
|
||||
assertEquals("SOCKS5", loadedA.getProxyType());
|
||||
assertEquals("socks.gateway.net", loadedA.getProxyHost());
|
||||
assertEquals(1080, loadedA.getProxyPort());
|
||||
assertEquals("alice", loadedA.getProxyUsername());
|
||||
assertEquals("passA", loadedA.getProxyPassword());
|
||||
|
||||
SavedHost loadedB = HostStorage.findById("id-2");
|
||||
assertNotNull(loadedB);
|
||||
assertEquals("HTTP", loadedB.getProxyType());
|
||||
assertEquals("squid.proxy.org", loadedB.getProxyHost());
|
||||
assertEquals(3128, loadedB.getProxyPort());
|
||||
assertEquals("bob", loadedB.getProxyUsername());
|
||||
assertEquals("passB", loadedB.getProxyPassword());
|
||||
|
||||
SavedHost loadedC = HostStorage.findById("id-3");
|
||||
assertNotNull(loadedC);
|
||||
assertEquals("NONE", loadedC.getProxyType());
|
||||
assertEquals("", loadedC.getProxyHost());
|
||||
assertTrue(loadedC.isAutoConnect());
|
||||
|
||||
// Verify getAutoConnectHost returns System C with its own connection settings
|
||||
SavedHost auto = HostStorage.getAutoConnectHost();
|
||||
assertNotNull(auto);
|
||||
assertEquals("id-3", auto.getId());
|
||||
assertEquals(ConnectionConfig.ProxyType.NONE, auto.toConnectionConfig().getProxyType());
|
||||
|
||||
// Verify deleteHost
|
||||
HostStorage.deleteHost("id-2");
|
||||
assertEquals(2, HostStorage.getSavedHosts().size());
|
||||
assertNull(HostStorage.findById("id-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HostStorage JSON matches a3270 format and parses a3270 JSON strings")
|
||||
public void testA3270JsonFormatCompatibility() {
|
||||
// String representative of a3270 saved_hosts preference JSON
|
||||
String a3270Json = "[\n" +
|
||||
" {\n" +
|
||||
" \"id\": \"android-uuid-1\",\n" +
|
||||
" \"name\": \"Dallas Mainframe\",\n" +
|
||||
" \"host\": \"dallas.ibm.com\",\n" +
|
||||
" \"port\": 2323,\n" +
|
||||
" \"model\": 4,\n" +
|
||||
" \"dynamicRows\": 62,\n" +
|
||||
" \"dynamicCols\": 160,\n" +
|
||||
" \"luName\": \"DAL01\",\n" +
|
||||
" \"autoConnect\": false,\n" +
|
||||
" \"hostType\": \"TSO\",\n" +
|
||||
" \"useTls\": true,\n" +
|
||||
" \"tlsVerifyCert\": false,\n" +
|
||||
" \"tn3270e\": true,\n" +
|
||||
" \"graphicsMode\": \"BOTH\",\n" +
|
||||
" \"codePage\": \"037\",\n" +
|
||||
" \"proxyType\": \"SOCKS5\",\n" +
|
||||
" \"proxyHost\": \"10.20.30.40\",\n" +
|
||||
" \"proxyPort\": 9050,\n" +
|
||||
" \"proxyUsername\": \"dallasuser\",\n" +
|
||||
" \"proxyPassword\": \"dallaspass\"\n" +
|
||||
" }\n" +
|
||||
"]";
|
||||
|
||||
List<SavedHost> parsed = HostStorage.parseHostsJson(a3270Json);
|
||||
assertEquals(1, parsed.size());
|
||||
SavedHost h = parsed.get(0);
|
||||
assertEquals("android-uuid-1", h.getId());
|
||||
assertEquals("Dallas Mainframe", h.getName());
|
||||
assertEquals("dallas.ibm.com", h.getHost());
|
||||
assertEquals(2323, h.getPort());
|
||||
assertEquals(4, h.getModel());
|
||||
assertTrue(h.isUseTls());
|
||||
assertFalse(h.isTlsVerifyCert());
|
||||
assertEquals("SOCKS5", h.getProxyType());
|
||||
assertEquals("10.20.30.40", h.getProxyHost());
|
||||
assertEquals(9050, h.getProxyPort());
|
||||
assertEquals("dallasuser", h.getProxyUsername());
|
||||
assertEquals("dallaspass", h.getProxyPassword());
|
||||
|
||||
// Roundtrip serialization
|
||||
String serialized = HostStorage.serializeHostsJson(parsed);
|
||||
List<SavedHost> roundtripped = HostStorage.parseHostsJson(serialized);
|
||||
assertEquals(1, roundtripped.size());
|
||||
SavedHost r = roundtripped.get(0);
|
||||
assertEquals(h.getId(), r.getId());
|
||||
assertEquals(h.getProxyType(), r.getProxyType());
|
||||
assertEquals(h.getProxyHost(), r.getProxyHost());
|
||||
assertEquals(h.getProxyPort(), r.getProxyPort());
|
||||
assertEquals(h.getProxyUsername(), r.getProxyUsername());
|
||||
assertEquals(h.getProxyPassword(), r.getProxyPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConnectDialog loads profile proxy and applies per-connection proxy to ConnectionConfig")
|
||||
public void testConnectDialogProxyHandling() {
|
||||
if (!GraphicsEnvironment.isHeadless()) {
|
||||
ConnectDialog dialog = new ConnectDialog((Frame) null);
|
||||
dialog.setInitialHost("test.mainframe.com");
|
||||
dialog.setInitialPort(23);
|
||||
dialog.setInitialProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", 1080, "testuser", "testpass");
|
||||
|
||||
assertEquals(ConnectionConfig.ProxyType.SOCKS5, dialog.getSelectedProxyType());
|
||||
assertEquals("127.0.0.1", dialog.getProxyHost());
|
||||
assertEquals(1080, dialog.getProxyPort());
|
||||
assertEquals("testuser", dialog.getProxyUsername());
|
||||
assertEquals("testpass", dialog.getProxyPassword());
|
||||
|
||||
// Loading a profile updates dialog proxy fields
|
||||
SavedHost profile = new SavedHost(
|
||||
"p1", "Profile 1", "prod.ibm.net", 992, 4, 43, 80, "", false,
|
||||
"TSO", true, true, true, "BOTH", "037",
|
||||
"HTTP", "proxy.corp.net", 8080, "corpuser", "corppass"
|
||||
);
|
||||
dialog.loadProfile(profile);
|
||||
|
||||
assertEquals(ConnectionConfig.ProxyType.HTTP, dialog.getSelectedProxyType());
|
||||
assertEquals("proxy.corp.net", dialog.getProxyHost());
|
||||
assertEquals(8080, dialog.getProxyPort());
|
||||
assertEquals("corpuser", dialog.getProxyUsername());
|
||||
assertEquals("corppass", dialog.getProxyPassword());
|
||||
dialog.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
|
||||
public void testSavedHostGraphicsModeSerializationRoundtrip() {
|
||||
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
|
||||
SavedHost host = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Gfx " + gm.name(),
|
||||
"test.host.com",
|
||||
23,
|
||||
4,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
false,
|
||||
"TSO",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
gm.name(),
|
||||
"037",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
String json = HostStorage.serializeHostsJson(List.of(host));
|
||||
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
|
||||
assertEquals(1, parsed.size());
|
||||
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
|
||||
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
|
||||
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
|
||||
"ConnectionConfig must reflect GraphicsMode." + gm.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.j3270.storage.HostStorage;
|
||||
import haus.nightmare.j3270.storage.SavedHost;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class AutoConnectSettingsAndTerminalCenteringTest {
|
||||
|
||||
private List<SavedHost> originalSavedHosts;
|
||||
private Settings.StartupBehavior originalStartupBehavior;
|
||||
private String originalAutoConnectHost;
|
||||
private int originalAutoConnectPort;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
originalSavedHosts = new ArrayList<>(HostStorage.getSavedHosts());
|
||||
originalStartupBehavior = Settings.getStartupBehavior();
|
||||
originalAutoConnectHost = Settings.getAutoConnectHost();
|
||||
originalAutoConnectPort = Settings.getAutoConnectPort();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
Settings.setStartupBehavior(originalStartupBehavior);
|
||||
Settings.setAutoConnectHost(originalAutoConnectHost);
|
||||
Settings.setAutoConnectPort(originalAutoConnectPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TerminalPanel centering: left bound is flush when constrained, centered with left <= right when too big")
|
||||
public void testTerminalPanelCenteringLogic() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
panel.setClient(client);
|
||||
|
||||
int cols = sb.getDisplayCols(); // 80
|
||||
int rows = sb.getDisplayRows(); // 24
|
||||
int gridW = cols * panel.cellWidth; // 800
|
||||
int gridH = rows * panel.cellHeight; // 480
|
||||
|
||||
// 1. Panel is smaller than grid: left bound must be 0 (flush left cells), vertical must be 0
|
||||
panel.setSize(gridW - 100, gridH - 50);
|
||||
assertEquals(0, panel.getRenderOffsetX(), "Left bound should be 0 when window width is constrained");
|
||||
assertEquals(0, panel.getRenderOffsetY(), "Top bound should be 0 when window height is constrained");
|
||||
|
||||
// 2. Panel exactly matches grid: offset must be 0
|
||||
panel.setSize(gridW, gridH);
|
||||
assertEquals(0, panel.getRenderOffsetX(), "Offset X must be 0 when width exactly matches grid");
|
||||
assertEquals(0, panel.getRenderOffsetY(), "Offset Y must be 0 when height exactly matches grid");
|
||||
|
||||
// 3. Panel is larger with odd extra (extra = 7)
|
||||
panel.setSize(gridW + 7, gridH + 11);
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int leftSpace = ox;
|
||||
int rightSpace = (gridW + 7) - (ox + gridW);
|
||||
assertTrue(leftSpace <= rightSpace, "Left blank space (" + leftSpace + ") must be <= right space (" + rightSpace + ")");
|
||||
assertEquals(3, leftSpace);
|
||||
assertEquals(4, rightSpace);
|
||||
|
||||
int oy = panel.getRenderOffsetY();
|
||||
int topSpace = oy;
|
||||
int bottomSpace = (gridH + 11) - (oy + gridH);
|
||||
assertTrue(topSpace <= bottomSpace, "Top space must be <= bottom space");
|
||||
assertEquals(5, topSpace);
|
||||
assertEquals(6, bottomSpace);
|
||||
|
||||
// 4. Panel is larger with even extra (extra = 6)
|
||||
panel.setSize(gridW + 6, gridH + 8);
|
||||
assertEquals(3, panel.getRenderOffsetX());
|
||||
assertEquals(4, panel.getRenderOffsetY());
|
||||
|
||||
panel.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("guardedPack does not resize window when window is too small")
|
||||
public void testGuardedPackDoesNotResizeWhenTooSmall() {
|
||||
try {
|
||||
JFrame frame = new JFrame();
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
panel.setClient(client);
|
||||
|
||||
frame.getContentPane().add(panel);
|
||||
frame.setSize(400, 300);
|
||||
frame.addNotify(); // makes frame displayable
|
||||
|
||||
// Set size explicitly smaller than preferred size (800+ x 480+)
|
||||
frame.setSize(400, 300);
|
||||
Dimension sizeBefore = frame.getSize();
|
||||
|
||||
// Run guardedPack
|
||||
panel.guardedPack();
|
||||
|
||||
// Window should not have expanded
|
||||
Dimension sizeAfter = frame.getSize();
|
||||
assertEquals(sizeBefore.width, sizeAfter.width, "Window width should NOT be enlarged when too small");
|
||||
assertEquals(sizeBefore.height, sizeAfter.height, "Window height should NOT be enlarged when too small");
|
||||
|
||||
frame.dispose();
|
||||
panel.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
// Environment without X display
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog Startup & Auto-Connect presents saved connections dropdown and updates auto-connect host")
|
||||
public void testSettingsDialogSavedConnectionsDropdown() throws Exception {
|
||||
SavedHost host1 = new SavedHost("id-test-1", "Host Alpha", "alpha.mainframe.test", 23, 2, 24, 80, "LU1", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
SavedHost host2 = new SavedHost("id-test-2", "Host Beta", "beta.mainframe.test", 992, 4, 43, 80, "LU2", false, "VM", true, false, true, "BOTH", "037", "NONE", "", 0, "", "");
|
||||
|
||||
List<SavedHost> testHosts = new ArrayList<>();
|
||||
testHosts.add(host1);
|
||||
testHosts.add(host2);
|
||||
HostStorage.saveAll(testHosts);
|
||||
Settings.setStartupBehavior(Settings.StartupBehavior.AUTO_CONNECT);
|
||||
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
JComboBox<SavedHost> combo = dialog.getSavedHostBox();
|
||||
assertNotNull(combo, "savedHostBox should not be null");
|
||||
assertEquals(2, combo.getItemCount(), "Dropdown should contain all saved hosts");
|
||||
|
||||
// Host Alpha was autoConnect=true, so it should be initially selected
|
||||
SavedHost initialSel = (SavedHost) combo.getSelectedItem();
|
||||
assertNotNull(initialSel);
|
||||
assertEquals("id-test-1", initialSel.getId());
|
||||
|
||||
// Switch to Host Beta
|
||||
combo.setSelectedIndex(1);
|
||||
|
||||
// Trigger applySettings via reflection
|
||||
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
|
||||
applyMethod.setAccessible(true);
|
||||
applyMethod.invoke(dialog);
|
||||
|
||||
// Verify Host Beta is now the auto-connect host
|
||||
SavedHost auto = HostStorage.getAutoConnectHost();
|
||||
assertNotNull(auto, "Auto-connect host should exist");
|
||||
assertEquals("id-test-2", auto.getId());
|
||||
assertEquals("Host Beta", auto.getName());
|
||||
assertEquals("beta.mainframe.test", auto.getHost());
|
||||
assertEquals(992, auto.getPort());
|
||||
|
||||
SavedHost loadedAlpha = HostStorage.findById("id-test-1");
|
||||
assertNotNull(loadedAlpha);
|
||||
assertFalse(loadedAlpha.isAutoConnect(), "Host Alpha should no longer have autoConnect = true");
|
||||
|
||||
// Verify Settings autoConnectHost/Port synchronized
|
||||
assertEquals("beta.mainframe.test", Settings.getAutoConnectHost());
|
||||
assertEquals(992, Settings.getAutoConnectPort());
|
||||
|
||||
// Switch startup behavior to SHOW_CONNECT and apply
|
||||
Field sbField = SettingsDialog.class.getDeclaredField("startupBehaviorBox");
|
||||
sbField.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
JComboBox<Settings.StartupBehavior> sbBox = (JComboBox<Settings.StartupBehavior>) sbField.get(dialog);
|
||||
sbBox.setSelectedItem(Settings.StartupBehavior.SHOW_CONNECT);
|
||||
|
||||
applyMethod.invoke(dialog);
|
||||
assertNull(HostStorage.getAutoConnectHost(), "When startup behavior is SHOW_CONNECT, auto-connect host should be cleared");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} finally {
|
||||
HostStorage.saveAll(originalSavedHosts);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Window size is adjusted once on connect for selected model and left untouched on onScreenSizeChanged")
|
||||
public void testWindowSizedOnceForModelAndNotOnScreenSizeChanged() {
|
||||
try {
|
||||
JFrame frame = new JFrame();
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
frame.getContentPane().add(panel);
|
||||
frame.addNotify();
|
||||
|
||||
// Pack for Model 4 (43 rows, 80 cols)
|
||||
panel.packForModel(43, 80);
|
||||
Dimension model4Size = frame.getSize();
|
||||
assertTrue(model4Size.height >= 43 * 20, "Height should accommodate 43 rows");
|
||||
assertTrue(model4Size.width >= 80 * 10, "Width should accommodate 80 cols");
|
||||
|
||||
// Pack for Model 5 (27 rows, 132 cols)
|
||||
panel.packForModel(27, 132);
|
||||
Dimension model5Size = frame.getSize();
|
||||
assertTrue(model5Size.width >= 132 * 10, "Width should accommodate 132 cols");
|
||||
|
||||
// Set custom size and verify guardedPack leaves the window alone
|
||||
frame.setSize(600, 400);
|
||||
Dimension fixedSize = frame.getSize();
|
||||
|
||||
panel.guardedPack();
|
||||
assertEquals(fixedSize.width, frame.getWidth());
|
||||
assertEquals(fixedSize.height, frame.getHeight());
|
||||
|
||||
frame.dispose();
|
||||
panel.dispose();
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public class BehaviorSettingsTest {
|
||||
private int origReconnectMaxRetries;
|
||||
private boolean origInputMask;
|
||||
private String origInputMaskChar;
|
||||
private boolean origBlockPaste;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
@@ -27,6 +28,7 @@ public class BehaviorSettingsTest {
|
||||
origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries();
|
||||
origInputMask = Settings.getInputMask();
|
||||
origInputMaskChar = Settings.getInputMaskChar();
|
||||
origBlockPaste = Settings.getBlockPaste();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -35,6 +37,7 @@ public class BehaviorSettingsTest {
|
||||
Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries);
|
||||
Settings.setInputMask(origInputMask);
|
||||
Settings.setInputMaskChar(origInputMaskChar);
|
||||
Settings.setBlockPaste(origBlockPaste);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,6 +77,39 @@ public class BehaviorSettingsTest {
|
||||
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 {
|
||||
@@ -173,4 +209,90 @@ public class BehaviorSettingsTest {
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SettingsDialog tabs are organized into Appearance, Behavior, Connection, and Entry Assist")
|
||||
public void testSettingsDialogReorganizedTabsAndGrouping() throws Exception {
|
||||
J3270App app;
|
||||
try {
|
||||
app = new J3270App();
|
||||
} catch (HeadlessException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SettingsDialog dialog = new SettingsDialog(app);
|
||||
|
||||
// Find top-level JTabbedPane
|
||||
JTabbedPane mainTabs = null;
|
||||
for (Component c : dialog.getContentPane().getComponents()) {
|
||||
if (c instanceof JTabbedPane) {
|
||||
mainTabs = (JTabbedPane) c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(mainTabs, "Main JTabbedPane must exist");
|
||||
assertEquals(4, mainTabs.getTabCount(), "Must have 4 main tabs");
|
||||
assertEquals("Appearance", mainTabs.getTitleAt(0));
|
||||
assertEquals("Behavior", mainTabs.getTitleAt(1));
|
||||
assertEquals("Connection", mainTabs.getTitleAt(2));
|
||||
assertEquals("Entry Assist", mainTabs.getTitleAt(3));
|
||||
|
||||
// Verify Appearance subtabs: Display & Font, Colors
|
||||
Component appComp = mainTabs.getComponentAt(0);
|
||||
assertTrue(appComp instanceof JPanel);
|
||||
JTabbedPane appTabs = findTabbedPane((JPanel) appComp);
|
||||
assertNotNull(appTabs, "Appearance panel must have subtabs");
|
||||
assertEquals(2, appTabs.getTabCount());
|
||||
assertEquals("Display & Font", appTabs.getTitleAt(0));
|
||||
assertEquals("Colors", appTabs.getTitleAt(1));
|
||||
|
||||
// Verify Behavior subtabs: General, Keymap
|
||||
Component behComp = mainTabs.getComponentAt(1);
|
||||
assertTrue(behComp instanceof JPanel);
|
||||
JTabbedPane behTabs = findTabbedPane((JPanel) behComp);
|
||||
assertNotNull(behTabs, "Behavior panel must have subtabs");
|
||||
assertEquals(2, behTabs.getTabCount());
|
||||
assertEquals("General", behTabs.getTitleAt(0));
|
||||
assertEquals("Keymap", behTabs.getTitleAt(1));
|
||||
|
||||
// Verify controls exist in SettingsDialog
|
||||
Field cursorStyleField = SettingsDialog.class.getDeclaredField("cursorStyleBox");
|
||||
cursorStyleField.setAccessible(true);
|
||||
assertNotNull(cursorStyleField.get(dialog), "cursorStyleBox must exist");
|
||||
|
||||
Field crosshairField = SettingsDialog.class.getDeclaredField("crosshairRulerCheck");
|
||||
crosshairField.setAccessible(true);
|
||||
assertNotNull(crosshairField.get(dialog), "crosshairRulerCheck must exist");
|
||||
|
||||
Field fourColorField = SettingsDialog.class.getDeclaredField("fourColorOverrideCheck");
|
||||
fourColorField.setAccessible(true);
|
||||
assertNotNull(fourColorField.get(dialog), "fourColorOverrideCheck must exist");
|
||||
|
||||
Field numericLockField = SettingsDialog.class.getDeclaredField("numericFieldLockCheck");
|
||||
numericLockField.setAccessible(true);
|
||||
assertNotNull(numericLockField.get(dialog), "numericFieldLockCheck must exist");
|
||||
|
||||
Field autoSkipField = SettingsDialog.class.getDeclaredField("autoSkipCheck");
|
||||
autoSkipField.setAccessible(true);
|
||||
assertNotNull(autoSkipField.get(dialog), "autoSkipCheck must exist");
|
||||
|
||||
dialog.dispose();
|
||||
app.dispose();
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static JTabbedPane findTabbedPane(Container container) {
|
||||
for (Component c : container.getComponents()) {
|
||||
if (c instanceof JTabbedPane) {
|
||||
return (JTabbedPane) c;
|
||||
}
|
||||
if (c instanceof Container) {
|
||||
JTabbedPane nested = findTabbedPane((Container) c);
|
||||
if (nested != null) return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.InputMethodEvent;
|
||||
import java.text.AttributedString;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class DBCSInputAndClipboardTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Clipboard selection skips DB_RIGHT cells to avoid duplicating wide characters")
|
||||
public void testDBCSClipboardCopyNoDoubling() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'A' at cell 2
|
||||
ExtendedAttribute cell0 = sb.getCell(0);
|
||||
cell0.db = ExtendedAttribute.DB_LEFT;
|
||||
cell0.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell1 = sb.getCell(1);
|
||||
cell1.db = ExtendedAttribute.DB_RIGHT;
|
||||
cell1.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell2 = sb.getCell(2);
|
||||
cell2.db = ExtendedAttribute.DB_NONE;
|
||||
cell2.ucs4 = 'A';
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
// Stream selection covering cells 0..2 (row 0, col 0..2)
|
||||
panel.select(0, 0, 0, 2);
|
||||
String selected = panel.getSelectedText();
|
||||
|
||||
assertEquals("\u3000A", selected, "Stream selection must not duplicate the DBCS character");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Block selection skips DB_RIGHT cells to avoid duplicating wide characters")
|
||||
public void testDBCSBlockSelectionNoDoubling() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
panel.setBlockSelectMode(true);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'B' at cell 2
|
||||
ExtendedAttribute cell0 = sb.getCell(0);
|
||||
cell0.db = ExtendedAttribute.DB_LEFT;
|
||||
cell0.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell1 = sb.getCell(1);
|
||||
cell1.db = ExtendedAttribute.DB_RIGHT;
|
||||
cell1.ucs4 = '\u3000';
|
||||
|
||||
ExtendedAttribute cell2 = sb.getCell(2);
|
||||
cell2.db = ExtendedAttribute.DB_NONE;
|
||||
cell2.ucs4 = 'B';
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
panel.select(0, 0, 0, 2);
|
||||
String selected = panel.getSelectedText();
|
||||
|
||||
assertEquals("\u3000B", selected, "Block selection must not duplicate the DBCS character");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InputMethodRequests returns cursor bounds and processes committed text")
|
||||
public void testDBCSInputMethodRequestsAndEvents() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
assertNotNull(panel.getInputMethodRequests(), "getInputMethodRequests must return this");
|
||||
|
||||
Rectangle loc = panel.getTextLocation(null);
|
||||
assertNotNull(loc, "Text location must not be null");
|
||||
assertTrue(loc.width > 0, "Cursor width must be positive");
|
||||
assertTrue(loc.height > 0, "Cursor height must be positive");
|
||||
|
||||
// Simulate committed IME text '\u3000'
|
||||
AttributedString as = new AttributedString("\u3000");
|
||||
InputMethodEvent imeEvent = new InputMethodEvent(panel, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
|
||||
as.getIterator(), 1, null, null);
|
||||
panel.inputMethodTextChanged(imeEvent);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
|
||||
assertEquals('\u3000', sb.getCell(0).ucs4);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
|
||||
assertEquals('\u3000', sb.getCell(1).ucs4);
|
||||
assertEquals(2, sb.getCursorAddress(), "Cursor should advance by 2 after typing 2-cell DBCS");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InputProcessor backspace over DBCS character deletes both cells")
|
||||
public void testInputProcessorDBCSTypingAndBackspace() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23);
|
||||
config.setCodePage("930");
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
InputProcessor input = client.getInputProcessor();
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
sb.setCursorAddress(0);
|
||||
|
||||
// Type DBCS character
|
||||
input.typeCharacter('\u3000');
|
||||
assertEquals(2, sb.getCursorAddress());
|
||||
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
|
||||
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
|
||||
|
||||
// Backspace over DBCS character
|
||||
input.backspace();
|
||||
assertEquals(0, sb.getCursorAddress(), "Cursor should back up 2 cells");
|
||||
assertEquals(0, sb.getCell(0).ucs4, "Cell 0 should be deleted (null)");
|
||||
assertEquals(0, sb.getCell(1).ucs4, "Cell 1 should be deleted (null)");
|
||||
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(0).db);
|
||||
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(1).db);
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,23 @@ public class FontSettingsPersistenceTest {
|
||||
|
||||
private int originalFontSize;
|
||||
private String originalFontFamily;
|
||||
private int originalUiFontSize;
|
||||
private String originalUiFontFamily;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
originalFontSize = Settings.getFontSize();
|
||||
originalFontFamily = Settings.getFontFamily();
|
||||
originalUiFontSize = Settings.getUiFontSize();
|
||||
originalUiFontFamily = Settings.getUiFontFamily();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setFontSize(originalFontSize);
|
||||
Settings.setFontFamily(originalFontFamily);
|
||||
Settings.setUiFontSize(originalUiFontSize);
|
||||
Settings.setUiFontFamily(originalUiFontFamily);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -39,6 +45,76 @@ public class FontSettingsPersistenceTest {
|
||||
assertEquals(18, Settings.getFontSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUiFontSettingsIndependence() {
|
||||
// Configure terminal fonts
|
||||
Settings.setFontFamily("Courier");
|
||||
Settings.setFontSize(20);
|
||||
|
||||
// Configure UI fonts
|
||||
Settings.setUiFontFamily("Dialog");
|
||||
Settings.setUiFontSize(15);
|
||||
|
||||
// Verify independent getters
|
||||
assertEquals("Courier", Settings.getFontFamily());
|
||||
assertEquals(20, Settings.getFontSize());
|
||||
assertEquals("Dialog", Settings.getUiFontFamily());
|
||||
assertEquals(15, Settings.getUiFontSize());
|
||||
|
||||
// Change UI font and ensure terminal font is untouched
|
||||
Settings.setUiFontSize(18);
|
||||
Settings.setUiFontFamily("SansSerif");
|
||||
assertEquals("Courier", Settings.getFontFamily(), "Terminal font family must not change when UI font changes");
|
||||
assertEquals(20, Settings.getFontSize(), "Terminal font size must not change when UI font changes");
|
||||
assertEquals("SansSerif", Settings.getUiFontFamily());
|
||||
assertEquals(18, Settings.getUiFontSize());
|
||||
|
||||
// Change terminal font and ensure UI font is untouched
|
||||
Settings.setFontSize(24);
|
||||
Settings.setFontFamily("IBM 3270");
|
||||
assertEquals("SansSerif", Settings.getUiFontFamily(), "UI font family must not change when terminal font changes");
|
||||
assertEquals(18, Settings.getUiFontSize(), "UI font size must not change when terminal font changes");
|
||||
assertEquals("IBM 3270", Settings.getFontFamily());
|
||||
assertEquals(24, Settings.getFontSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUiFontIniImportExport() throws Exception {
|
||||
java.io.File tempIni = java.io.File.createTempFile("j3270_ui_font_test", ".ini");
|
||||
tempIni.deleteOnExit();
|
||||
|
||||
try {
|
||||
try (java.io.PrintWriter pw = new java.io.PrintWriter(tempIni)) {
|
||||
pw.println("[appearance]");
|
||||
pw.println("uiFontFamily = Dialog");
|
||||
pw.println("uiFontSize = 16");
|
||||
pw.println("fontFamily = Monospaced");
|
||||
pw.println("fontSize = 22");
|
||||
}
|
||||
|
||||
Settings.loadFromIniFile(tempIni.getAbsolutePath());
|
||||
|
||||
assertEquals("Dialog", Settings.getUiFontFamily());
|
||||
assertEquals(16, Settings.getUiFontSize());
|
||||
assertEquals("Monospaced", Settings.getFontFamily());
|
||||
assertEquals(22, Settings.getFontSize());
|
||||
|
||||
// Export to another file and check contents
|
||||
java.io.File exportIni = java.io.File.createTempFile("j3270_ui_font_export", ".ini");
|
||||
exportIni.deleteOnExit();
|
||||
|
||||
Settings.exportToIniFile(exportIni.getAbsolutePath());
|
||||
String exported = new String(java.nio.file.Files.readAllBytes(exportIni.toPath()));
|
||||
|
||||
assertTrue(exported.contains("uiFontFamily = Dialog"), "Exported INI must contain uiFontFamily");
|
||||
assertTrue(exported.contains("uiFontSize = 16"), "Exported INI must contain uiFontSize");
|
||||
assertTrue(exported.contains("fontFamily = Monospaced"), "Exported INI must contain terminal fontFamily");
|
||||
assertTrue(exported.contains("fontSize = 22"), "Exported INI must contain terminal fontSize");
|
||||
} finally {
|
||||
tempIni.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTerminalPanelResizeDoesNotResetFontSize() {
|
||||
try {
|
||||
@@ -77,4 +153,57 @@ public class FontSettingsPersistenceTest {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusBarXSystemAndXProt() {
|
||||
try {
|
||||
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_2, false);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
|
||||
|
||||
StatusBar statusBar = new StatusBar();
|
||||
statusBar.setClient(client, null);
|
||||
statusBar.updateStatus();
|
||||
|
||||
// Initially not locked
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 1. Setup protected field and type into it -> triggers X PROT
|
||||
client.getScreenBuffer().setFieldAttribute(0, (byte) haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT);
|
||||
client.getScreenBuffer().setCursorAddress(1);
|
||||
client.typeCharacter('A');
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X PROT", statusBar.getLockStatusText());
|
||||
|
||||
// 2. Reset -> clears X PROT
|
||||
client.reset();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 3. Send Enter -> triggers X SYSTEM
|
||||
client.sendEnter();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X SYSTEM", statusBar.getLockStatusText());
|
||||
|
||||
// 4. Host unlocks keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
statusBar.updateStatus();
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 5. Subsequent Enter -> triggers X SYSTEM again
|
||||
client.sendEnter();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X SYSTEM", statusBar.getLockStatusText());
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TerminalPanelSelectionTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
|
||||
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
|
||||
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
|
||||
String headerText = " ISPF PRIMARY OPTION MENU";
|
||||
for (int i = 0; i < headerText.length(); i++) {
|
||||
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
|
||||
sb.getCell(1 + i).ec = (byte) (0x40 + i);
|
||||
}
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
// Cell dimensions in panel
|
||||
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
|
||||
MouseListener[] mouseListeners = panel.getMouseListeners();
|
||||
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
|
||||
assertTrue(mouseListeners.length > 0);
|
||||
assertTrue(motionListeners.length > 0);
|
||||
|
||||
MouseListener ml = mouseListeners[0];
|
||||
MouseMotionListener mml = motionListeners[0];
|
||||
|
||||
// 1. Mouse Pressed at cell (row 0, col 2)
|
||||
int startX = ox + 2 * cellW + cellW / 2;
|
||||
int startY = oy + cellH / 2;
|
||||
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, startX, startY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mousePressed(pressEvent);
|
||||
|
||||
// 2. Drag to cell (row 0, col 15)
|
||||
int dragX = ox + 15 * cellW + cellW / 2;
|
||||
int dragY = oy + cellH / 2;
|
||||
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
|
||||
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
|
||||
mml.mouseDragged(dragEvent);
|
||||
|
||||
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
|
||||
|
||||
// 3. Mouse Released at cell (row 0, col 15)
|
||||
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mouseReleased(releaseEvent);
|
||||
|
||||
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
|
||||
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
|
||||
String selected = panel.getSelectedText();
|
||||
assertNotNull(selected);
|
||||
assertTrue(selected.length() > 0, "Selected text must not be empty");
|
||||
|
||||
// 4. A subsequent single click without drag should clear the selection and move the cursor
|
||||
int clickX = ox + 5 * cellW + cellW / 2;
|
||||
int clickY = oy + cellH / 2;
|
||||
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mousePressed(clickPress);
|
||||
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mouseReleased(clickRelease);
|
||||
|
||||
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
|
||||
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
|
||||
|
||||
} catch (HeadlessException e) {
|
||||
// Handled gracefully in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Mouse press and release on TerminalPanel requests focus")
|
||||
public void testTerminalPanelMousePressRequestsFocus() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
assertTrue(panel.isFocusable());
|
||||
|
||||
MouseListener[] mouseListeners = panel.getMouseListeners();
|
||||
assertTrue(mouseListeners.length > 0);
|
||||
|
||||
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, 50, 50, 1, false, MouseEvent.BUTTON1);
|
||||
mouseListeners[0].mousePressed(pressEvent);
|
||||
|
||||
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, 50, 50, 1, false, MouseEvent.BUTTON1);
|
||||
mouseListeners[0].mouseReleased(releaseEvent);
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,4 +262,81 @@ public class ThemeManagerTest {
|
||||
// Handled in headless CI
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUiFontRetrievalAndDefaults() {
|
||||
String prevFamily = Settings.getUiFontFamily();
|
||||
int prevSize = Settings.getUiFontSize();
|
||||
|
||||
try {
|
||||
Settings.setUiFontFamily("Dialog");
|
||||
Settings.setUiFontSize(16);
|
||||
|
||||
Font uiFont = ThemeManager.getUiFont();
|
||||
assertEquals("Dialog", uiFont.getFamily());
|
||||
assertEquals(16, uiFont.getSize());
|
||||
assertEquals(Font.PLAIN, uiFont.getStyle());
|
||||
|
||||
Font boldOffset = ThemeManager.getUiFont(Font.BOLD, 2);
|
||||
assertEquals(18, boldOffset.getSize());
|
||||
assertEquals(Font.BOLD, boldOffset.getStyle());
|
||||
|
||||
Font monoFont = ThemeManager.getMonospacedUiFont();
|
||||
assertEquals(Font.MONOSPACED, monoFont.getName());
|
||||
assertEquals(16, monoFont.getSize());
|
||||
|
||||
// Test applyUIManagerDefaults sets font properties
|
||||
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
|
||||
assertEquals(uiFont, UIManager.get("Button.font"));
|
||||
assertEquals(uiFont, UIManager.get("Label.font"));
|
||||
assertEquals(uiFont, UIManager.get("Menu.font"));
|
||||
assertEquals(uiFont, UIManager.get("Table.font"));
|
||||
assertEquals(monoFont, UIManager.get("TextArea.font"));
|
||||
} finally {
|
||||
Settings.setUiFontFamily(prevFamily);
|
||||
Settings.setUiFontSize(prevSize);
|
||||
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLabelFontNotIncrementedOnRepeatedThemeApplication() {
|
||||
String prevFamily = Settings.getUiFontFamily();
|
||||
int prevSize = Settings.getUiFontSize();
|
||||
|
||||
try {
|
||||
Settings.setUiFontFamily("Dialog");
|
||||
Settings.setUiFontSize(16);
|
||||
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
|
||||
|
||||
JLabel label = new JLabel("Settings Label");
|
||||
ThemeManager.applyTheme(label);
|
||||
assertEquals(16, label.getFont().getSize(), "Initial label font size must match configured uiFontSize");
|
||||
|
||||
// Repeated theme applications (e.g. subsequent clicks of 'Apply' in Settings dialog)
|
||||
// must never increment font size
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
ThemeManager.applyTheme(label);
|
||||
assertEquals(16, label.getFont().getSize(),
|
||||
"Label font size must remain 16 on subsequent application #" + i);
|
||||
}
|
||||
|
||||
// Test label with custom delta
|
||||
JLabel headerLabel = new JLabel("Header Label");
|
||||
headerLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3));
|
||||
headerLabel.putClientProperty("uiFontSizeDelta", 3);
|
||||
ThemeManager.applyTheme(headerLabel);
|
||||
assertEquals(19, headerLabel.getFont().getSize(), "Custom header label should have base + 3 size");
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
ThemeManager.applyTheme(headerLabel);
|
||||
assertEquals(19, headerLabel.getFont().getSize(),
|
||||
"Header label font size must remain 19 on subsequent application #" + i);
|
||||
}
|
||||
} finally {
|
||||
Settings.setUiFontFamily(prevFamily);
|
||||
Settings.setUiFontSize(prevSize);
|
||||
ThemeManager.applyUIManagerDefaults(UITheme.DARK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Communications event matching IBM Host On-Demand ECLCommEvent.
|
||||
*/
|
||||
public class ECLCommEvent extends ECLEvent {
|
||||
|
||||
private final boolean status;
|
||||
private final int eventType;
|
||||
|
||||
public ECLCommEvent(Object object, boolean status) {
|
||||
super(object);
|
||||
this.status = status;
|
||||
this.eventType = status ? 1 : 2;
|
||||
}
|
||||
|
||||
public ECLCommEvent(Object object, int eventType, boolean status) {
|
||||
super(object);
|
||||
this.eventType = eventType;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public boolean connected() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public int getEventType() {
|
||||
return this.eventType;
|
||||
}
|
||||
|
||||
public ECLConnection GetConnection() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLConnection) return (ECLConnection) src;
|
||||
if (src instanceof haus.nightmare.lib3270j.ecl.ECLConnection) {
|
||||
return new ECLConnection((haus.nightmare.lib3270j.ecl.ECLConnection) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Communications notification interface matching IBM Host On-Demand ECLCommNotify.
|
||||
*/
|
||||
public interface ECLCommNotify {
|
||||
void NotifyEvent(ECLConnection conn, boolean connected);
|
||||
default void NotifyStop(ECLConnection conn, int reason) {}
|
||||
default void NotifyError(ECLConnection conn, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Communications and connection management facade matching IBM Host On-Demand ECLConnection.
|
||||
*/
|
||||
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection implements ECLConstants {
|
||||
|
||||
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
|
||||
|
||||
public ECLConnection() {
|
||||
super();
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(Properties props) {
|
||||
super(props);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(String host, int port) {
|
||||
super(host, port);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) {
|
||||
super(session);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLConnection delegate) {
|
||||
super(delegate != null ? delegate.getSession() : null);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
private void setupListener() {
|
||||
RegisterCommEvent(new haus.nightmare.lib3270j.ecl.ECLCommListener() {
|
||||
@Override
|
||||
public void CommNotifyEvent(haus.nightmare.lib3270j.ecl.ECLCommEvent event) {
|
||||
boolean conn = event != null && event.isConnected();
|
||||
for (ECLCommNotify n : commNotifies) {
|
||||
try {
|
||||
n.NotifyEvent(ECLConnection.this, conn);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void RegisterCommEvent(ECLCommNotify notify) {
|
||||
if (notify != null && !commNotifies.contains(notify)) {
|
||||
commNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerCommEvent(ECLCommNotify notify) { RegisterCommEvent(notify); }
|
||||
|
||||
public void UnregisterCommEvent(ECLCommNotify notify) {
|
||||
if (notify != null) {
|
||||
commNotifies.remove(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
|
||||
|
||||
@Override
|
||||
public void StartCommunication() throws IOException {
|
||||
super.StartCommunication();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StopCommunication() {
|
||||
super.StopCommunication();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* IBM Host Access Class Library (HACL) common constants.
|
||||
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLConstants.
|
||||
*/
|
||||
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
|
||||
|
||||
// Presentation space logical planes (HoD bitmask values)
|
||||
int TEXT_PLANE = 1;
|
||||
int COLOR_PLANE = 2;
|
||||
int DBCS_PLANE = 4;
|
||||
int GRID_PLANE = 8;
|
||||
int FIELD_PLANE = 16;
|
||||
int EXFIELD_PLANE = 32;
|
||||
int NLS_PLANE = 64;
|
||||
int HOST_PLANE = 128;
|
||||
int RAWTEXT_PLANE = 256;
|
||||
int COLOR_ATTRIBUTES_PLANE = 512;
|
||||
int ALL_PLANES = 319;
|
||||
|
||||
// Search directions
|
||||
int SEARCH_FORWARD = 1;
|
||||
int SEARCH_BACKWARD = 2;
|
||||
|
||||
// Field filters
|
||||
int ALL_FIELDS = 0;
|
||||
int MODIFIED_FIELD = 1;
|
||||
int ALPHANUM_FIELD = 2;
|
||||
int HILIGHT_FIELD = 16;
|
||||
int PROTECTED_FIELD = 32;
|
||||
int DISPLAYABLE_FIELD = 64;
|
||||
int PENDETECTABLE_FIELD = 128;
|
||||
int UNMODIFIED_FIELD = 256;
|
||||
int NUMERIC_FIELD = 512;
|
||||
int LOLIGHT_FIELD = 4096;
|
||||
int UNPROTECTED_FIELD = 8192;
|
||||
int NON_DISPLAYABLE_FIELD = 16384;
|
||||
int NON_PENDETECTABLE_FIELD = 32768;
|
||||
|
||||
// Standard Mnemonics Constants
|
||||
String ENTER_STR = "[enter]";
|
||||
String CLEAR_STR = "[clear]";
|
||||
String RESET_STR = "[reset]";
|
||||
String TAB_STR = "[tab]";
|
||||
String FWDTAB_STR = "[tab]";
|
||||
String BACKTAB_STR = "[backtab]";
|
||||
String BACKSP_STR = "[backspace]";
|
||||
String DELCHAR_STR = "[delete]";
|
||||
String INSERT_STR = "[insert]";
|
||||
String HOME_STR = "[home]";
|
||||
String NEWLINE_STR = "[newline]";
|
||||
String ENDLINE_STR = "[eof]";
|
||||
String ERASEEOF_STR = "[eraseeof]";
|
||||
String ERASEFLD_STR = "[erasefld]";
|
||||
String ERASEINPUT_STR = "[erinp]";
|
||||
String ATTN_STR = "[attn]";
|
||||
String SYSREQ_STR = "[sysreq]";
|
||||
String DUP_STR = "[dup]";
|
||||
String FLDMRK_STR = "[fieldmark]";
|
||||
String PA1_STR = "[pa1]";
|
||||
String PA2_STR = "[pa2]";
|
||||
String PA3_STR = "[pa3]";
|
||||
String CURUP_STR = "[up]";
|
||||
String CURDOWN_STR = "[down]";
|
||||
String CURLEFT_STR = "[left]";
|
||||
String CURRIGHT_STR = "[right]";
|
||||
String PAGEUP_STR = "[pageup]";
|
||||
String PAGEDWN_STR = "[pagedn]";
|
||||
String COPY_STR = "[copy]";
|
||||
String PASTE_STR = "[paste]";
|
||||
String CUT_STR = "[cut]";
|
||||
String SELECTALL_STR = "[selectall]";
|
||||
|
||||
// Function keys F1 - F24
|
||||
String F1_STR = "[pf1]";
|
||||
String F2_STR = "[pf2]";
|
||||
String F3_STR = "[pf3]";
|
||||
String F4_STR = "[pf4]";
|
||||
String F5_STR = "[pf5]";
|
||||
String F6_STR = "[pf6]";
|
||||
String F7_STR = "[pf7]";
|
||||
String F8_STR = "[pf8]";
|
||||
String F9_STR = "[pf9]";
|
||||
String F10_STR = "[pf10]";
|
||||
String F11_STR = "[pf11]";
|
||||
String F12_STR = "[pf12]";
|
||||
String F13_STR = "[pf13]";
|
||||
String F14_STR = "[pf14]";
|
||||
String F15_STR = "[pf15]";
|
||||
String F16_STR = "[pf16]";
|
||||
String F17_STR = "[pf17]";
|
||||
String F18_STR = "[pf18]";
|
||||
String F19_STR = "[pf19]";
|
||||
String F20_STR = "[pf20]";
|
||||
String F21_STR = "[pf21]";
|
||||
String F22_STR = "[pf22]";
|
||||
String F23_STR = "[pf23]";
|
||||
String F24_STR = "[pf24]";
|
||||
|
||||
// Shift F1 - F24
|
||||
String SHIFT_F1_STR = "[Spf1]";
|
||||
String SHIFT_F2_STR = "[Spf2]";
|
||||
String SHIFT_F3_STR = "[Spf3]";
|
||||
String SHIFT_F4_STR = "[Spf4]";
|
||||
String SHIFT_F5_STR = "[Spf5]";
|
||||
String SHIFT_F6_STR = "[Spf6]";
|
||||
String SHIFT_F7_STR = "[Spf7]";
|
||||
String SHIFT_F8_STR = "[Spf8]";
|
||||
String SHIFT_F9_STR = "[Spf9]";
|
||||
String SHIFT_F10_STR = "[Spf10]";
|
||||
String SHIFT_F11_STR = "[Spf11]";
|
||||
String SHIFT_F12_STR = "[Spf12]";
|
||||
String SHIFT_F13_STR = "[Spf13]";
|
||||
String SHIFT_F14_STR = "[Spf14]";
|
||||
String SHIFT_F15_STR = "[Spf15]";
|
||||
String SHIFT_F16_STR = "[Spf16]";
|
||||
String SHIFT_F17_STR = "[Spf17]";
|
||||
String SHIFT_F18_STR = "[Spf18]";
|
||||
String SHIFT_F19_STR = "[Spf19]";
|
||||
String SHIFT_F20_STR = "[Spf20]";
|
||||
|
||||
// Field attributes
|
||||
short ATTR_PROTECTED = 32;
|
||||
short ATTR_ALPHANUM = 16;
|
||||
short ATTR_DISPLAY1 = 8;
|
||||
short ATTR_DISPLAY2 = 4;
|
||||
short ATTR_MDT = 1;
|
||||
|
||||
// Extended attributes
|
||||
short EXT_ATTR_3270_UNDERLINE = 192;
|
||||
short EXT_ATTR_3270_REVERSE = 128;
|
||||
short EXT_ATTR_3270_BLINK = 64;
|
||||
short EXT_ATTR_3270_EBCDIC = 0;
|
||||
|
||||
// Timeout
|
||||
int NO_TIMEOUT = -1;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* IBM Host Access Class Library (HACL) ECLErr checked exception facade.
|
||||
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLErr.
|
||||
*/
|
||||
public class ECLErr extends haus.nightmare.lib3270j.ecl.ECLErr {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLErr() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLErr(String text) {
|
||||
super(text);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum) {
|
||||
super(location, msgNum);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert) {
|
||||
super(location, msgNum, insert);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2) {
|
||||
super(location, msgNum, insert1, insert2);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
|
||||
super(location, msgNum, insert1, insert2, log);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
|
||||
super(location, msgNum, insert1, insert2, insert3);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, int reasonCode) {
|
||||
super(location, msgNum, reasonCode);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
|
||||
super(location, msgNum, insert1, reasonCode);
|
||||
}
|
||||
|
||||
public ECLErr(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ECLErr(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Discrete 3270 field facade matching IBM Host On-Demand ECLField.
|
||||
*/
|
||||
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements ECLConstants {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLField delegate;
|
||||
|
||||
public ECLField(haus.nightmare.lib3270j.ecl.ECLField delegate) {
|
||||
super(delegate != null ? delegate.getPS() : null,
|
||||
delegate != null ? delegate.getStart() : 0,
|
||||
delegate != null ? delegate.getDataStart() : 0,
|
||||
delegate != null ? delegate.getEnd() : 0,
|
||||
delegate != null ? delegate.getLength() : 0,
|
||||
(byte) (delegate != null ? delegate.getAttribute() : 0));
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public int GetStart() {
|
||||
return delegate != null ? delegate.GetStart() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStart() { return GetStart(); }
|
||||
|
||||
public int GetStartRow() {
|
||||
return delegate != null ? delegate.GetStartRow() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStartRow() { return GetStartRow(); }
|
||||
|
||||
public int GetStartCol() {
|
||||
return delegate != null ? delegate.GetStartCol() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStartCol() { return GetStartCol(); }
|
||||
|
||||
public int GetEnd() {
|
||||
return delegate != null ? delegate.GetEnd() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEnd() { return GetEnd(); }
|
||||
|
||||
public int GetEndRow() {
|
||||
return delegate != null ? delegate.GetEndRow() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEndRow() { return GetEndRow(); }
|
||||
|
||||
public int GetEndCol() {
|
||||
return delegate != null ? delegate.GetEndCol() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEndCol() { return GetEndCol(); }
|
||||
|
||||
public int GetLength() {
|
||||
return delegate != null ? delegate.GetLength() : 0;
|
||||
}
|
||||
|
||||
public int getLength() { return GetLength(); }
|
||||
|
||||
public String GetString() {
|
||||
return delegate != null ? delegate.getString() : "";
|
||||
}
|
||||
|
||||
public String getString() { return GetString(); }
|
||||
|
||||
public int GetString(char[] buffer, int len) throws ECLErr {
|
||||
if (buffer == null) throw new ECLErr("ECLField:GetString", "ECL0011", "1");
|
||||
String text = GetString();
|
||||
int count = Math.min(len, Math.min(buffer.length, text.length()));
|
||||
for (int i = 0; i < count; i++) {
|
||||
buffer[i] = text.charAt(i);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public void SetString(String text) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
delegate.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetText(String text) throws ECLErr {
|
||||
SetString(text);
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
if (delegate != null) {
|
||||
delegate.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean IsModified() {
|
||||
return delegate != null && delegate.isModified();
|
||||
}
|
||||
|
||||
public boolean isModified() { return IsModified(); }
|
||||
|
||||
public boolean IsProtected() {
|
||||
return delegate != null && delegate.isProtected();
|
||||
}
|
||||
|
||||
public boolean isProtected() { return IsProtected(); }
|
||||
|
||||
public boolean IsNumeric() {
|
||||
return delegate != null && delegate.isNumeric();
|
||||
}
|
||||
|
||||
public boolean isNumeric() { return IsNumeric(); }
|
||||
|
||||
public boolean IsHighIntensity() {
|
||||
return delegate != null && delegate.isHighIntensity();
|
||||
}
|
||||
|
||||
public boolean isHighIntensity() { return IsHighIntensity(); }
|
||||
|
||||
public boolean IsPenDetectable() {
|
||||
return delegate != null && delegate.isPenDetectable();
|
||||
}
|
||||
|
||||
public boolean isPenDetectable() { return IsPenDetectable(); }
|
||||
|
||||
public boolean IsDisplay() {
|
||||
return delegate != null && delegate.isDisplay();
|
||||
}
|
||||
|
||||
public boolean isDisplay() { return IsDisplay(); }
|
||||
|
||||
public short GetAttribute() {
|
||||
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
|
||||
}
|
||||
|
||||
public boolean IsDBCS() {
|
||||
return delegate != null && delegate.isDBCS();
|
||||
}
|
||||
|
||||
public boolean isDBCS() { return IsDBCS(); }
|
||||
|
||||
public boolean IsDBCSOnly() {
|
||||
return delegate != null && delegate.isDBCSOnly();
|
||||
}
|
||||
|
||||
public boolean isDBCSOnly() { return IsDBCSOnly(); }
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Collection of 3270 fields matching IBM Host On-Demand ECLFieldList.
|
||||
*/
|
||||
public class ECLFieldList extends Vector<ECLField> implements ECLConstants {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLFieldList delegate;
|
||||
private final ECLPS ps;
|
||||
|
||||
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate, ECLPS ps) {
|
||||
this.delegate = delegate;
|
||||
this.ps = ps;
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate) {
|
||||
this(delegate, null);
|
||||
}
|
||||
|
||||
private void refreshFields() {
|
||||
clear();
|
||||
if (delegate != null) {
|
||||
delegate.refresh();
|
||||
for (haus.nightmare.lib3270j.ecl.ECLField f : delegate.getFields()) {
|
||||
add(new ECLField(f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh() {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public void Refresh(int planes) throws ECLErr {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public int GetFieldCount() {
|
||||
return size();
|
||||
}
|
||||
|
||||
public int getFieldCount() { return GetFieldCount(); }
|
||||
|
||||
public ECLField GetFirstField() {
|
||||
return GetFirstField(0);
|
||||
}
|
||||
|
||||
public ECLField GetFirstField(int filter) {
|
||||
for (ECLField f : this) {
|
||||
if (matchesFilter(f, filter)) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField GetNextField(ECLField current) {
|
||||
return GetNextField(current, 0);
|
||||
}
|
||||
|
||||
public ECLField GetNextField(ECLField current, int filter) {
|
||||
if (current == null) return GetFirstField(filter);
|
||||
int idx = indexOf(current);
|
||||
if (idx >= 0) {
|
||||
for (int i = idx + 1; i < size(); i++) {
|
||||
ECLField f = get(i);
|
||||
if (matchesFilter(f, filter)) return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField FindField(int pos) throws ECLErr {
|
||||
if (ps != null && (pos < 1 || pos > ps.GetSize())) {
|
||||
throw new ECLErr("ECLFieldList:FindField", "ECL0010", "1", String.valueOf(pos));
|
||||
}
|
||||
for (ECLField f : this) {
|
||||
int start = f.GetStart();
|
||||
int end = f.GetEnd();
|
||||
if (start <= end) {
|
||||
if (pos >= start && pos <= end) return f;
|
||||
} else {
|
||||
// Wrapped field
|
||||
if (pos >= start || pos <= end) return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField FindField(int row, int col) throws ECLErr {
|
||||
if (ps == null) return null;
|
||||
int pos = ps.ConvertRowColToPos(row, col);
|
||||
return FindField(pos);
|
||||
}
|
||||
|
||||
private boolean matchesFilter(ECLField f, int filter) {
|
||||
if (filter == 0 || filter == ALL_FIELDS) return true;
|
||||
if ((filter & MODIFIED_FIELD) != 0 && !f.IsModified()) return false;
|
||||
if ((filter & PROTECTED_FIELD) != 0 && !f.IsProtected()) return false;
|
||||
if ((filter & UNPROTECTED_FIELD) != 0 && f.IsProtected()) return false;
|
||||
if ((filter & NUMERIC_FIELD) != 0 && !f.IsNumeric()) return false;
|
||||
if ((filter & HILIGHT_FIELD) != 0 && !f.IsHighIntensity()) return false;
|
||||
if ((filter & DISPLAYABLE_FIELD) != 0 && !f.IsDisplay()) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Host Print Session facade matching IBM Host On-Demand ECLHostPrintSession.
|
||||
*/
|
||||
public class ECLHostPrintSession extends ECLSession {
|
||||
|
||||
public static final String SESSION_PRINT_SESSION_LABEL_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_TYPE_DEFAULT = "5";
|
||||
public static final String SESSION_PRINT_SESSION_HOST_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_HOST_PORT_DEFAULT = "23";
|
||||
public static final String SESSION_PRINT_SESSION_LU_NAME_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_AUTOCONNECT_DEFAULT = "true";
|
||||
public static final String SESSION_PRINT_SESSION_AUTORECONNECT_DEFAULT = "true";
|
||||
public static final String SESSION_LU_TYPE = "printerLUType";
|
||||
|
||||
public ECLHostPrintSession(Properties properties) throws ECLErr {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public ECLHostPrintSession() throws ECLErr {
|
||||
super();
|
||||
}
|
||||
|
||||
public void cancelPrintJob() {
|
||||
ECLPS ps = GetPS();
|
||||
if (ps != null) {
|
||||
try {
|
||||
ps.SendKeys("[cancelpjob]");
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelPrintJob() {
|
||||
cancelPrintJob();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Operator Information Area facade matching IBM Host On-Demand ECLOIA.
|
||||
*/
|
||||
public class ECLOIA implements ECLConstants {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLOIA delegate;
|
||||
private final List<ECLOIANotify> oiaNotifies = new CopyOnWriteArrayList<>();
|
||||
|
||||
public ECLOIA(haus.nightmare.lib3270j.ecl.ECLOIA delegate) {
|
||||
this.delegate = delegate;
|
||||
if (delegate != null) {
|
||||
delegate.RegisterOIAEvent(new haus.nightmare.lib3270j.ecl.ECLOIAListener() {
|
||||
@Override
|
||||
public void OIANotifyEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent event) {
|
||||
ECLOIAEvent facadeEvt = new ECLOIAEvent(event);
|
||||
for (ECLOIANotify n : oiaNotifies) {
|
||||
try {
|
||||
n.OIANotifyEvent(facadeEvt);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterOIAEvent(ECLOIANotify notify) {
|
||||
if (notify != null && !oiaNotifies.contains(notify)) {
|
||||
oiaNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerOIAEvent(ECLOIANotify notify) { RegisterOIAEvent(notify); }
|
||||
|
||||
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
|
||||
if (delegate != null) {
|
||||
delegate.RegisterOIAEvent(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterOIAEvent(ECLOIANotify notify) {
|
||||
if (notify != null) {
|
||||
oiaNotifies.remove(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterOIAEvent(ECLOIANotify notify) { UnregisterOIAEvent(notify); }
|
||||
|
||||
public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
|
||||
if (delegate != null) {
|
||||
delegate.UnregisterOIAEvent(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean IsAlphanumeric() {
|
||||
return delegate != null && delegate.IsAlphanumeric();
|
||||
}
|
||||
|
||||
public boolean isAlphanumeric() { return IsAlphanumeric(); }
|
||||
|
||||
public boolean IsNumeric() {
|
||||
return delegate != null && delegate.IsNumeric();
|
||||
}
|
||||
|
||||
public boolean isNumeric() { return IsNumeric(); }
|
||||
|
||||
public boolean IsDBCS() {
|
||||
return delegate != null && delegate.IsDBCS();
|
||||
}
|
||||
|
||||
public boolean isDBCS() { return IsDBCS(); }
|
||||
|
||||
public boolean IsInsertMode() {
|
||||
return delegate != null && delegate.IsInsertMode();
|
||||
}
|
||||
|
||||
public boolean isInsertMode() { return IsInsertMode(); }
|
||||
|
||||
public boolean IsCommError() {
|
||||
return delegate != null && delegate.IsCommError();
|
||||
}
|
||||
|
||||
public boolean isCommError() { return IsCommError(); }
|
||||
|
||||
public boolean IsKeyLocked() {
|
||||
return delegate != null && delegate.isKeyLocked();
|
||||
}
|
||||
|
||||
public boolean isKeyLocked() { return IsKeyLocked(); }
|
||||
|
||||
public boolean IsMessageWaiting() {
|
||||
return delegate != null && delegate.IsMessageWaiting();
|
||||
}
|
||||
|
||||
public boolean isMessageWaiting() { return IsMessageWaiting(); }
|
||||
|
||||
public int GetInputInhibited() {
|
||||
return delegate != null ? delegate.getInputInhibited() : 0;
|
||||
}
|
||||
|
||||
public int getInputInhibited() { return GetInputInhibited(); }
|
||||
|
||||
public int GetAlphanumericType() {
|
||||
return delegate != null ? delegate.getAlphanumericType() : 0;
|
||||
}
|
||||
|
||||
public int getAlphanumericType() { return GetAlphanumericType(); }
|
||||
|
||||
public boolean WaitForInput(long timeout) {
|
||||
return delegate != null && delegate.WaitForInput(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForSystemAvailable(long timeout) {
|
||||
return delegate != null && delegate.WaitForSystemAvailable(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForAppAvailable(long timeout) {
|
||||
return delegate != null && delegate.WaitForAppAvailable(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForTransition(long timeout) {
|
||||
return delegate != null && delegate.WaitForTransition(timeout);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLOIA getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Event object dispatched on Operator Information Area (ECLOIA) status changes matching IBM HoD.
|
||||
*/
|
||||
public class ECLOIAEvent extends ECLEvent {
|
||||
|
||||
public String data;
|
||||
public long newState;
|
||||
public long changedMask;
|
||||
|
||||
private int inputInhibited = 0;
|
||||
private int alphanumericType = 0;
|
||||
private boolean insertMode = false;
|
||||
|
||||
public ECLOIAEvent(Object source, long newState, long changedMask, String data) {
|
||||
super(source);
|
||||
this.newState = newState;
|
||||
this.changedMask = changedMask;
|
||||
this.data = data != null ? data : "";
|
||||
}
|
||||
|
||||
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
|
||||
boolean insertMode, String statusString) {
|
||||
super(source);
|
||||
this.inputInhibited = inputInhibited;
|
||||
this.alphanumericType = alphanumericType;
|
||||
this.insertMode = insertMode;
|
||||
this.data = statusString != null ? statusString : "";
|
||||
this.newState = inputInhibited;
|
||||
this.changedMask = eventType;
|
||||
}
|
||||
|
||||
public ECLOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent delegate) {
|
||||
super(delegate != null ? delegate.getSource() : null);
|
||||
if (delegate != null) {
|
||||
this.inputInhibited = delegate.getInputInhibited();
|
||||
this.alphanumericType = delegate.getAlphanumericType();
|
||||
this.insertMode = delegate.isInsertMode();
|
||||
this.data = delegate.getStatusString();
|
||||
this.newState = delegate.getInputInhibited();
|
||||
this.changedMask = delegate.getEventType();
|
||||
}
|
||||
}
|
||||
|
||||
public ECLOIA GetOIA() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLOIA) {
|
||||
return (ECLOIA) src;
|
||||
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLOIA) {
|
||||
return new ECLOIA((haus.nightmare.lib3270j.ecl.ECLOIA) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLOIA getOIA() { return GetOIA(); }
|
||||
|
||||
public int GetInputInhibited() { return inputInhibited; }
|
||||
public int getInputInhibited() { return inputInhibited; }
|
||||
|
||||
public int GetAlphanumericType() { return alphanumericType; }
|
||||
public int getAlphanumericType() { return alphanumericType; }
|
||||
|
||||
public boolean IsInsertMode() { return insertMode; }
|
||||
public boolean isInsertMode() { return insertMode; }
|
||||
|
||||
public boolean IsInputInhibited() { return inputInhibited != 0; }
|
||||
public boolean isInputInhibited() { return IsInputInhibited(); }
|
||||
|
||||
public String GetStatusString() { return data; }
|
||||
public String getStatusString() { return data; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ECLOIAEvent: changedMask=0x" + Long.toHexString(this.changedMask) +
|
||||
", newState=0x" + Long.toHexString(this.newState) + ", data=" + this.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Operator Information Area notification interface matching IBM Host On-Demand ECLOIANotify.
|
||||
*/
|
||||
public interface ECLOIANotify {
|
||||
void NotifyEvent(ECLOIA oia, int inhibitReason, int state, String text);
|
||||
default void NotifyStop(ECLOIA oia, int reason) {}
|
||||
default void NotifyError(ECLOIA oia, ECLErr err) {}
|
||||
default void OIANotifyStop(ECLOIA oia, int reason) { NotifyStop(oia, reason); }
|
||||
default void OIANotifyError(ECLOIA oia, ECLErr err) { NotifyError(oia, err); }
|
||||
default void OIANotifyEvent(ECLOIAEvent event) {
|
||||
if (event != null) {
|
||||
NotifyEvent(event.GetOIA(), event.GetInputInhibited(), 0, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLPSListener;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Presentation Space facade matching IBM Host On-Demand ECLPS.
|
||||
* Provides 1-based indexing, method signature parity, and checked ECLErr exception semantics.
|
||||
*/
|
||||
public class ECLPS implements ECLConstants, Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLPS delegate;
|
||||
|
||||
public ECLPS(haus.nightmare.lib3270j.ecl.ECLPS delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public ECLPS(haus.nightmare.lib3270j.ecl.ECLSession session) {
|
||||
this(session != null ? session.GetPS() : null);
|
||||
}
|
||||
|
||||
public ECLPS(ECLSession session) {
|
||||
this(session != null ? session.getDelegate() : null);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLPS getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
// ========== Dimensions & Position Conversion (1-based) ==========
|
||||
|
||||
public int GetSize() {
|
||||
return delegate != null ? delegate.GetSize() : 0;
|
||||
}
|
||||
|
||||
public int getSize() { return GetSize(); }
|
||||
|
||||
public int GetRows() {
|
||||
return delegate != null ? delegate.GetRows() : 0;
|
||||
}
|
||||
|
||||
public int getRows() { return GetRows(); }
|
||||
|
||||
public int GetCols() {
|
||||
return delegate != null ? delegate.GetCols() : 0;
|
||||
}
|
||||
|
||||
public int getCols() { return GetCols(); }
|
||||
|
||||
public int GetSizeRows() { return GetRows(); }
|
||||
public int GetSizeCols() { return GetCols(); }
|
||||
|
||||
public int ConvertRowColToPos(int row, int col) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertRowColToPos(row, col);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertRowColToPos(int row, int col) throws ECLErr {
|
||||
return ConvertRowColToPos(row, col);
|
||||
}
|
||||
|
||||
public int ConvertPosToRow(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToRow(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertPosToRow(int pos) throws ECLErr {
|
||||
return ConvertPosToRow(pos);
|
||||
}
|
||||
|
||||
public int ConvertPosToCol(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToCol(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertPosToCol(int pos) throws ECLErr {
|
||||
return ConvertPosToCol(pos);
|
||||
}
|
||||
|
||||
public void ConvertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.ConvertPosToRowCol(pos, rowCol);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void convertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
|
||||
ConvertPosToRowCol(pos, rowCol);
|
||||
}
|
||||
|
||||
public int[] ConvertPosToRowCol(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToRowCol(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return new int[]{1, 1};
|
||||
}
|
||||
|
||||
public int[] convertPosToRowCol(int pos) throws ECLErr {
|
||||
return ConvertPosToRowCol(pos);
|
||||
}
|
||||
|
||||
// ========== Cursor Operations (1-based) ==========
|
||||
|
||||
public int GetCursorPos() {
|
||||
return delegate != null ? delegate.GetCursorPos() : 1;
|
||||
}
|
||||
|
||||
public int getCursorPos() { return GetCursorPos(); }
|
||||
|
||||
public int GetCursorRow() {
|
||||
return delegate != null ? delegate.GetCursorRow() : 1;
|
||||
}
|
||||
|
||||
public int getCursorRow() { return GetCursorRow(); }
|
||||
|
||||
public int GetCursorCol() {
|
||||
return delegate != null ? delegate.GetCursorCol() : 1;
|
||||
}
|
||||
|
||||
public int getCursorCol() { return GetCursorCol(); }
|
||||
|
||||
public void SetCursorPos(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetCursorPos(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursorPos(int pos) throws ECLErr {
|
||||
SetCursorPos(pos);
|
||||
}
|
||||
|
||||
public void SetCursorPos(int row, int col) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetCursorPos(row, col);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursorPos(int row, int col) throws ECLErr {
|
||||
SetCursorPos(row, col);
|
||||
}
|
||||
|
||||
// ========== GetString & SetString Operations (1-based) ==========
|
||||
|
||||
public int GetString(char[] buffer, int len, int pos) throws ECLErr {
|
||||
return GetString(buffer, len, pos, len);
|
||||
}
|
||||
|
||||
public int GetString(char[] buffer, int len, int pos, int length) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.GetString(buffer, len, pos, length);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int GetString(char[] buffer, int len, int row, int col, int length) throws ECLErr {
|
||||
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
|
||||
}
|
||||
|
||||
public String GetString(int pos, int length) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.GetString(pos, length);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getString(int pos, int length) throws ECLErr {
|
||||
return GetString(pos, length);
|
||||
}
|
||||
|
||||
public String GetString(int row, int col, int length) throws ECLErr {
|
||||
return GetString(ConvertRowColToPos(row, col), length);
|
||||
}
|
||||
|
||||
public String getString(int row, int col, int length) throws ECLErr {
|
||||
return GetString(row, col, length);
|
||||
}
|
||||
|
||||
public void SetString(String text, int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetString(text, pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setString(String text, int pos) throws ECLErr {
|
||||
SetString(text, pos);
|
||||
}
|
||||
|
||||
public void SetString(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, ConvertRowColToPos(row, col));
|
||||
}
|
||||
|
||||
public void setString(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, row, col);
|
||||
}
|
||||
|
||||
public void SetString(String text) throws ECLErr {
|
||||
SetString(text, GetCursorPos());
|
||||
}
|
||||
|
||||
public void setString(String text) throws ECLErr {
|
||||
SetString(text);
|
||||
}
|
||||
|
||||
public void SetText(String text, int pos) throws ECLErr {
|
||||
SetString(text, pos);
|
||||
}
|
||||
|
||||
public void SetText(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, row, col);
|
||||
}
|
||||
|
||||
public void setText(String text, int pos) {
|
||||
if (delegate != null) delegate.setText(text, pos);
|
||||
}
|
||||
|
||||
public void setText(String text, int row, int col) {
|
||||
if (delegate != null) delegate.setText(text, row, col);
|
||||
}
|
||||
|
||||
// ========== Search Operations (1-based) ==========
|
||||
|
||||
public int SearchText(String text, int dir, int startPos) {
|
||||
return delegate != null ? delegate.SearchText(text, dir, startPos) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text, int dir) {
|
||||
return delegate != null ? delegate.SearchText(text, dir) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text) {
|
||||
return delegate != null ? delegate.SearchText(text) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text, int row, int col, int dir) throws ECLErr {
|
||||
return SearchText(text, dir, ConvertRowColToPos(row, col));
|
||||
}
|
||||
|
||||
public int SearchString(String text, int dir, int startPos) {
|
||||
return SearchText(text, dir, startPos);
|
||||
}
|
||||
|
||||
public int SearchString(String text, int dir) {
|
||||
return SearchText(text, dir);
|
||||
}
|
||||
|
||||
public int SearchString(String text) {
|
||||
return SearchText(text);
|
||||
}
|
||||
|
||||
public int SearchString(String text, int row, int col, int dir) throws ECLErr {
|
||||
return SearchText(text, row, col, dir);
|
||||
}
|
||||
|
||||
public int SearchPS(String text) {
|
||||
return delegate != null ? delegate.SearchPS(text) : 0;
|
||||
}
|
||||
|
||||
public int SearchPS(String text, int startPos) {
|
||||
return delegate != null ? delegate.SearchPS(text, startPos) : 0;
|
||||
}
|
||||
|
||||
public int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
|
||||
return delegate != null ? delegate.SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap) : 0;
|
||||
}
|
||||
|
||||
// ========== Keystrokes & Mnemonics ==========
|
||||
|
||||
public void SendKeys(String keys) {
|
||||
if (delegate != null) delegate.SendKeys(keys);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys) { SendKeys(keys); }
|
||||
|
||||
public void SendKeys(String keys, int pos) {
|
||||
if (delegate != null) delegate.SendKeys(keys, pos);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys, int pos) { SendKeys(keys, pos); }
|
||||
|
||||
public void SendKeys(String keys, int row, int col) {
|
||||
if (delegate != null) delegate.SendKeys(keys, row, col);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys, int row, int col) { SendKeys(keys, row, col); }
|
||||
|
||||
public void SendCharacters(String keys) {
|
||||
if (delegate != null) delegate.SendCharacters(keys);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys) { SendCharacters(keys); }
|
||||
|
||||
public void SendCharacters(String keys, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int delayMs) { SendCharacters(keys, delayMs); }
|
||||
|
||||
public void SendCharacters(String keys, int pos, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, pos, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int pos, int delayMs) { SendCharacters(keys, pos, delayMs); }
|
||||
|
||||
public void SendCharacters(String keys, int row, int col, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, row, col, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int row, int col, int delayMs) { SendCharacters(keys, row, col, delayMs); }
|
||||
|
||||
public String[] GetSendKeyMnemonics() {
|
||||
return delegate != null ? delegate.GetSendKeyMnemonics() : new String[0];
|
||||
}
|
||||
|
||||
public String[] getSendKeyMnemonics() { return GetSendKeyMnemonics(); }
|
||||
|
||||
// ========== Field Operations ==========
|
||||
|
||||
public ECLFieldList GetFieldList() {
|
||||
haus.nightmare.lib3270j.ecl.ECLFieldList fl = delegate != null ? delegate.GetFieldList() : null;
|
||||
return new ECLFieldList(fl, this);
|
||||
}
|
||||
|
||||
public ECLFieldList getFieldList() { return GetFieldList(); }
|
||||
|
||||
public ECLField GetField(int pos) throws ECLErr {
|
||||
return GetFieldList().FindField(pos);
|
||||
}
|
||||
|
||||
public ECLField getField(int pos) throws ECLErr { return GetField(pos); }
|
||||
|
||||
public ECLField GetField(int row, int col) throws ECLErr {
|
||||
return GetFieldList().FindField(row, col);
|
||||
}
|
||||
|
||||
public ECLField getField(int row, int col) throws ECLErr { return GetField(row, col); }
|
||||
|
||||
public ECLField GetFirstField() {
|
||||
return GetFieldList().GetFirstField();
|
||||
}
|
||||
|
||||
public ECLField getFirstField() { return GetFirstField(); }
|
||||
|
||||
public ECLField GetNextField(ECLField prev) {
|
||||
return GetFieldList().GetNextField(prev);
|
||||
}
|
||||
|
||||
public ECLField getNextField(ECLField prev) { return GetNextField(prev); }
|
||||
|
||||
// ========== Listener Overloads for HoD Interfaces ==========
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify) { RegisterPSEvent(notify); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, int eventType) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, eventType);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, int eventType) { RegisterPSEvent(notify, eventType); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, desc);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { RegisterPSEvent(notify, desc); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, desc, eventType);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { RegisterPSEvent(notify, desc, eventType); }
|
||||
|
||||
public void UnregisterPSEvent(ECLPSNotify notify) {
|
||||
if (delegate != null) delegate.UnregisterPSEvent(notify);
|
||||
}
|
||||
|
||||
public void unregisterPSEvent(ECLPSNotify notify) { UnregisterPSEvent(notify); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSListener listener) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); }
|
||||
|
||||
public void UnregisterPSEvent(ECLPSListener listener) {
|
||||
if (delegate != null) delegate.UnregisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); }
|
||||
|
||||
// ========== Synchronization & Waits ==========
|
||||
|
||||
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForScreen(desc, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
|
||||
|
||||
public boolean WaitForScreen(ECLScreenDesc desc) {
|
||||
return delegate != null && delegate.WaitForScreen(desc);
|
||||
}
|
||||
|
||||
public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); }
|
||||
|
||||
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitWhileScreen(desc, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); }
|
||||
|
||||
public boolean WaitWhileScreen(ECLScreenDesc desc) {
|
||||
return delegate != null && delegate.WaitWhileScreen(desc);
|
||||
}
|
||||
|
||||
public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); }
|
||||
|
||||
public boolean WaitForString(String text, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForString(text, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, long timeoutMs) { return WaitForString(text, timeoutMs); }
|
||||
|
||||
public boolean WaitForString(String text) {
|
||||
return delegate != null && delegate.WaitForString(text);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text) { return WaitForString(text); }
|
||||
|
||||
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForString(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, int row, int col, long timeoutMs) { return WaitForString(text, row, col, timeoutMs); }
|
||||
|
||||
public boolean WaitForCursor(int row, int col, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForCursor(row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
|
||||
|
||||
// ========== Screen Update Inquiries ==========
|
||||
|
||||
public ECLPSUpdate GetScreen(int startRow, int startCol, int length) throws ECLErr {
|
||||
return GetScreen(startRow, startCol, length, false);
|
||||
}
|
||||
|
||||
public ECLPSUpdate GetScreen(int startRow, int startCol, int length, boolean fillNulls) throws ECLErr {
|
||||
int pos = ConvertRowColToPos(startRow, startCol);
|
||||
char[] buf = new char[length];
|
||||
GetString(buf, length, pos, length);
|
||||
if (fillNulls) {
|
||||
for (int i = 0; i < buf.length; i++) {
|
||||
if (buf[i] == '\0') buf[i] = ' ';
|
||||
}
|
||||
}
|
||||
int endPos = pos + length - 1;
|
||||
int endRow = ConvertPosToRow(endPos);
|
||||
int endCol = ConvertPosToCol(endPos);
|
||||
return new ECLPSUpdate(this, startRow, startCol, endRow, endCol, new String(buf));
|
||||
}
|
||||
|
||||
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
|
||||
return delegate.GetScreen(buffer, len, row, col, length, plane);
|
||||
}
|
||||
|
||||
// ========== DBCS Character Status ==========
|
||||
|
||||
public boolean IsDBCSChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCSChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCSChar(int pos) { return IsDBCSChar(pos); }
|
||||
|
||||
public boolean IsDBCSChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCSChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCSChar(int row, int col) { return IsDBCSChar(row, col); }
|
||||
|
||||
public boolean IsDBCS1stChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCS1stChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCS1stChar(int pos) { return IsDBCS1stChar(pos); }
|
||||
|
||||
public boolean IsDBCS1stChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCS1stChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCS1stChar(int row, int col) { return IsDBCS1stChar(row, col); }
|
||||
|
||||
public boolean IsDBCS2ndChar(int pos) {
|
||||
return delegate != null && delegate.IsDBCS2ndChar(pos);
|
||||
}
|
||||
|
||||
public boolean isDBCS2ndChar(int pos) { return IsDBCS2ndChar(pos); }
|
||||
|
||||
public boolean IsDBCS2ndChar(int row, int col) {
|
||||
return delegate != null && delegate.IsDBCS2ndChar(row, col);
|
||||
}
|
||||
|
||||
public boolean isDBCS2ndChar(int row, int col) { return IsDBCS2ndChar(row, col); }
|
||||
|
||||
public boolean IsSOChar(int pos) {
|
||||
return delegate != null && delegate.IsSOChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSOChar(int pos) { return IsSOChar(pos); }
|
||||
|
||||
public boolean IsSIChar(int pos) {
|
||||
return delegate != null && delegate.IsSIChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSIChar(int pos) { return IsSIChar(pos); }
|
||||
|
||||
public boolean IsSOSIChar(int pos) {
|
||||
return delegate != null && delegate.IsSOSIChar(pos);
|
||||
}
|
||||
|
||||
public boolean isSOSIChar(int pos) { return IsSOSIChar(pos); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Presentation space notification interface matching IBM Host On-Demand ECLPSNotify.
|
||||
*/
|
||||
public interface ECLPSNotify {
|
||||
void NotifyEvent(ECLPS ps);
|
||||
default void NotifyStop(ECLPS ps, int reason) {}
|
||||
default void NotifyError(ECLPS ps, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Conforms to IBM Host On-Demand ECLPSUpdate.
|
||||
*/
|
||||
public class ECLPSUpdate implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate;
|
||||
private final int startRow;
|
||||
private final int startCol;
|
||||
private final int endRow;
|
||||
private final int endCol;
|
||||
private final int start;
|
||||
private final int end;
|
||||
private final boolean fullUpdate;
|
||||
private final String text;
|
||||
private final transient ECLPS ps;
|
||||
|
||||
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol,
|
||||
int start, int end, boolean fullUpdate, String text) {
|
||||
this.ps = ps;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.endRow = endRow;
|
||||
this.endCol = endCol;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.fullUpdate = fullUpdate;
|
||||
this.text = text != null ? text : "";
|
||||
this.delegate = null;
|
||||
}
|
||||
|
||||
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, String text) {
|
||||
this(ps, startRow, startCol, endRow, endCol, 0, 0, false, text);
|
||||
}
|
||||
|
||||
public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, String text) {
|
||||
this(null, startRow, startCol, endRow, endCol, 0, 0, false, text);
|
||||
}
|
||||
|
||||
public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate) {
|
||||
this.delegate = delegate;
|
||||
this.ps = (delegate != null && delegate.getPS() != null) ? new ECLPS(delegate.getPS()) : null;
|
||||
this.startRow = delegate != null ? delegate.getStartRow() : 0;
|
||||
this.startCol = delegate != null ? delegate.getStartCol() : 0;
|
||||
this.endRow = delegate != null ? delegate.getEndRow() : 0;
|
||||
this.endCol = delegate != null ? delegate.getEndCol() : 0;
|
||||
this.start = delegate != null ? delegate.getStart() : 0;
|
||||
this.end = delegate != null ? delegate.getEnd() : 0;
|
||||
this.fullUpdate = delegate != null && delegate.isFullUpdate();
|
||||
this.text = delegate != null ? delegate.getText() : "";
|
||||
}
|
||||
|
||||
public ECLPS getPS() { return ps; }
|
||||
public ECLPS GetPS() { return ps; }
|
||||
|
||||
public int getStartRow() { return startRow; }
|
||||
public int GetStartRow() { return startRow + 1; }
|
||||
|
||||
public int getStartCol() { return startCol; }
|
||||
public int GetStartCol() { return startCol + 1; }
|
||||
|
||||
public int getEndRow() { return endRow; }
|
||||
public int GetEndRow() { return endRow + 1; }
|
||||
|
||||
public int getEndCol() { return endCol; }
|
||||
public int GetEndCol() { return endCol + 1; }
|
||||
|
||||
public int getStart() { return start; }
|
||||
public int GetStart() { return start + 1; }
|
||||
|
||||
public int getEnd() { return end; }
|
||||
public int GetEnd() { return end + 1; }
|
||||
|
||||
public boolean isFullUpdate() { return fullUpdate; }
|
||||
public boolean IsFullUpdate() { return fullUpdate; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public String GetText() { return text; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ECLPSUpdate[start=(%d,%d), end=(%d,%d), full=%b]",
|
||||
startRow, startCol, endRow, endCol, fullUpdate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching engine facade matching IBM Host On-Demand ECLScreenDesc.
|
||||
*/
|
||||
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLScreenDesc() {
|
||||
super();
|
||||
}
|
||||
|
||||
public boolean Matches(ECLPS ps, ECLOIA oia) {
|
||||
haus.nightmare.lib3270j.ecl.ECLPS p = (ps != null) ? ps.getDelegate() : null;
|
||||
haus.nightmare.lib3270j.ecl.ECLOIA o = (oia != null) ? oia.getDelegate() : null;
|
||||
return super.Matches(p, o);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Listener interface for ECLScreenReco matching notifications matching IBM HoD.
|
||||
*/
|
||||
public interface ECLScreenNotify {
|
||||
void screenMatching(ECLScreenRecoEvent event);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Screen recognition facade matching IBM Host On-Demand ECLScreenReco.
|
||||
*/
|
||||
public class ECLScreenReco {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLScreenReco delegate;
|
||||
private final ECLPS ps;
|
||||
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
|
||||
|
||||
public ECLScreenReco() {
|
||||
this((ECLPS) null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLSession session) {
|
||||
this(session != null ? session.GetPS() : null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLPS ps) {
|
||||
this(ps, (ECLOIA) null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
|
||||
this.ps = ps;
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLScreenReco(ps != null ? ps.getDelegate() : null,
|
||||
oia != null ? oia.getDelegate() : null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLScreenReco delegate) {
|
||||
this.delegate = delegate;
|
||||
this.ps = null;
|
||||
}
|
||||
|
||||
public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
|
||||
if (desc != null && notify != null) {
|
||||
notifyMap.put(desc, notify);
|
||||
if (delegate != null) {
|
||||
delegate.RegisterScreen(desc, event -> notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
|
||||
RegisterScreen(desc, notify);
|
||||
}
|
||||
|
||||
public void UnregisterScreen(ECLScreenDesc desc) {
|
||||
if (desc != null) {
|
||||
notifyMap.remove(desc);
|
||||
if (delegate != null) {
|
||||
delegate.UnregisterScreen(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterScreen(ECLScreenDesc desc) {
|
||||
UnregisterScreen(desc);
|
||||
}
|
||||
|
||||
public boolean IsMatch(ECLScreenDesc desc) {
|
||||
if (delegate != null) {
|
||||
return delegate.IsMatch(desc);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) {
|
||||
if (ps == null) return false;
|
||||
if (desc == null) return true;
|
||||
return desc.Matches(ps, (ECLOIA) null);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLScreenReco getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Event delivered when an ECLScreenDesc matches the Presentation Space.
|
||||
*/
|
||||
public class ECLScreenRecoEvent extends ECLEvent {
|
||||
|
||||
private final ECLScreenReco source;
|
||||
private final ECLScreenDesc screenDesc;
|
||||
private final ECLPS ps;
|
||||
|
||||
public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) {
|
||||
super(source);
|
||||
this.source = source;
|
||||
this.screenDesc = screenDesc;
|
||||
this.ps = ps;
|
||||
}
|
||||
|
||||
public ECLScreenRecoEvent(Object source, ECLPS ps) {
|
||||
super(source);
|
||||
this.source = (source instanceof ECLScreenReco) ? (ECLScreenReco) source : null;
|
||||
this.screenDesc = null;
|
||||
this.ps = ps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object GetSource() { return source != null ? source : super.GetSource(); }
|
||||
|
||||
public ECLScreenReco GetScreenReco() { return source; }
|
||||
public ECLScreenReco getScreenReco() { return source; }
|
||||
|
||||
public ECLScreenDesc GetScreenDesc() { return screenDesc; }
|
||||
public ECLScreenDesc getScreenDesc() { return screenDesc; }
|
||||
|
||||
public ECLPS GetPS() { return ps; }
|
||||
public ECLPS getPS() { return ps; }
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Top-level session facade matching IBM Host On-Demand ECLSession.
|
||||
* Extends ECLConnection and provides access to Presentation Space (ECLPS),
|
||||
* Operator Information Area (ECLOIA), File Transfer (ECLXfer), and Screen Recognition (ECLScreenReco).
|
||||
*/
|
||||
public class ECLSession extends ECLConnection implements ECLConstants {
|
||||
|
||||
public static final String SESSION_NAME = "SESSION_NAME";
|
||||
public static final String SESSION_LABEL = "SESSION_LABEL";
|
||||
public static final String SESSION_TYPE = "SESSION_TYPE";
|
||||
public static final String SESSION_HOST = "SESSION_HOST";
|
||||
public static final String SESSION_HOST_PORT = "SESSION_HOST_PORT";
|
||||
public static final String SESSION_PORT = "SESSION_PORT";
|
||||
public static final String SESSION_PS_SIZE = "SESSION_PS_SIZE";
|
||||
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
|
||||
public static final String SESSION_SSL = "SESSION_SSL";
|
||||
public static final String SESSION_SECURITY_PROTOCOL = "SESSION_SECURITY_PROTOCOL";
|
||||
public static final String SESSION_TLS_PROTOCOL_VERSION = "tlsProtocolVersion";
|
||||
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
|
||||
public static final String SESSION_TN3270E = "SESSION_TN3270E";
|
||||
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
|
||||
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
|
||||
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
|
||||
public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE";
|
||||
public static final String KEY_KEEPALIVE_TYPE = "keepAliveType";
|
||||
public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout";
|
||||
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
|
||||
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
|
||||
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
|
||||
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
|
||||
public static final String BLOCK_PASTE = "blockPaste";
|
||||
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
|
||||
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
|
||||
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLSession delegate;
|
||||
private final ECLPS ps;
|
||||
private final ECLOIA oia;
|
||||
private final ECLXfer xfer;
|
||||
private final ECLScreenReco screenReco;
|
||||
|
||||
public ECLSession(Properties properties) throws ECLErr {
|
||||
super(properties);
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(properties);
|
||||
this.ps = new ECLPS(delegate.getPS());
|
||||
this.oia = new ECLOIA(delegate.getOIA());
|
||||
this.xfer = new ECLXfer(delegate.getXfer());
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLSession() throws ECLErr {
|
||||
this(new Properties());
|
||||
}
|
||||
|
||||
public ECLSession(String host, int port) throws ECLErr {
|
||||
super(host, port);
|
||||
Properties props = new Properties();
|
||||
if (host != null) props.setProperty(SESSION_HOST, host);
|
||||
props.setProperty(SESSION_PORT, String.valueOf(port));
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(props);
|
||||
this.ps = new ECLPS(delegate.getPS());
|
||||
this.oia = new ECLOIA(delegate.getOIA());
|
||||
this.xfer = new ECLXfer(delegate.getXfer());
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLSession(haus.nightmare.lib3270j.ecl.ECLSession delegate) {
|
||||
super(delegate);
|
||||
this.delegate = delegate;
|
||||
this.ps = new ECLPS(delegate != null ? delegate.getPS() : null);
|
||||
this.oia = new ECLOIA(delegate != null ? delegate.getOIA() : null);
|
||||
this.xfer = new ECLXfer(delegate != null ? delegate.getXfer() : null);
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLPS GetPS() {
|
||||
return ps;
|
||||
}
|
||||
|
||||
public ECLPS getPS() {
|
||||
return ps;
|
||||
}
|
||||
|
||||
public ECLOIA GetOIA() {
|
||||
return oia;
|
||||
}
|
||||
|
||||
public ECLOIA getOIA() {
|
||||
return oia;
|
||||
}
|
||||
|
||||
public ECLXfer GetXfer() {
|
||||
return xfer;
|
||||
}
|
||||
|
||||
public ECLXfer getXfer() {
|
||||
return xfer;
|
||||
}
|
||||
|
||||
public ECLScreenReco GetScreenReco() {
|
||||
return screenReco;
|
||||
}
|
||||
|
||||
public ECLScreenReco getScreenReco() {
|
||||
return screenReco;
|
||||
}
|
||||
|
||||
public ECLConnection GetConnection() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ECLConnection getConnection() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLSession getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StartCommunication() throws IOException {
|
||||
if (delegate != null) {
|
||||
delegate.StartCommunication();
|
||||
} else {
|
||||
super.StartCommunication();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StopCommunication() {
|
||||
if (delegate != null) {
|
||||
delegate.StopCommunication();
|
||||
} else {
|
||||
super.StopCommunication();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean StartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null);
|
||||
}
|
||||
|
||||
public boolean startCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout);
|
||||
}
|
||||
|
||||
public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.StartCommunicationWithBlocking(timeout, desc);
|
||||
} catch (IOException e) {
|
||||
throw new ECLErr("ECLSession.StartCommunicationWithBlocking", "ECL0001", e.getMessage());
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout, desc);
|
||||
}
|
||||
|
||||
public boolean RestartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.RestartCommunicationWithBlocking(timeout);
|
||||
} catch (IOException e) {
|
||||
throw new ECLErr("ECLSession.RestartCommunicationWithBlocking", "ECL0001", e.getMessage());
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean restartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return RestartCommunicationWithBlocking(timeout);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (delegate != null) {
|
||||
delegate.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLXferListener;
|
||||
|
||||
/**
|
||||
* File transfer facade matching IBM Host On-Demand ECLXfer.
|
||||
*/
|
||||
public class ECLXfer {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLXfer delegate;
|
||||
|
||||
public ECLXfer(ECLSession session, Boolean bidi) {
|
||||
this.delegate = (session != null && session.getDelegate() != null)
|
||||
? session.getDelegate().GetXfer() : null;
|
||||
}
|
||||
|
||||
public ECLXfer(haus.nightmare.lib3270j.ecl.ECLXfer delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public void SendFile(String localFile, String hostFile, String options) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
int rc = delegate.SendFile(localFile, hostFile, options);
|
||||
if (rc != 0) {
|
||||
throw new ECLErr("ECLXfer:SendFile", "ECL0150", String.valueOf(rc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ReceiveFile(String localFile, String hostFile, String options) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
int rc = delegate.ReceiveFile(localFile, hostFile, options);
|
||||
if (rc != 0) {
|
||||
throw new ECLErr("ECLXfer:ReceiveFile", "ECL0150", String.valueOf(rc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel() {
|
||||
if (delegate != null) {
|
||||
delegate.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterXferEvent(ECLXferListener listener) {
|
||||
// Can bridge listener if needed
|
||||
}
|
||||
|
||||
public void UnregisterXferEvent(ECLXferListener listener) {
|
||||
// Can bridge listener if needed
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLXfer getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
/**
|
||||
* Base event class matching IBM Host On-Demand ECLEvent.
|
||||
*/
|
||||
public class ECLEvent {
|
||||
private final Object originator;
|
||||
|
||||
public ECLEvent(Object object) {
|
||||
this.originator = object;
|
||||
}
|
||||
|
||||
public Object GetSource() {
|
||||
return this.originator;
|
||||
}
|
||||
|
||||
public Object getSource() {
|
||||
return this.originator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + ": " + (this.originator != null ? this.originator.toString() : "null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLPS;
|
||||
import com.ibm.eNetwork.ECL.ECLPSUpdate;
|
||||
|
||||
/**
|
||||
* Presentation Space event class matching IBM Host On-Demand ECLPSEvent.
|
||||
*/
|
||||
public class ECLPSEvent extends ECLEvent {
|
||||
|
||||
public static final int USER_EVENTS = 1;
|
||||
public static final int HOST_EVENTS = 2;
|
||||
public static final int ALL_EVENTS = 3;
|
||||
|
||||
private int type = 0;
|
||||
private int start = 1;
|
||||
private int end = 1;
|
||||
private int startRow = 1;
|
||||
private int startCol = 1;
|
||||
private int endRow = 1;
|
||||
private int endCol = 1;
|
||||
private int oldCursorPos = 1;
|
||||
private int newCursorPos = 1;
|
||||
private boolean fullUpdate = true;
|
||||
private boolean cursorVisible = true;
|
||||
private int ringCounter = 0;
|
||||
private ECLPSUpdate psUpdate = null;
|
||||
private haus.nightmare.lib3270j.ecl.ECLPSEvent delegate = null;
|
||||
|
||||
public ECLPSEvent(Object source) {
|
||||
super(source);
|
||||
if (source instanceof ECLPS) {
|
||||
ECLPS ps = (ECLPS) source;
|
||||
this.end = ps.GetSize();
|
||||
this.endRow = ps.GetRows();
|
||||
this.endCol = ps.GetCols();
|
||||
this.newCursorPos = ps.GetCursorPos();
|
||||
} else if (source instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
|
||||
haus.nightmare.lib3270j.ecl.ECLPS ps = (haus.nightmare.lib3270j.ecl.ECLPS) source;
|
||||
this.end = ps.GetSize();
|
||||
this.endRow = ps.GetRows();
|
||||
this.endCol = ps.GetCols();
|
||||
this.newCursorPos = ps.GetCursorPos();
|
||||
}
|
||||
}
|
||||
|
||||
public ECLPSEvent(Object source, int type) {
|
||||
this(source);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ECLPSEvent(haus.nightmare.lib3270j.ecl.ECLPSEvent delegate) {
|
||||
super(delegate != null ? delegate.getSource() : null);
|
||||
this.delegate = delegate;
|
||||
if (delegate != null) {
|
||||
this.type = delegate.getType();
|
||||
this.start = delegate.getStart();
|
||||
this.end = delegate.getEnd();
|
||||
this.startRow = delegate.getStartRow();
|
||||
this.startCol = delegate.getStartCol();
|
||||
this.endRow = delegate.getEndRow();
|
||||
this.endCol = delegate.getEndCol();
|
||||
this.oldCursorPos = delegate.getOldCursorAddress() + 1;
|
||||
this.newCursorPos = delegate.getNewCursorAddress() + 1;
|
||||
this.fullUpdate = delegate.isFullUpdate();
|
||||
this.cursorVisible = delegate.isCursorVisible();
|
||||
this.ringCounter = delegate.getRingCounter();
|
||||
if (delegate.getPSUpdate() != null) {
|
||||
this.psUpdate = new ECLPSUpdate(delegate.getPSUpdate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetType() { return type; }
|
||||
public int getType() { return type; }
|
||||
|
||||
public ECLPS GetPS() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLPS) {
|
||||
return (ECLPS) src;
|
||||
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
|
||||
return new ECLPS((haus.nightmare.lib3270j.ecl.ECLPS) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetStart(int start) { this.start = start; }
|
||||
public int GetStart() { return start; }
|
||||
public int getStart() { return start; }
|
||||
|
||||
public int GetStartRow() { return startRow; }
|
||||
public int getStartRow() { return startRow; }
|
||||
|
||||
public int GetStartCol() { return startCol; }
|
||||
public int getStartCol() { return startCol; }
|
||||
|
||||
public void SetEnd(int end) { this.end = end; }
|
||||
public int GetEnd() { return end; }
|
||||
public int getEnd() { return end; }
|
||||
|
||||
public int GetEndRow() { return endRow; }
|
||||
public int getEndRow() { return endRow; }
|
||||
|
||||
public int GetEndCol() { return endCol; }
|
||||
public int getEndCol() { return endCol; }
|
||||
|
||||
public int GetOldCursorPos() { return oldCursorPos; }
|
||||
public int getOldCursorPos() { return oldCursorPos; }
|
||||
|
||||
public int GetNewCursorPos() { return newCursorPos; }
|
||||
public int getNewCursorPos() { return newCursorPos; }
|
||||
|
||||
public boolean IsFullUpdate() { return fullUpdate; }
|
||||
public boolean isFullUpdate() { return fullUpdate; }
|
||||
|
||||
public boolean IsCursorVisible() { return cursorVisible; }
|
||||
public boolean isCursorVisible() { return cursorVisible; }
|
||||
|
||||
public int GetRingCounter() { return ringCounter; }
|
||||
public int getRingCounter() { return ringCounter; }
|
||||
|
||||
public ECLPSUpdate GetPSUpdate() { return psUpdate; }
|
||||
public ECLPSUpdate getPSUpdate() { return psUpdate; }
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLPSEvent getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLErr;
|
||||
import com.ibm.eNetwork.ECL.ECLPS;
|
||||
|
||||
/**
|
||||
* Presentation Space listener interface matching IBM Host On-Demand ECLPSListener.
|
||||
*/
|
||||
public interface ECLPSListener {
|
||||
void PSNotifyEvent(ECLPSEvent event);
|
||||
default void PSNotifyStop(ECLPS ps, int reason) {}
|
||||
default void PSNotifyError(ECLPS ps, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLXfer;
|
||||
|
||||
/**
|
||||
* File transfer event matching IBM Host On-Demand ECLXferEvent.
|
||||
*/
|
||||
public class ECLXferEvent extends ECLEvent {
|
||||
|
||||
private final String sourceFile;
|
||||
private final String targetFile;
|
||||
private final boolean receive;
|
||||
private final long unitsDone;
|
||||
private final long unitsToDo;
|
||||
|
||||
public ECLXferEvent(Object source, long unitsDone, String sourceFile, String targetFile, boolean receive) {
|
||||
this(source, unitsDone, -1L, sourceFile, targetFile, receive);
|
||||
}
|
||||
|
||||
public ECLXferEvent(Object source, long unitsDone, long unitsToDo, String sourceFile, String targetFile, boolean receive) {
|
||||
super(source);
|
||||
this.unitsDone = unitsDone;
|
||||
this.unitsToDo = unitsToDo;
|
||||
this.sourceFile = sourceFile;
|
||||
this.targetFile = targetFile;
|
||||
this.receive = receive;
|
||||
}
|
||||
|
||||
public ECLXfer GetECLXfer() {
|
||||
Object src = GetSource();
|
||||
return (src instanceof ECLXfer) ? (ECLXfer) src : null;
|
||||
}
|
||||
|
||||
public String GetSourceFile() { return sourceFile; }
|
||||
public String getSourceFile() { return sourceFile; }
|
||||
|
||||
public String GetTargetFile() { return targetFile; }
|
||||
public String getTargetFile() { return targetFile; }
|
||||
|
||||
public long GetBytesSent() { return unitsDone; }
|
||||
public long getBytesSent() { return unitsDone; }
|
||||
|
||||
public long GetBytesToSend() { return unitsToDo; }
|
||||
public long getBytesToSend() { return unitsToDo; }
|
||||
|
||||
public boolean IsReceive() { return receive; }
|
||||
public boolean isReceive() { return receive; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLErr;
|
||||
import com.ibm.eNetwork.ECL.ECLXfer;
|
||||
|
||||
/**
|
||||
* File transfer listener interface matching IBM Host On-Demand ECLXferListener.
|
||||
*/
|
||||
public interface ECLXferListener {
|
||||
void XferNotifyEvent(ECLXferEvent event);
|
||||
default void XferNotifyStop(ECLXfer xfer, int reason) {}
|
||||
default void XferNotifyError(ECLXfer xfer, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.ECL.macrovariable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroExpressionParser.
|
||||
*/
|
||||
public class MacroExpressionParser extends haus.nightmare.lib3270j.macro.variable.MacroExpressionParser implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroExpressionParser() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.ECL.macrovariable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroVariables.
|
||||
*/
|
||||
public class MacroVariables extends haus.nightmare.lib3270j.macro.variable.MacroVariables implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroVariables() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,12 @@ public class ConnectionConfig {
|
||||
private int reconnectMaxRetries = 5;
|
||||
private int tcpUserTimeoutMs = 0;
|
||||
|
||||
// Host operating system type (TSO, CMS, CICS)
|
||||
private String hostType = "TSO";
|
||||
private boolean savedConnection = false;
|
||||
private String savedProfileId = null;
|
||||
private String savedProfileName = null;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
@@ -312,6 +318,51 @@ public class ConnectionConfig {
|
||||
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
|
||||
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
|
||||
|
||||
public String getHostType() {
|
||||
return hostType != null ? hostType : "TSO";
|
||||
}
|
||||
|
||||
public void setHostType(String hostType) {
|
||||
this.hostType = (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO";
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ft.FTConfig.HostType getFtHostType() {
|
||||
if (hostType != null) {
|
||||
try {
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(hostType.trim().toUpperCase());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
|
||||
}
|
||||
|
||||
public void setFtHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ftHostType) {
|
||||
this.hostType = (ftHostType != null) ? ftHostType.name() : "TSO";
|
||||
}
|
||||
|
||||
public boolean isSavedConnection() {
|
||||
return savedConnection;
|
||||
}
|
||||
|
||||
public void setSavedConnection(boolean savedConnection) {
|
||||
this.savedConnection = savedConnection;
|
||||
}
|
||||
|
||||
public String getSavedProfileId() {
|
||||
return savedProfileId;
|
||||
}
|
||||
|
||||
public void setSavedProfileId(String savedProfileId) {
|
||||
this.savedProfileId = savedProfileId;
|
||||
}
|
||||
|
||||
public String getSavedProfileName() {
|
||||
return savedProfileName;
|
||||
}
|
||||
|
||||
public void setSavedProfileName(String savedProfileName) {
|
||||
this.savedProfileName = savedProfileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
|
||||
@@ -435,6 +486,14 @@ public class ConnectionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse [lu@]host[:port] (e.g. "00C2@mvs.host.com:1023")
|
||||
String parsedLu = null;
|
||||
int atIdx = s.indexOf('@');
|
||||
if (atIdx > 0 && atIdx < s.length() - 1) {
|
||||
parsedLu = s.substring(0, atIdx).trim();
|
||||
s = s.substring(atIdx + 1).trim();
|
||||
}
|
||||
|
||||
String host = s;
|
||||
int port = (defaultPort > 0) ? defaultPort : (tls ? 992 : 23);
|
||||
|
||||
@@ -458,6 +517,9 @@ public class ConnectionConfig {
|
||||
}
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
if (parsedLu != null && !parsedLu.isEmpty()) {
|
||||
config.setLuName(parsedLu);
|
||||
}
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
config.setKeepAliveEnabled(keepAlive);
|
||||
|
||||
@@ -11,6 +11,7 @@ import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Main API entry point for lib3270j.
|
||||
@@ -46,6 +47,7 @@ public class Telnet3270Client {
|
||||
|
||||
private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
private volatile Thread reconnectThread;
|
||||
private volatile boolean intentionallyDisconnected = false;
|
||||
|
||||
public Telnet3270Client(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
@@ -92,6 +94,9 @@ public class Telnet3270Client {
|
||||
ps.notifyAlarm();
|
||||
}
|
||||
@Override public void onKeyboardUnlocked() {
|
||||
if (screenBuffer != null) {
|
||||
screenBuffer.getHistoryBuffer().snapshot(screenBuffer);
|
||||
}
|
||||
ps.notifyKeyUnlocked();
|
||||
}
|
||||
});
|
||||
@@ -127,6 +132,7 @@ public class Telnet3270Client {
|
||||
* then returns while telnet/TN3270E negotiation continues asynchronously.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
intentionallyDisconnected = false;
|
||||
log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
|
||||
" model=" + config.getModel());
|
||||
|
||||
@@ -190,8 +196,16 @@ public class Telnet3270Client {
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
intentionallyDisconnected = true;
|
||||
cancelAutoReconnect();
|
||||
if (connection != null) {
|
||||
try {
|
||||
if (fsm != null) {
|
||||
fsm.sendTelnetDisconnect();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.log(Level.FINE, "Error during Telnet disconnect negotiation", e);
|
||||
}
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
@@ -200,14 +214,20 @@ public class Telnet3270Client {
|
||||
|
||||
public void cancelAutoReconnect() {
|
||||
reconnecting.set(false);
|
||||
if (reconnectThread != null) {
|
||||
reconnectThread.interrupt();
|
||||
Thread t = reconnectThread;
|
||||
if (t != null) {
|
||||
t.interrupt();
|
||||
try {
|
||||
t.join(200);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
reconnectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void initiateAutoReconnect() {
|
||||
if (config == null || !config.isAutoReconnect()) {
|
||||
if (intentionallyDisconnected || config == null || !config.isAutoReconnect()) {
|
||||
return;
|
||||
}
|
||||
if (!reconnecting.compareAndSet(false, true)) {
|
||||
@@ -658,6 +678,9 @@ public class Telnet3270Client {
|
||||
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
|
||||
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. */
|
||||
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
|
||||
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
|
||||
@@ -705,4 +728,8 @@ public class Telnet3270Client {
|
||||
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return dsProcessor.getGocaDecoder();
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.screen.ScreenHistoryBuffer getScreenHistoryBuffer() {
|
||||
return screenBuffer != null ? screenBuffer.getHistoryBuffer() : null;
|
||||
}
|
||||
}
|
||||
|
||||