Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
79ce0a7dc6
|
|||
|
fe185bd1f0
|
|||
|
ee44759668
|
|||
|
69ebe6bfb5
|
|||
|
63d14bd2d9
|
|||
|
377626013d
|
|||
|
bf5eefd190
|
|||
|
c941fd3a2a
|
|||
|
b71f0f5ba4
|
|||
|
b81a244bb9
|
|||
|
351379b5b3
|
|||
|
eba1406cbb
|
|||
|
88e3ffcbe9
|
|||
|
f389e626ea
|
|||
|
0eaab32e2f
|
|||
|
da289730db
|
|||
|
f77f51474c
|
@@ -36,11 +36,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Android SDK
|
- name: Set up Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
packages: ''
|
||||||
|
|
||||||
- name: Make scripts executable
|
- name: Make scripts executable
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./gradlew ./build.sh
|
chmod +x ./gradlew ./build.sh
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
run: |
|
||||||
|
./gradlew testDebugUnitTest --no-daemon
|
||||||
|
env:
|
||||||
|
J3270_DIR: ${{ github.workspace }}/j3270
|
||||||
|
|
||||||
- name: Build Android APK
|
- name: Build Android APK
|
||||||
run: |
|
run: |
|
||||||
./gradlew assembleDebug --no-daemon
|
./gradlew assembleDebug --no-daemon
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Android SDK
|
- name: Set up Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
packages: ''
|
||||||
|
|
||||||
- name: Make scripts executable
|
- name: Make scripts executable
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ captures/
|
|||||||
.cxx
|
.cxx
|
||||||
*.jar
|
*.jar
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
*.txt
|
||||||
|
*.log*
|
||||||
|
|||||||
@@ -34,20 +34,70 @@ adb install -r a3270.apk
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📱 Compatibility & System Requirements
|
||||||
|
|
||||||
|
- **Minimum Android Version**: Android 7.0 (Nougat) — **API Level 24** (`minSdk 24`)
|
||||||
|
- **Target Android Version**: Android 14 — **API Level 34** (`targetSdk 34`)
|
||||||
|
- **Compile SDK**: **API Level 34** (`compileSdk 34`)
|
||||||
|
- **Architecture**: Universal — pure Kotlin/Java running on ART, fully compatible with all CPU ABIs (`arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64`)
|
||||||
|
- **Form Factors**: Adaptive UI scaling optimized for smartphones, foldables, tablets, and Chromebooks (Android runtime)
|
||||||
|
- **Input Devices**: Full touch-screen support, native virtual keyboards, and external physical/Bluetooth keyboards
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Modern Jetpack Compose UI**: Responsive, touch-optimized 3270 terminal screen rendering.
|
### 🖥️ Terminal Emulation & Protocol
|
||||||
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption via `lib3270j`.
|
- **RFC 2355 TN3270 & TN3270E Engine**: Robust state machine, Option Negotiation, Device-Type Query, and LU name specification powered by `lib3270j`.
|
||||||
- **On-Screen Function Key Bar**: Quick-access touch bar for PF1–PF24, PA1–PA3, Enter, Clear, Reset, and Attn keys.
|
- **Standard & Dynamic Terminal Models**:
|
||||||
- **IND$FILE File Transfer**: Full mobile UI for **CUT** and **DFT (DDM)** high-speed structured field file transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS.
|
- Model 2 (24 × 80)
|
||||||
- **OIA Status Line**: Real-time Operator Information Area displaying connection state, keyboard lock, insert mode, cursor row/col, and LU name.
|
- Model 3 (32 × 80)
|
||||||
- **Host Profile Storage**: Save and manage multiple mainframe connection profiles with custom ports, models, and SSL/TLS options.
|
- Model 4 (43 × 80)
|
||||||
- **Background Session Persistence**: Android foreground service keeps your terminal session alive when multitasking.
|
- Model 5 (27 × 132)
|
||||||
- **Terminal Models**:
|
- **Dynamic Screen Dimensions**: Configurable custom geometry (rows & columns up to 255 × 255).
|
||||||
- Model 2 (24x80)
|
- **EBCDIC Code Pages**: Full character translation across international code pages (037, 1047, 500, etc.).
|
||||||
- Model 3 (32x80)
|
|
||||||
- Model 4 (43x80)
|
### 📊 Graphics & Peripherals
|
||||||
- Model 5 (27x132)
|
- **GDDM & GOCA Graphics**: Native rendering of Programmed Symbols (PS) and GOCA vector graphics directly in the Compose canvas.
|
||||||
|
- **Light Pen Simulation**: Interactive touch support to simulate light pen selection and graphic cursor pick correlation.
|
||||||
|
- **IBM 3287 Printer Emulation**: Integrated printer session support for spool/report capture (via associated display LU or dedicated printer LU) with direct printing through Android PrintManager (PDF export / system print).
|
||||||
|
|
||||||
|
### 📁 IND$FILE File Transfer
|
||||||
|
- Full mobile UI supporting both **CUT** and **DFT (DDM)** high-speed structured field transfers.
|
||||||
|
- Compatible with **TSO**, **VM/CMS**, and **CICS** environments.
|
||||||
|
- ASCII/binary translation, CRLF conversions, and configurable record format (`recfm`), record length (`lrecl`), and allocation units.
|
||||||
|
|
||||||
|
### 🔍 Developer & Productivity Tools
|
||||||
|
- **Field Inspector**: Live interactive inspector displaying start/end addresses, length, protection, numeric status, MDT (Modified Data Tag), display intensity, and extended highlighting/color, with one-tap "Jump to Field".
|
||||||
|
- **Screen Search / Find**: On-screen text search with case sensitivity, forward/backward navigation, and visual match highlighting.
|
||||||
|
- **Screen Exporter**: Export the current screen buffer to **Plain Text**, formatted **HTML**, or **PNG image**, with one-tap clipboard copy and Android system share sheet integration.
|
||||||
|
- **Script & Macro Automation**: Built-in script runner supporting automated keystroke and wait sequences (e.g. `[enter]`, `[wait]`, `[tab]`) for login macros and repetitive tasks.
|
||||||
|
|
||||||
|
### ✍️ Entry Assist & Typing Modes
|
||||||
|
- **Document (DOC) Mode**: Configurable left/right margins, custom tab stops, and end-of-line bell warning.
|
||||||
|
- **Word Wrap**: Automatic word wrapping across lines during input.
|
||||||
|
- **APL Mode**: Support for APL extended character sets and symbols.
|
||||||
|
- **Field Handling**: Automatic field skip (Auto-Skip), Numeric field lock, and Insert Mode auto-reset on AID transmission.
|
||||||
|
|
||||||
|
### 🎨 Display, Themes & Ergonomics
|
||||||
|
- **Jetpack Compose UI**: Smooth, hardware-accelerated 3270 terminal screen rendering with touch-to-position cursor.
|
||||||
|
- **Color Themes**: Light and Dark application themes, 3270 extended color support, and 4-color legacy override mode.
|
||||||
|
- **Cursor Customization**: Block and Underline cursor styles with toggleable blinking.
|
||||||
|
- **Crosshair Ruler**: Toggleable horizontal and vertical ruler lines following the cursor for easy data tracking.
|
||||||
|
- **Block Selection**: Rectangular text selection and copy mode.
|
||||||
|
- **OIA Status Line**: Real-time Operator Information Area displaying connection status, keyboard lock, insert mode, cursor row/col, LU name, and inhibit reason codes.
|
||||||
|
|
||||||
|
### 🔒 Security, Networking & Storage
|
||||||
|
- **SSL / TLS Encryption**: Secure encrypted mainframe sessions with TLS certificate verification.
|
||||||
|
- **Interactive Untrusted Certificate Prompt**: Inspect SSL certificates, view full certificate chain details, and accept/reject unverified connections.
|
||||||
|
- **Proxy Support**: Connect via HTTP CONNECT, SOCKS4, SOCKS5, and Telnet proxies with optional user/password authentication.
|
||||||
|
- **Background Session Persistence**: Android foreground service (`specialUse`) keeps your terminal TCP connection alive with status notifications when multitasking or screen is off.
|
||||||
|
- **Host Profile Manager & INI Export**: Save multiple mainframe profiles, configure auto-connect on launch, and export/import configuration in INI format (interoperable with desktop `j3270`).
|
||||||
|
|
||||||
|
### ⌨️ Input & Keyboards
|
||||||
|
- **On-Screen Function Key Bar**: Two-row responsive touch bar providing PF1–PF24, PA1–PA3, Enter, Clear, Reset, Attn, SysReq, Erase EOF, Erase Input, Tab, BackTab, and D-pad cursor controls with haptic feedback.
|
||||||
|
- **Termux-Style Native Input**: Low-latency native input connection with permanent number row and lock protection against soft-keyboard resets.
|
||||||
|
- **Hardware Keyboard Support**: Complete physical keyboard integration with F1–F12 (mapped to PF1–PF12 / PF13–PF24 with Shift/Alt), Ctrl shortcuts, dead keys, and international character input.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,6 +106,7 @@ adb install -r a3270.apk
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
- **Java Development Kit (JDK)**: JDK 17 or JDK 21
|
- **Java Development Kit (JDK)**: JDK 17 or JDK 21
|
||||||
- **Android SDK**: API 34 platforms and build-tools (`ANDROID_HOME` or `ANDROID_SDK_ROOT` set)
|
- **Android SDK**: API 34 platforms and build-tools (`ANDROID_HOME` or `ANDROID_SDK_ROOT` set)
|
||||||
|
- **Minimum Target Device**: Android 7.0+ (API 24+)
|
||||||
|
|
||||||
### 1. Clone Repositories Side-by-Side
|
### 1. Clone Repositories Side-by-Side
|
||||||
For the easiest setup, clone both `j3270` and `a3270` into the same parent folder:
|
For the easiest setup, clone both `j3270` and `a3270` into the same parent folder:
|
||||||
|
|||||||
@@ -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_a3270_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 |
+2
-2
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "haus.nightmare.a3270"
|
applicationId "haus.nightmare.a3270"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 6
|
versionCode 8
|
||||||
versionName "1.0.1"
|
versionName "1.2.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -45,14 +45,19 @@ if [ ! -d "$J3270_PATH/lib3270j" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Building debug APK..."
|
if [ $# -gt 0 ]; then
|
||||||
./gradlew assembleDebug
|
echo "Running: ./gradlew $@"
|
||||||
|
./gradlew "$@"
|
||||||
APK_PATH="$SCRIPT_DIR/build/outputs/apk/debug/a3270-debug.apk"
|
|
||||||
if [ -f "$APK_PATH" ]; then
|
|
||||||
echo "=== Build Complete ==="
|
|
||||||
echo "APK created at: $APK_PATH"
|
|
||||||
ls -lh "$APK_PATH"
|
|
||||||
else
|
else
|
||||||
echo "Build completed, but APK not found at $APK_PATH"
|
echo "Building debug APK..."
|
||||||
|
./gradlew assembleDebug --no-daemon
|
||||||
|
|
||||||
|
APK_PATH="$SCRIPT_DIR/build/outputs/apk/debug/a3270-debug.apk"
|
||||||
|
if [ -f "$APK_PATH" ]; then
|
||||||
|
echo "=== Build Complete ==="
|
||||||
|
echo "APK created at: $APK_PATH"
|
||||||
|
ls -lh "$APK_PATH"
|
||||||
|
else
|
||||||
|
echo "Build completed, but APK not found at $APK_PATH"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -115,6 +115,26 @@ esac
|
|||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Prefer compatible JDK (JDK 17 or 21) if active Java is incompatible (Java 22+)
|
||||||
|
if [ -d "/Users/rudi/.sdkman/candidates/java/21.0.6-sem" ]; then
|
||||||
|
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/21.0.6-sem"
|
||||||
|
elif [ -d "$HOME/.sdkman/candidates/java/21.0.2-open" ]; then
|
||||||
|
CURRENT_JAVA_CMD="${JAVA_HOME:+$JAVA_HOME/bin/java}"
|
||||||
|
CURRENT_JAVA_CMD="${CURRENT_JAVA_CMD:-java}"
|
||||||
|
CURRENT_VER=$("$CURRENT_JAVA_CMD" -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1)
|
||||||
|
if [ -z "$JAVA_HOME" ] || [ "$CURRENT_VER" -gt 21 ] 2>/dev/null; then
|
||||||
|
export JAVA_HOME="$HOME/.sdkman/candidates/java/21.0.2-open"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ANDROID_HOME" ]; then
|
||||||
|
if [ -d "$HOME/Library/Android/sdk" ]; then
|
||||||
|
export ANDROID_HOME="$HOME/Library/Android/sdk"
|
||||||
|
elif [ -d "$HOME/Android/Sdk" ]; then
|
||||||
|
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:versionCode="8"
|
||||||
|
android:versionName="1.2.3">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@@ -9,6 +11,8 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="a3270"
|
android:label="a3270"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
||||||
@@ -33,6 +37,16 @@
|
|||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
android:value="3270 Terminal TCP socket connection persistence" />
|
android:value="3270 Terminal TCP socket connection persistence" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -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_a3270_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 |
@@ -22,11 +22,15 @@ import androidx.core.view.WindowCompat
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.view.KeyCharacterMap
|
import android.view.KeyCharacterMap
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
import haus.nightmare.a3270.service.TerminalService
|
import haus.nightmare.a3270.service.TerminalService
|
||||||
import haus.nightmare.a3270.storage.AppSettings
|
import haus.nightmare.a3270.storage.AppSettings
|
||||||
import haus.nightmare.a3270.storage.HostStorage
|
import haus.nightmare.a3270.storage.HostStorage
|
||||||
|
import haus.nightmare.a3270.ui.ConnectChoice
|
||||||
|
import haus.nightmare.a3270.ui.ConnectChoiceDialog
|
||||||
import haus.nightmare.a3270.ui.ConnectDialog
|
import haus.nightmare.a3270.ui.ConnectDialog
|
||||||
import haus.nightmare.a3270.ui.FileTransferDialog
|
import haus.nightmare.a3270.ui.FileTransferDialog
|
||||||
import haus.nightmare.a3270.ui.OiaStatusBar
|
import haus.nightmare.a3270.ui.OiaStatusBar
|
||||||
@@ -39,7 +43,8 @@ import haus.nightmare.a3270.ui.FindDialog
|
|||||||
import haus.nightmare.a3270.ui.ScreenExporterDialog
|
import haus.nightmare.a3270.ui.ScreenExporterDialog
|
||||||
import haus.nightmare.a3270.ui.ScriptDialog
|
import haus.nightmare.a3270.ui.ScriptDialog
|
||||||
import haus.nightmare.a3270.ui.PrinterSessionDialog
|
import haus.nightmare.a3270.ui.PrinterSessionDialog
|
||||||
|
import haus.nightmare.a3270.ui.ScreenHistoryDialog
|
||||||
|
import haus.nightmare.a3270.ui.MacroDialog
|
||||||
import haus.nightmare.a3270.ui.UntrustedCertificateDialog
|
import haus.nightmare.a3270.ui.UntrustedCertificateDialog
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -191,6 +196,29 @@ class MainActivity : ComponentActivity() {
|
|||||||
val alt = event.isAltPressed || (event.metaState and KeyEvent.META_ALT_ON != 0)
|
val alt = event.isAltPressed || (event.metaState and KeyEvent.META_ALT_ON != 0)
|
||||||
|
|
||||||
// 1. Function Keys (F1..F12 -> PF1..PF12 or PF13..PF24 with Shift or Alt)
|
// 1. Function Keys (F1..F12 -> PF1..PF12 or PF13..PF24 with Shift or Alt)
|
||||||
|
// Mode toggles matching j3270:
|
||||||
|
// Alt+F1: DOC mode (Entry Assist)
|
||||||
|
// Alt+F2: Word Wrap
|
||||||
|
// Alt+F3: APL Mode
|
||||||
|
if (alt && keyCode == KeyEvent.KEYCODE_F1) {
|
||||||
|
viewModel.toggleDocMode()
|
||||||
|
val isDoc = viewModel.isDocMode.value
|
||||||
|
Toast.makeText(this, "DOC Mode: " + (if (isDoc) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (alt && keyCode == KeyEvent.KEYCODE_F2) {
|
||||||
|
viewModel.toggleWordWrap()
|
||||||
|
val isWrap = viewModel.isWordWrap.value
|
||||||
|
Toast.makeText(this, "Word Wrap: " + (if (isWrap) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (alt && keyCode == KeyEvent.KEYCODE_F3) {
|
||||||
|
viewModel.toggleAplMode()
|
||||||
|
val isApl = viewModel.isAplMode.value
|
||||||
|
Toast.makeText(this, "APL Mode: " + (if (isApl) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if (keyCode in KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12) {
|
if (keyCode in KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12) {
|
||||||
val fNum = keyCode - KeyEvent.KEYCODE_F1 + 1
|
val fNum = keyCode - KeyEvent.KEYCODE_F1 + 1
|
||||||
val pfNum = if (shift || alt) fNum + 12 else fNum
|
val pfNum = if (shift || alt) fNum + 12 else fNum
|
||||||
@@ -339,6 +367,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
// 5. Standard Navigation & Special Keys
|
// 5. Standard Navigation & Special Keys
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_INSERT -> {
|
||||||
|
viewModel.toggleInsert()
|
||||||
|
return true
|
||||||
|
}
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||||
viewModel.cursorLeft()
|
viewModel.cursorLeft()
|
||||||
return true
|
return true
|
||||||
@@ -501,26 +533,58 @@ fun MainScreen(
|
|||||||
val isCrosshairRuler by viewModel.isCrosshairRuler.collectAsState()
|
val isCrosshairRuler by viewModel.isCrosshairRuler.collectAsState()
|
||||||
val cursorStyle by viewModel.cursorStyle.collectAsState()
|
val cursorStyle by viewModel.cursorStyle.collectAsState()
|
||||||
val blockSelectMode by viewModel.blockSelectMode.collectAsState()
|
val blockSelectMode by viewModel.blockSelectMode.collectAsState()
|
||||||
|
val blockPaste by viewModel.blockPaste.collectAsState()
|
||||||
val uiTheme by viewModel.uiTheme.collectAsState()
|
val uiTheme by viewModel.uiTheme.collectAsState()
|
||||||
val isInsertMode by viewModel.isInsertMode.collectAsState()
|
val isInsertMode by viewModel.isInsertMode.collectAsState()
|
||||||
|
val isDocMode by viewModel.isDocMode.collectAsState()
|
||||||
|
val isWordWrap by viewModel.isWordWrap.collectAsState()
|
||||||
|
val isAplMode by viewModel.isAplMode.collectAsState()
|
||||||
|
val fourColorOverride by viewModel.fourColorOverride.collectAsState()
|
||||||
|
val trimRectSizingHandles by viewModel.trimRectSizingHandles.collectAsState()
|
||||||
|
val trimRectRemainAfterEdit by viewModel.trimRectRemainAfterEdit.collectAsState()
|
||||||
|
val pasteWordBreak by viewModel.pasteWordBreak.collectAsState()
|
||||||
|
val hotspotsEnabled by viewModel.hotspotsEnabled.collectAsState()
|
||||||
|
val hotspotsUrlEnabled by viewModel.hotspotsUrlEnabled.collectAsState()
|
||||||
|
val hotspotsPfEnabled by viewModel.hotspotsPfEnabled.collectAsState()
|
||||||
|
val hotspotsMenuChoicesEnabled by viewModel.hotspotsMenuChoicesEnabled.collectAsState()
|
||||||
|
|
||||||
val isNumericField = remember(screenBuffer, cursorAddr, screenVersion) {
|
val isDbcsSession = remember(screenBuffer, screenVersion) {
|
||||||
if (screenBuffer != null && screenBuffer?.isFormatted == true) {
|
screenBuffer?.translator?.isDBCS == true
|
||||||
val fa = screenBuffer?.getFieldAttributeAt(cursorAddr) ?: 0.toByte()
|
|
||||||
(fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0
|
|
||||||
} else false
|
|
||||||
}
|
}
|
||||||
val modelName = remember(rows, cols) {
|
|
||||||
when {
|
val fieldStatus = remember(screenBuffer, cursorAddr, screenVersion) {
|
||||||
rows == 24 && cols == 80 -> "3279-2"
|
val buf = screenBuffer
|
||||||
rows == 32 && cols == 80 -> "3279-3"
|
if (buf != null && buf.isFormatted) {
|
||||||
rows == 43 && cols == 80 -> "3279-4"
|
val cell = buf.getCell(cursorAddr)
|
||||||
rows == 27 && cols == 132 -> "3279-5"
|
val isDbcs = (cell != null && (cell.db == haus.nightmare.lib3270j.screen.ExtendedAttribute.DB_LEFT ||
|
||||||
else -> "DYNAMIC"
|
cell.db == haus.nightmare.lib3270j.screen.ExtendedAttribute.DB_RIGHT ||
|
||||||
|
cell.cs == haus.nightmare.lib3270j.screen.ExtendedAttribute.CS_DBCS))
|
||||||
|
if (isDbcs) {
|
||||||
|
"DBCS"
|
||||||
|
} else {
|
||||||
|
val fa = buf.getFieldAttributeAt(cursorAddr)
|
||||||
|
if ((fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0) {
|
||||||
|
"NUM"
|
||||||
|
} else {
|
||||||
|
"ALPHA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"ALPHA"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isNumericField = remember(fieldStatus) {
|
||||||
|
fieldStatus == "NUM"
|
||||||
|
}
|
||||||
|
|
||||||
|
val activity = context as? ComponentActivity
|
||||||
|
val openConnectFromIntent = remember {
|
||||||
|
activity?.intent?.getBooleanExtra("OPEN_CONNECT_DIALOG", false) == true
|
||||||
|
}
|
||||||
|
|
||||||
var showConnectDialog by remember { mutableStateOf(false) }
|
var showConnectDialog by remember { mutableStateOf(false) }
|
||||||
|
var showConnectChoiceDialog by remember { mutableStateOf(false) }
|
||||||
var showFtDialog by remember { mutableStateOf(false) }
|
var showFtDialog by remember { mutableStateOf(false) }
|
||||||
var showSettingsDialog by remember { mutableStateOf(false) }
|
var showSettingsDialog by remember { mutableStateOf(false) }
|
||||||
var showFieldInspectorDialog by remember { mutableStateOf(false) }
|
var showFieldInspectorDialog by remember { mutableStateOf(false) }
|
||||||
@@ -528,9 +592,14 @@ fun MainScreen(
|
|||||||
var showScreenExportDialog by remember { mutableStateOf(false) }
|
var showScreenExportDialog by remember { mutableStateOf(false) }
|
||||||
var showScriptDialog by remember { mutableStateOf(false) }
|
var showScriptDialog by remember { mutableStateOf(false) }
|
||||||
var showPrinterSessionDialog by remember { mutableStateOf(false) }
|
var showPrinterSessionDialog by remember { mutableStateOf(false) }
|
||||||
|
var showScreenHistoryDialog by remember { mutableStateOf(false) }
|
||||||
|
var showMacroDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(isFindDialogOpen) {
|
LaunchedEffect(isFindDialogOpen) {
|
||||||
if (isFindDialogOpen) {
|
if (isFindDialogOpen) {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
showFindDialog = true
|
showFindDialog = true
|
||||||
onSetFindDialogOpen(false)
|
onSetFindDialogOpen(false)
|
||||||
}
|
}
|
||||||
@@ -538,13 +607,12 @@ fun MainScreen(
|
|||||||
|
|
||||||
LaunchedEffect(isFtDialogOpen) {
|
LaunchedEffect(isFtDialogOpen) {
|
||||||
if (isFtDialogOpen) {
|
if (isFtDialogOpen) {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
showFtDialog = true
|
showFtDialog = true
|
||||||
onSetFtDialogOpen(false)
|
onSetFtDialogOpen(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
|
||||||
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
DisposableEffect(lifecycleOwner) {
|
DisposableEffect(lifecycleOwner) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
@@ -561,30 +629,17 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
if (openConnectFromIntent) {
|
||||||
|
activity?.intent?.removeExtra("OPEN_CONNECT_DIALOG")
|
||||||
|
showConnectDialog = true
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
val startupBehavior = AppSettings.getStartupBehavior(context)
|
val startupBehavior = AppSettings.getStartupBehavior(context)
|
||||||
when (startupBehavior) {
|
when (startupBehavior) {
|
||||||
AppSettings.StartupBehavior.AUTO_CONNECT -> {
|
AppSettings.StartupBehavior.AUTO_CONNECT -> {
|
||||||
val autoHost = HostStorage.getAutoConnectHost(context)
|
val autoHost = HostStorage.getAutoConnectHost(context)
|
||||||
if (autoHost != null && !connectionState.isConnected()) {
|
if (autoHost != null && !connectionState.isConnected()) {
|
||||||
viewModel.connect(
|
viewModel.connect(autoHost.toConnectionConfig())
|
||||||
autoHost.host,
|
|
||||||
autoHost.port,
|
|
||||||
autoHost.model,
|
|
||||||
autoHost.dynamicRows,
|
|
||||||
autoHost.dynamicCols,
|
|
||||||
autoHost.luName,
|
|
||||||
autoHost.hostType,
|
|
||||||
autoHost.useTls,
|
|
||||||
autoHost.tlsVerifyCert,
|
|
||||||
autoHost.tn3270e,
|
|
||||||
autoHost.graphicsMode,
|
|
||||||
autoHost.codePage,
|
|
||||||
autoHost.proxyType,
|
|
||||||
autoHost.proxyHost,
|
|
||||||
autoHost.proxyPort,
|
|
||||||
autoHost.proxyUsername,
|
|
||||||
autoHost.proxyPassword
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppSettings.StartupBehavior.SHOW_CONNECT -> {
|
AppSettings.StartupBehavior.SHOW_CONNECT -> {
|
||||||
@@ -636,6 +691,26 @@ fun MainScreen(
|
|||||||
cursorStyle = cursorStyle,
|
cursorStyle = cursorStyle,
|
||||||
isInsertMode = isInsertMode,
|
isInsertMode = isInsertMode,
|
||||||
blockSelectMode = blockSelectMode,
|
blockSelectMode = blockSelectMode,
|
||||||
|
blockPaste = blockPaste,
|
||||||
|
onToggleBlockSelectMode = { viewModel.setBlockSelectMode(!blockSelectMode) },
|
||||||
|
onToggleBlockPaste = { viewModel.setBlockPaste(!blockPaste) },
|
||||||
|
trimRectSizingHandles = trimRectSizingHandles,
|
||||||
|
trimRectRemainAfterEdit = trimRectRemainAfterEdit,
|
||||||
|
hotspotsEnabled = hotspotsEnabled,
|
||||||
|
hotspotEngine = viewModel.hotspotEngine,
|
||||||
|
onSendHotspotAction = { action -> viewModel.sendKeys(action) },
|
||||||
|
onOpenUrl = { url ->
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(context, "Cannot open URL: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onToggleHotspots = { viewModel.toggleHotspots() },
|
||||||
|
fourColorOverride = fourColorOverride,
|
||||||
onPasteLineWrap = { text, wrapCol, wordWrap ->
|
onPasteLineWrap = { text, wrapCol, wordWrap ->
|
||||||
viewModel.pasteLineWrap(text, wrapCol, wordWrap)
|
viewModel.pasteLineWrap(text, wrapCol, wordWrap)
|
||||||
},
|
},
|
||||||
@@ -660,6 +735,7 @@ fun MainScreen(
|
|||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
TerminalInputView(ctx).apply {
|
TerminalInputView(ctx).apply {
|
||||||
|
this.isDbcsSession = isDbcsSession
|
||||||
onInputText = { text ->
|
onInputText = { text ->
|
||||||
viewModel.typeString(text)
|
viewModel.typeString(text)
|
||||||
onClearShift()
|
onClearShift()
|
||||||
@@ -681,6 +757,7 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { inputView ->
|
update = { inputView ->
|
||||||
|
inputView.isDbcsSession = isDbcsSession
|
||||||
terminalInputViewRef = inputView
|
terminalInputViewRef = inputView
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(1.dp)
|
modifier = Modifier.size(1.dp)
|
||||||
@@ -691,18 +768,68 @@ fun MainScreen(
|
|||||||
connectionState = connectionState,
|
connectionState = connectionState,
|
||||||
isShiftPressed = isShiftPressed,
|
isShiftPressed = isShiftPressed,
|
||||||
isLightPenMode = isLightPenMode,
|
isLightPenMode = isLightPenMode,
|
||||||
|
isDocMode = isDocMode,
|
||||||
|
isWordWrap = isWordWrap,
|
||||||
|
isAplMode = isAplMode,
|
||||||
|
isInsertMode = isInsertMode,
|
||||||
|
fourColorOverride = fourColorOverride,
|
||||||
hapticFeedbackEnabled = hapticFeedback,
|
hapticFeedbackEnabled = hapticFeedback,
|
||||||
onClearShift = onClearShift,
|
onClearShift = onClearShift,
|
||||||
onConnectClick = { showConnectDialog = true },
|
onConnectClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
if (connectionState.isConnected()) {
|
||||||
|
showConnectChoiceDialog = true
|
||||||
|
} else {
|
||||||
|
showConnectDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
onDisconnectClick = { viewModel.disconnect() },
|
onDisconnectClick = { viewModel.disconnect() },
|
||||||
onFtClick = { showFtDialog = true },
|
onFtClick = {
|
||||||
onFieldInspectorClick = { showFieldInspectorDialog = true },
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
onFindClick = { showFindDialog = true },
|
showFtDialog = true
|
||||||
onScreenExportClick = { showScreenExportDialog = true },
|
},
|
||||||
onScriptClick = { showScriptDialog = true },
|
onFieldInspectorClick = {
|
||||||
onPrinterSessionClick = { showPrinterSessionDialog = true },
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showFieldInspectorDialog = true
|
||||||
|
},
|
||||||
|
onFindClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showFindDialog = true
|
||||||
|
},
|
||||||
|
onScreenExportClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showScreenExportDialog = true
|
||||||
|
},
|
||||||
|
onScreenHistoryClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showScreenHistoryDialog = true
|
||||||
|
},
|
||||||
|
onScriptClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showScriptDialog = true
|
||||||
|
},
|
||||||
|
onMacroRuntimeClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showMacroDialog = true
|
||||||
|
},
|
||||||
|
onPrinterSessionClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showPrinterSessionDialog = true
|
||||||
|
},
|
||||||
onToggleLightPen = { viewModel.toggleLightPen() },
|
onToggleLightPen = { viewModel.toggleLightPen() },
|
||||||
onSettingsClick = { showSettingsDialog = true },
|
onToggleDocMode = { viewModel.toggleDocMode() },
|
||||||
|
onToggleWordWrap = { viewModel.toggleWordWrap() },
|
||||||
|
onToggleAplMode = { viewModel.toggleAplMode() },
|
||||||
|
onToggleInsert = { viewModel.toggleInsert() },
|
||||||
|
onToggleFourColorOverride = {
|
||||||
|
val next = !fourColorOverride
|
||||||
|
viewModel.setFourColorOverride(next)
|
||||||
|
AppSettings.setFourColorOverride(context, next)
|
||||||
|
},
|
||||||
|
onSettingsClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showSettingsDialog = true
|
||||||
|
},
|
||||||
onSendAid = { aid ->
|
onSendAid = { aid ->
|
||||||
viewModel.sendAid(aid)
|
viewModel.sendAid(aid)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
@@ -769,23 +896,83 @@ fun MainScreen(
|
|||||||
isTls = isTlsActive,
|
isTls = isTlsActive,
|
||||||
isTlsVerified = isTlsVerified,
|
isTlsVerified = isTlsVerified,
|
||||||
graphicsMode = defaultGraphicsMode,
|
graphicsMode = defaultGraphicsMode,
|
||||||
codePage = codePage,
|
|
||||||
isLightPenMode = isLightPenMode,
|
isLightPenMode = isLightPenMode,
|
||||||
isInsertMode = isInsertMode,
|
isInsertMode = isInsertMode,
|
||||||
|
isDocMode = isDocMode,
|
||||||
|
isWordWrap = isWordWrap,
|
||||||
|
isAplMode = isAplMode,
|
||||||
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
||||||
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
|
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
|
||||||
isNumericField = isNumericField,
|
isNumericField = isNumericField,
|
||||||
modelName = modelName
|
fieldStatus = fieldStatus,
|
||||||
|
uiTheme = uiTheme,
|
||||||
|
onToggleDocMode = { viewModel.toggleDocMode() },
|
||||||
|
onToggleWordWrap = { viewModel.toggleWordWrap() },
|
||||||
|
onToggleAplMode = { viewModel.toggleAplMode() },
|
||||||
|
onToggleInsert = { viewModel.toggleInsert() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showConnectChoiceDialog) {
|
||||||
|
ConnectChoiceDialog(
|
||||||
|
currentTarget = viewModel.getConnectedTarget(),
|
||||||
|
onChoice = { choice ->
|
||||||
|
showConnectChoiceDialog = false
|
||||||
|
when (choice) {
|
||||||
|
ConnectChoice.NEW_WINDOW -> {
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||||
|
putExtra("OPEN_CONNECT_DIALOG", true)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
ConnectChoice.ABORT_CURRENT -> {
|
||||||
|
viewModel.disconnect()
|
||||||
|
showConnectDialog = true
|
||||||
|
}
|
||||||
|
ConnectChoice.CANCEL -> {
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showConnectChoiceDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showConnectDialog) {
|
if (showConnectDialog) {
|
||||||
ConnectDialog(
|
ConnectDialog(
|
||||||
onDismiss = { showConnectDialog = false },
|
onDismiss = {
|
||||||
onConnect = { host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass ->
|
|
||||||
showConnectDialog = false
|
showConnectDialog = false
|
||||||
viewModel.connect(host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
|
onConnect = { host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass, savedConn, savedProfId, savedProfName ->
|
||||||
|
showConnectDialog = false
|
||||||
|
viewModel.connect(
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
modelNum = model,
|
||||||
|
dynamicRows = dynamicRows,
|
||||||
|
dynamicCols = dynamicCols,
|
||||||
|
luName = luName,
|
||||||
|
hostType = hostType,
|
||||||
|
useTls = useTls,
|
||||||
|
tlsVerifyCert = tlsVerifyCert,
|
||||||
|
tn3270e = tn3270e,
|
||||||
|
graphicsModeStr = graphicsMode,
|
||||||
|
codePageStr = cp,
|
||||||
|
proxyTypeStr = pType,
|
||||||
|
proxyHost = pHost,
|
||||||
|
proxyPort = pPort,
|
||||||
|
proxyUser = pUser,
|
||||||
|
proxyPass = pPass,
|
||||||
|
savedConnection = savedConn,
|
||||||
|
savedProfileId = savedProfId,
|
||||||
|
savedProfileName = savedProfName
|
||||||
|
)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -796,7 +983,10 @@ fun MainScreen(
|
|||||||
initialHostType = viewModel.activeHostType,
|
initialHostType = viewModel.activeHostType,
|
||||||
ftProgressState = ftState,
|
ftProgressState = ftState,
|
||||||
client = viewModel.getClient(),
|
client = viewModel.getClient(),
|
||||||
onDismiss = { showFtDialog = false },
|
onDismiss = {
|
||||||
|
showFtDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onStartTransfer = { config ->
|
onStartTransfer = { config ->
|
||||||
viewModel.startFileTransfer(config) { error ->
|
viewModel.startFileTransfer(config) { error ->
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
@@ -822,20 +1012,66 @@ fun MainScreen(
|
|||||||
initialCrosshairRuler = isCrosshairRuler,
|
initialCrosshairRuler = isCrosshairRuler,
|
||||||
initialCursorStyle = cursorStyle,
|
initialCursorStyle = cursorStyle,
|
||||||
initialBlockSelectMode = blockSelectMode,
|
initialBlockSelectMode = blockSelectMode,
|
||||||
|
initialBlockPaste = blockPaste,
|
||||||
|
initialTrimRectSizingHandles = trimRectSizingHandles,
|
||||||
|
initialTrimRectRemainAfterEdit = trimRectRemainAfterEdit,
|
||||||
|
initialPasteWordBreak = pasteWordBreak,
|
||||||
|
initialHotspotsEnabled = hotspotsEnabled,
|
||||||
|
initialHotspotsUrlEnabled = hotspotsUrlEnabled,
|
||||||
|
initialHotspotsPfEnabled = hotspotsPfEnabled,
|
||||||
|
initialHotspotsMenuChoicesEnabled = hotspotsMenuChoicesEnabled,
|
||||||
initialStartupBehavior = AppSettings.getStartupBehavior(context).name,
|
initialStartupBehavior = AppSettings.getStartupBehavior(context).name,
|
||||||
initialDynamicRows = AppSettings.getDynamicRows(context),
|
initialDynamicRows = AppSettings.getDynamicRows(context),
|
||||||
initialDynamicCols = AppSettings.getDynamicCols(context),
|
initialDynamicCols = AppSettings.getDynamicCols(context),
|
||||||
onDismiss = { showSettingsDialog = false },
|
initialEntryAssistDocMode = isDocMode,
|
||||||
onSave = { mask, blink, haptic, verify, gfx, defaultCp, theme, crosshair, style, blockSel, startup, dynRows, dynCols ->
|
initialEntryAssistWordWrap = isWordWrap,
|
||||||
|
initialEntryAssistStartCol = AppSettings.getEntryAssistStartCol(context),
|
||||||
|
initialEntryAssistEndCol = AppSettings.getEntryAssistEndCol(context),
|
||||||
|
initialEntryAssistBell = AppSettings.getEntryAssistBell(context),
|
||||||
|
initialEntryAssistBellCol = AppSettings.getEntryAssistBellCol(context),
|
||||||
|
initialEntryAssistTabStops = AppSettings.getEntryAssistTabStops(context),
|
||||||
|
initialInsertOffOnAid = AppSettings.getInsertOffOnAid(context),
|
||||||
|
initialFourColorOverride = fourColorOverride,
|
||||||
|
initialNumericFieldLock = AppSettings.getNumericFieldLock(context),
|
||||||
|
initialAutoSkipEnabled = AppSettings.getAutoSkipEnabled(context),
|
||||||
|
initialDefaultHostType = AppSettings.getDefaultHostType(context),
|
||||||
|
onDismiss = {
|
||||||
|
showSettingsDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
|
onSave = { mask, blink, haptic, verify, gfx, defaultCp, theme, crosshair, style, blockSel, blockPst,
|
||||||
|
sizingHandles, remainAfterEdit, wordBreak, hsEnabled, hsUrl, hsPf, hsChoices,
|
||||||
|
startup, dynRows, dynCols, doc, wrap, startCol, endCol, bell, bellCol, tabs, resetIns, fourCol, numLk, autoSk, defHostType ->
|
||||||
showSettingsDialog = false
|
showSettingsDialog = false
|
||||||
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
|
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
|
||||||
viewModel.setUiTheme(theme)
|
viewModel.setUiTheme(theme)
|
||||||
viewModel.setCrosshairRuler(crosshair)
|
viewModel.setCrosshairRuler(crosshair)
|
||||||
viewModel.setCursorStyle(style)
|
viewModel.setCursorStyle(style)
|
||||||
viewModel.setBlockSelectMode(blockSel)
|
viewModel.setBlockSelectMode(blockSel)
|
||||||
|
viewModel.setBlockPaste(blockPst)
|
||||||
|
viewModel.setTrimRectSizingHandles(sizingHandles)
|
||||||
|
viewModel.setTrimRectRemainAfterEdit(remainAfterEdit)
|
||||||
|
viewModel.setPasteWordBreak(wordBreak)
|
||||||
|
viewModel.setHotspotsEnabled(hsEnabled)
|
||||||
|
viewModel.setHotspotsUrlEnabled(hsUrl)
|
||||||
|
viewModel.setHotspotsPfEnabled(hsPf)
|
||||||
|
viewModel.setHotspotsMenuChoicesEnabled(hsChoices)
|
||||||
AppSettings.setStartupBehavior(context, AppSettings.StartupBehavior.valueOf(startup))
|
AppSettings.setStartupBehavior(context, AppSettings.StartupBehavior.valueOf(startup))
|
||||||
AppSettings.setDynamicRows(context, dynRows)
|
AppSettings.setDynamicRows(context, dynRows)
|
||||||
AppSettings.setDynamicCols(context, dynCols)
|
AppSettings.setDynamicCols(context, dynCols)
|
||||||
|
AppSettings.setEntryAssistDocMode(context, doc)
|
||||||
|
AppSettings.setEntryAssistWordWrap(context, wrap)
|
||||||
|
AppSettings.setEntryAssistStartCol(context, startCol)
|
||||||
|
AppSettings.setEntryAssistEndCol(context, endCol)
|
||||||
|
AppSettings.setEntryAssistBell(context, bell)
|
||||||
|
AppSettings.setEntryAssistBellCol(context, bellCol)
|
||||||
|
AppSettings.setEntryAssistTabStops(context, tabs)
|
||||||
|
AppSettings.setInsertOffOnAid(context, resetIns)
|
||||||
|
AppSettings.setFourColorOverride(context, fourCol)
|
||||||
|
AppSettings.setNumericFieldLock(context, numLk)
|
||||||
|
AppSettings.setAutoSkipEnabled(context, autoSk)
|
||||||
|
AppSettings.setDefaultHostType(context, defHostType)
|
||||||
|
viewModel.reloadSettings()
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -845,7 +1081,10 @@ fun MainScreen(
|
|||||||
FieldInspectorDialog(
|
FieldInspectorDialog(
|
||||||
client = viewModel.getClient(),
|
client = viewModel.getClient(),
|
||||||
screenVersion = screenVersion,
|
screenVersion = screenVersion,
|
||||||
onDismiss = { showFieldInspectorDialog = false },
|
onDismiss = {
|
||||||
|
showFieldInspectorDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onJumpToField = { addr ->
|
onJumpToField = { addr ->
|
||||||
viewModel.setCursor(addr)
|
viewModel.setCursor(addr)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
@@ -856,7 +1095,10 @@ fun MainScreen(
|
|||||||
if (showFindDialog) {
|
if (showFindDialog) {
|
||||||
FindDialog(
|
FindDialog(
|
||||||
cols = cols,
|
cols = cols,
|
||||||
onDismiss = { showFindDialog = false },
|
onDismiss = {
|
||||||
|
showFindDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onFind = { query: String, matchCase: Boolean, forward: Boolean ->
|
onFind = { query: String, matchCase: Boolean, forward: Boolean ->
|
||||||
viewModel.find(query, matchCase, forward)
|
viewModel.find(query, matchCase, forward)
|
||||||
},
|
},
|
||||||
@@ -874,13 +1116,19 @@ fun MainScreen(
|
|||||||
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
||||||
graphicsPlane = viewModel.getClient()?.graphicsPlane,
|
graphicsPlane = viewModel.getClient()?.graphicsPlane,
|
||||||
maskHidden = maskHiddenInput,
|
maskHidden = maskHiddenInput,
|
||||||
onDismiss = { showScreenExportDialog = false }
|
onDismiss = {
|
||||||
|
showScreenExportDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showScriptDialog) {
|
if (showScriptDialog) {
|
||||||
ScriptDialog(
|
ScriptDialog(
|
||||||
onDismiss = { showScriptDialog = false },
|
onDismiss = {
|
||||||
|
showScriptDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onExecuteScript = { script: String, onStatus: (String) -> Unit, onComplete: (String?) -> Unit ->
|
onExecuteScript = { script: String, onStatus: (String) -> Unit, onComplete: (String?) -> Unit ->
|
||||||
viewModel.executeScript(script, onStatus, onComplete)
|
viewModel.executeScript(script, onStatus, onComplete)
|
||||||
}
|
}
|
||||||
@@ -893,7 +1141,30 @@ fun MainScreen(
|
|||||||
initialHost = activeHost,
|
initialHost = activeHost,
|
||||||
initialCodePage = codePage,
|
initialCodePage = codePage,
|
||||||
initialUseTls = isTlsActive,
|
initialUseTls = isTlsActive,
|
||||||
onDismiss = { showPrinterSessionDialog = false }
|
onDismiss = {
|
||||||
|
showPrinterSessionDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showScreenHistoryDialog) {
|
||||||
|
ScreenHistoryDialog(
|
||||||
|
historyBuffer = viewModel.getScreenHistoryBuffer(),
|
||||||
|
onDismiss = {
|
||||||
|
showScreenHistoryDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showMacroDialog) {
|
||||||
|
MacroDialog(
|
||||||
|
client = viewModel.getClient(),
|
||||||
|
onDismiss = {
|
||||||
|
showMacroDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ import haus.nightmare.lib3270j.listener.ConnectionListener
|
|||||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer
|
||||||
import haus.nightmare.a3270.ft.FileTransfer
|
import haus.nightmare.a3270.ft.FileTransfer
|
||||||
import haus.nightmare.a3270.service.TerminalService
|
import haus.nightmare.a3270.service.TerminalService
|
||||||
import haus.nightmare.a3270.storage.AppSettings
|
import haus.nightmare.a3270.storage.AppSettings
|
||||||
|
import haus.nightmare.a3270.ui.HotspotEngine
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
@@ -53,8 +56,16 @@ sealed interface TerminalInputAction {
|
|||||||
data object Dup : TerminalInputAction
|
data object Dup : TerminalInputAction
|
||||||
data object FieldMark : TerminalInputAction
|
data object FieldMark : TerminalInputAction
|
||||||
data object ToggleInsert : TerminalInputAction
|
data object ToggleInsert : TerminalInputAction
|
||||||
|
data object ToggleDocMode : TerminalInputAction
|
||||||
|
data object ToggleWordWrap : TerminalInputAction
|
||||||
|
data object ToggleApl : TerminalInputAction
|
||||||
|
data class SetDocMode(val enabled: Boolean) : TerminalInputAction
|
||||||
|
data class SetWordWrap(val enabled: Boolean) : TerminalInputAction
|
||||||
|
data class SetAplMode(val enabled: Boolean) : TerminalInputAction
|
||||||
data class SetCursor(val baddr: Int) : TerminalInputAction
|
data class SetCursor(val baddr: Int) : TerminalInputAction
|
||||||
data class LightPenSelect(val baddr: Int) : TerminalInputAction
|
data class LightPenSelect(val baddr: Int) : TerminalInputAction
|
||||||
|
data class PasteText(val text: String) : TerminalInputAction
|
||||||
|
data class SendKeysAction(val keys: String) : TerminalInputAction
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UntrustedCertPromptState(
|
data class UntrustedCertPromptState(
|
||||||
@@ -155,12 +166,101 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _blockSelectMode = MutableStateFlow(AppSettings.isBlockSelectModeEnabled(application))
|
private val _blockSelectMode = MutableStateFlow(AppSettings.isBlockSelectModeEnabled(application))
|
||||||
val blockSelectMode: StateFlow<Boolean> = _blockSelectMode.asStateFlow()
|
val blockSelectMode: StateFlow<Boolean> = _blockSelectMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _blockPaste = MutableStateFlow(AppSettings.isBlockPasteEnabled(application))
|
||||||
|
val blockPaste: StateFlow<Boolean> = _blockPaste.asStateFlow()
|
||||||
|
|
||||||
private val _uiTheme = MutableStateFlow(AppSettings.getUiTheme(application))
|
private val _uiTheme = MutableStateFlow(AppSettings.getUiTheme(application))
|
||||||
val uiTheme: StateFlow<String> = _uiTheme.asStateFlow()
|
val uiTheme: StateFlow<String> = _uiTheme.asStateFlow()
|
||||||
|
|
||||||
private val _isInsertMode = MutableStateFlow(false)
|
private val _isInsertMode = MutableStateFlow(false)
|
||||||
val isInsertMode: StateFlow<Boolean> = _isInsertMode.asStateFlow()
|
val isInsertMode: StateFlow<Boolean> = _isInsertMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _isDocMode = MutableStateFlow(AppSettings.getEntryAssistDocMode(application))
|
||||||
|
val isDocMode: StateFlow<Boolean> = _isDocMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _isWordWrap = MutableStateFlow(AppSettings.getEntryAssistWordWrap(application))
|
||||||
|
val isWordWrap: StateFlow<Boolean> = _isWordWrap.asStateFlow()
|
||||||
|
|
||||||
|
private val _isAplMode = MutableStateFlow(false)
|
||||||
|
val isAplMode: StateFlow<Boolean> = _isAplMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _fourColorOverride = MutableStateFlow(AppSettings.getFourColorOverride(application))
|
||||||
|
val fourColorOverride: StateFlow<Boolean> = _fourColorOverride.asStateFlow()
|
||||||
|
|
||||||
|
private val _hotspotsEnabled = MutableStateFlow(AppSettings.isHotspotsEnabled(application))
|
||||||
|
val hotspotsEnabled: StateFlow<Boolean> = _hotspotsEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _hotspotsUrlEnabled = MutableStateFlow(AppSettings.isHotspotsUrlEnabled(application))
|
||||||
|
val hotspotsUrlEnabled: StateFlow<Boolean> = _hotspotsUrlEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _hotspotsPfEnabled = MutableStateFlow(AppSettings.isHotspotsPfEnabled(application))
|
||||||
|
val hotspotsPfEnabled: StateFlow<Boolean> = _hotspotsPfEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _hotspotsMenuChoicesEnabled = MutableStateFlow(AppSettings.isHotspotsMenuChoicesEnabled(application))
|
||||||
|
val hotspotsMenuChoicesEnabled: StateFlow<Boolean> = _hotspotsMenuChoicesEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _trimRectSizingHandles = MutableStateFlow(AppSettings.isTrimRectSizingHandlesEnabled(application))
|
||||||
|
val trimRectSizingHandles: StateFlow<Boolean> = _trimRectSizingHandles.asStateFlow()
|
||||||
|
|
||||||
|
private val _trimRectRemainAfterEdit = MutableStateFlow(AppSettings.isTrimRectRemainAfterEditEnabled(application))
|
||||||
|
val trimRectRemainAfterEdit: StateFlow<Boolean> = _trimRectRemainAfterEdit.asStateFlow()
|
||||||
|
|
||||||
|
private val _pasteWordBreak = MutableStateFlow(AppSettings.isPasteWordBreakEnabled(application))
|
||||||
|
val pasteWordBreak: StateFlow<Boolean> = _pasteWordBreak.asStateFlow()
|
||||||
|
|
||||||
|
val hotspotEngine = HotspotEngine().apply {
|
||||||
|
urlEnabled = _hotspotsUrlEnabled.value
|
||||||
|
pfKeysEnabled = _hotspotsPfEnabled.value
|
||||||
|
menuChoicesEnabled = _hotspotsMenuChoicesEnabled.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHotspotsEnabled(enabled: Boolean) {
|
||||||
|
AppSettings.setHotspotsEnabled(getApplication(), enabled)
|
||||||
|
_hotspotsEnabled.value = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleHotspots() {
|
||||||
|
setHotspotsEnabled(!_hotspotsEnabled.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHotspotsUrlEnabled(enabled: Boolean) {
|
||||||
|
AppSettings.setHotspotsUrlEnabled(getApplication(), enabled)
|
||||||
|
_hotspotsUrlEnabled.value = enabled
|
||||||
|
hotspotEngine.urlEnabled = enabled
|
||||||
|
hotspotEngine.invalidateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHotspotsPfEnabled(enabled: Boolean) {
|
||||||
|
AppSettings.setHotspotsPfEnabled(getApplication(), enabled)
|
||||||
|
_hotspotsPfEnabled.value = enabled
|
||||||
|
hotspotEngine.pfKeysEnabled = enabled
|
||||||
|
hotspotEngine.invalidateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHotspotsMenuChoicesEnabled(enabled: Boolean) {
|
||||||
|
AppSettings.setHotspotsMenuChoicesEnabled(getApplication(), enabled)
|
||||||
|
_hotspotsMenuChoicesEnabled.value = enabled
|
||||||
|
hotspotEngine.menuChoicesEnabled = enabled
|
||||||
|
hotspotEngine.invalidateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTrimRectSizingHandles(enabled: Boolean) {
|
||||||
|
AppSettings.setTrimRectSizingHandlesEnabled(getApplication(), enabled)
|
||||||
|
_trimRectSizingHandles.value = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTrimRectRemainAfterEdit(enabled: Boolean) {
|
||||||
|
AppSettings.setTrimRectRemainAfterEditEnabled(getApplication(), enabled)
|
||||||
|
_trimRectRemainAfterEdit.value = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPasteWordBreak(enabled: Boolean) {
|
||||||
|
val app = getApplication<Application>()
|
||||||
|
AppSettings.setPasteWordBreakEnabled(app, enabled)
|
||||||
|
_pasteWordBreak.value = enabled
|
||||||
|
applyModeSettings()
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleLightPen() {
|
fun toggleLightPen() {
|
||||||
_isLightPenMode.value = !_isLightPenMode.value
|
_isLightPenMode.value = !_isLightPenMode.value
|
||||||
}
|
}
|
||||||
@@ -188,11 +288,95 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_blockSelectMode.value = enabled
|
_blockSelectMode.value = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setBlockPaste(enabled: Boolean) {
|
||||||
|
val app = getApplication<Application>()
|
||||||
|
AppSettings.setBlockPasteEnabled(app, enabled)
|
||||||
|
_blockPaste.value = enabled
|
||||||
|
applyModeSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleBlockPaste() {
|
||||||
|
setBlockPaste(!_blockPaste.value)
|
||||||
|
}
|
||||||
|
|
||||||
fun setUiTheme(theme: String) {
|
fun setUiTheme(theme: String) {
|
||||||
AppSettings.setUiTheme(getApplication(), theme)
|
AppSettings.setUiTheme(getApplication(), theme)
|
||||||
_uiTheme.value = theme
|
_uiTheme.value = theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggleDocMode() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.ToggleDocMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDocMode(enabled: Boolean) {
|
||||||
|
inputChannel.trySend(TerminalInputAction.SetDocMode(enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleWordWrap() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.ToggleWordWrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWordWrap(enabled: Boolean) {
|
||||||
|
inputChannel.trySend(TerminalInputAction.SetWordWrap(enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAplMode() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.ToggleApl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAplMode(enabled: Boolean) {
|
||||||
|
inputChannel.trySend(TerminalInputAction.SetAplMode(enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFourColorOverride(override: Boolean) {
|
||||||
|
AppSettings.setFourColorOverride(getApplication(), override)
|
||||||
|
_fourColorOverride.value = override
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleFourColorOverride() {
|
||||||
|
setFourColorOverride(!_fourColorOverride.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playBellSound() {
|
||||||
|
try {
|
||||||
|
val tg = android.media.ToneGenerator(android.media.AudioManager.STREAM_NOTIFICATION, 80)
|
||||||
|
tg.startTone(android.media.ToneGenerator.TONE_PROP_BEEP, 150)
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyModeSettings() {
|
||||||
|
val app = getApplication<Application>()
|
||||||
|
val c = client ?: return
|
||||||
|
val sb = c.screenBuffer
|
||||||
|
if (sb != null) {
|
||||||
|
sb.isEntryAssistDOCmode = AppSettings.getEntryAssistDocMode(app)
|
||||||
|
sb.isEntryAssistWordWrap = AppSettings.getEntryAssistWordWrap(app)
|
||||||
|
sb.setLeftMargin((AppSettings.getEntryAssistStartCol(app) - 1).coerceAtLeast(0))
|
||||||
|
sb.setRightMargin((AppSettings.getEntryAssistEndCol(app) - 1).coerceAtLeast(0))
|
||||||
|
sb.setWordTabPositions(AppSettings.getEntryAssistTabStopsArray(app))
|
||||||
|
}
|
||||||
|
val ip = c.inputProcessor
|
||||||
|
if (ip != null) {
|
||||||
|
ip.isBellEnabled = AppSettings.getEntryAssistBell(app)
|
||||||
|
ip.bellColumn = (AppSettings.getEntryAssistBellCol(app) - 1).coerceAtLeast(0)
|
||||||
|
ip.isInsertOffOnAid = AppSettings.getInsertOffOnAid(app)
|
||||||
|
ip.isNumericFieldLock = AppSettings.getNumericFieldLock(app)
|
||||||
|
ip.isAutoSkipEnabled = AppSettings.getAutoSkipEnabled(app)
|
||||||
|
ip.isBlockPaste = AppSettings.isBlockPasteEnabled(app)
|
||||||
|
ip.isPasteWordBreak = AppSettings.isPasteWordBreakEnabled(app)
|
||||||
|
}
|
||||||
|
c.ps?.isBlockPaste = AppSettings.isBlockPasteEnabled(app)
|
||||||
|
c.ps?.isPasteWordBreak = AppSettings.isPasteWordBreakEnabled(app)
|
||||||
|
c.isPasteWordBreak = AppSettings.isPasteWordBreakEnabled(app)
|
||||||
|
_fourColorOverride.value = AppSettings.getFourColorOverride(app)
|
||||||
|
_isDocMode.value = c.isDocMode
|
||||||
|
_isWordWrap.value = c.isWordWrap
|
||||||
|
_isAplMode.value = c.isAplMode
|
||||||
|
_isInsertMode.value = c.isInsertMode
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadSettings() {
|
fun reloadSettings() {
|
||||||
val app = getApplication<Application>()
|
val app = getApplication<Application>()
|
||||||
_maskHiddenInput.value = AppSettings.isMaskHiddenInputEnabled(app)
|
_maskHiddenInput.value = AppSettings.isMaskHiddenInputEnabled(app)
|
||||||
@@ -204,7 +388,21 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_isCrosshairRuler.value = AppSettings.isCrosshairRulerEnabled(app)
|
_isCrosshairRuler.value = AppSettings.isCrosshairRulerEnabled(app)
|
||||||
_cursorStyle.value = AppSettings.getCursorStyle(app)
|
_cursorStyle.value = AppSettings.getCursorStyle(app)
|
||||||
_blockSelectMode.value = AppSettings.isBlockSelectModeEnabled(app)
|
_blockSelectMode.value = AppSettings.isBlockSelectModeEnabled(app)
|
||||||
|
_blockPaste.value = AppSettings.isBlockPasteEnabled(app)
|
||||||
_uiTheme.value = AppSettings.getUiTheme(app)
|
_uiTheme.value = AppSettings.getUiTheme(app)
|
||||||
|
_fourColorOverride.value = AppSettings.getFourColorOverride(app)
|
||||||
|
_hotspotsEnabled.value = AppSettings.isHotspotsEnabled(app)
|
||||||
|
_hotspotsUrlEnabled.value = AppSettings.isHotspotsUrlEnabled(app)
|
||||||
|
_hotspotsPfEnabled.value = AppSettings.isHotspotsPfEnabled(app)
|
||||||
|
_hotspotsMenuChoicesEnabled.value = AppSettings.isHotspotsMenuChoicesEnabled(app)
|
||||||
|
hotspotEngine.urlEnabled = _hotspotsUrlEnabled.value
|
||||||
|
hotspotEngine.pfKeysEnabled = _hotspotsPfEnabled.value
|
||||||
|
hotspotEngine.menuChoicesEnabled = _hotspotsMenuChoicesEnabled.value
|
||||||
|
hotspotEngine.invalidateCache()
|
||||||
|
_trimRectSizingHandles.value = AppSettings.isTrimRectSizingHandlesEnabled(app)
|
||||||
|
_trimRectRemainAfterEdit.value = AppSettings.isTrimRectRemainAfterEditEnabled(app)
|
||||||
|
_pasteWordBreak.value = AppSettings.isPasteWordBreakEnabled(app)
|
||||||
|
applyModeSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pasteLineWrap(text: String, endCol: Int, wordWrap: Boolean) {
|
fun pasteLineWrap(text: String, endCol: Int, wordWrap: Boolean) {
|
||||||
@@ -245,11 +443,29 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private var client: Telnet3270Client? = null
|
private var client: Telnet3270Client? = null
|
||||||
var currentHost: String = ""
|
var currentHost: String = ""
|
||||||
private set
|
private set
|
||||||
|
var currentPort: Int = 23
|
||||||
|
private set
|
||||||
|
var currentProfileName: String? = null
|
||||||
|
private set
|
||||||
var activeHostType: String = "TSO"
|
var activeHostType: String = "TSO"
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
fun getConnectedTarget(): String {
|
||||||
|
val pName = currentProfileName
|
||||||
|
if (!pName.isNullOrBlank()) return pName
|
||||||
|
val h = currentHost
|
||||||
|
val p = currentPort
|
||||||
|
return if (h.isNotBlank()) "$h:$p" else "the host"
|
||||||
|
}
|
||||||
|
|
||||||
fun getClient(): Telnet3270Client? = client
|
fun getClient(): Telnet3270Client? = client
|
||||||
|
|
||||||
|
fun getScreenHistoryBuffer(): ScreenHistoryBuffer? = client?.screenBuffer?.historyBuffer
|
||||||
|
|
||||||
|
fun sendKeys(keys: String) {
|
||||||
|
inputChannel.trySend(TerminalInputAction.SendKeysAction(keys))
|
||||||
|
}
|
||||||
|
|
||||||
private var lastScreenContentHash: Int = 0
|
private var lastScreenContentHash: Int = 0
|
||||||
private var hasInitialScreenLoaded: Boolean = false
|
private var hasInitialScreenLoaded: Boolean = false
|
||||||
|
|
||||||
@@ -303,6 +519,31 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is TerminalInputAction.PasteText -> {
|
||||||
|
log.info("PasteText: len=${action.text.length}, curAddr=${buf.cursorAddress}, formatted=${buf.isFormatted}, isNvt=$isNvt")
|
||||||
|
ip.isKeyboardLocked = false
|
||||||
|
if (isNvt) {
|
||||||
|
for (ch in action.text) {
|
||||||
|
if (ch == '\n' || ch == '\r') {
|
||||||
|
c.sendNVTString("\r\n")
|
||||||
|
} else {
|
||||||
|
c.sendNVTChar(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val app = getApplication<Application>()
|
||||||
|
val blockPaste = AppSettings.isBlockPasteEnabled(app)
|
||||||
|
val pasteWordBreak = AppSettings.isPasteWordBreakEnabled(app)
|
||||||
|
ip.pasteText(action.text, true, false, blockPaste, pasteWordBreak)
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.SendKeysAction -> {
|
||||||
|
log.info("SendKeysAction: keys='${action.keys}', curAddr=${buf.cursorAddress}, formatted=${buf.isFormatted}, isNvt=$isNvt")
|
||||||
|
ip.isKeyboardLocked = false
|
||||||
|
c.ps?.SendKeys(action.keys)
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
is TerminalInputAction.SendAid -> {
|
is TerminalInputAction.SendAid -> {
|
||||||
if (isNvt) {
|
if (isNvt) {
|
||||||
if (action.aidCode == AID_ENTER) {
|
if (action.aidCode == AID_ENTER) {
|
||||||
@@ -472,8 +713,48 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
is TerminalInputAction.ToggleInsert -> {
|
is TerminalInputAction.ToggleInsert -> {
|
||||||
if (!isNvt) {
|
if (!isNvt) {
|
||||||
ip.processToggleInsert()
|
c.toggleInsert()
|
||||||
_isInsertMode.value = ip.isInsertMode
|
_isInsertMode.value = c.isInsertMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.ToggleDocMode -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.toggleDocMode()
|
||||||
|
AppSettings.setEntryAssistDocMode(getApplication(), c.isDocMode)
|
||||||
|
_isDocMode.value = c.isDocMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.ToggleWordWrap -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.toggleWordWrap()
|
||||||
|
AppSettings.setEntryAssistWordWrap(getApplication(), c.isWordWrap)
|
||||||
|
_isWordWrap.value = c.isWordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.ToggleApl -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.toggleAplMode()
|
||||||
|
_isAplMode.value = c.isAplMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.SetDocMode -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.isDocMode = action.enabled
|
||||||
|
AppSettings.setEntryAssistDocMode(getApplication(), action.enabled)
|
||||||
|
_isDocMode.value = action.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.SetWordWrap -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.isWordWrap = action.enabled
|
||||||
|
AppSettings.setEntryAssistWordWrap(getApplication(), action.enabled)
|
||||||
|
_isWordWrap.value = action.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.SetAplMode -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.isAplMode = action.enabled
|
||||||
|
_isAplMode.value = action.enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TerminalInputAction.SetCursor -> {
|
is TerminalInputAction.SetCursor -> {
|
||||||
@@ -623,9 +904,14 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
proxyHost: String = "",
|
proxyHost: String = "",
|
||||||
proxyPort: Int = 0,
|
proxyPort: Int = 0,
|
||||||
proxyUser: String = "",
|
proxyUser: String = "",
|
||||||
proxyPass: String = ""
|
proxyPass: String = "",
|
||||||
|
savedConnection: Boolean = false,
|
||||||
|
savedProfileId: String? = null,
|
||||||
|
savedProfileName: String? = null
|
||||||
) {
|
) {
|
||||||
currentHost = host
|
currentHost = host
|
||||||
|
currentPort = port
|
||||||
|
currentProfileName = savedProfileName
|
||||||
activeHostType = hostType
|
activeHostType = hostType
|
||||||
lastScreenContentHash = 0
|
lastScreenContentHash = 0
|
||||||
hasInitialScreenLoaded = false
|
hasInitialScreenLoaded = false
|
||||||
@@ -634,6 +920,18 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_codePage.value = codePageStr
|
_codePage.value = codePageStr
|
||||||
clearSearchHighlight()
|
clearSearchHighlight()
|
||||||
|
|
||||||
|
if (savedConnection) {
|
||||||
|
val lastState = haus.nightmare.a3270.ui.getLastTransferState()
|
||||||
|
if (lastState != null) {
|
||||||
|
lastState.hostType = when (hostType.uppercase(Locale.US)) {
|
||||||
|
"CMS", "VM/CMS", "VM" -> FTConfig.HostType.CMS
|
||||||
|
"CICS" -> FTConfig.HostType.CICS
|
||||||
|
else -> FTConfig.HostType.TSO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppSettings.setDefaultHostType(getApplication(), hostType)
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Always disconnect any existing client before establishing a new connection
|
// Always disconnect any existing client before establishing a new connection
|
||||||
@@ -661,21 +959,31 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
val globalVerify = AppSettings.isVerifyCertsEnabled(getApplication())
|
val globalVerify = AppSettings.isVerifyCertsEnabled(getApplication())
|
||||||
val effectiveVerify = tlsVerifyCert && globalVerify
|
val effectiveVerify = tlsVerifyCert && globalVerify
|
||||||
val gMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(graphicsModeStr)
|
val gMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(graphicsModeStr)
|
||||||
|
val effectiveLu = if (luName.isNotBlank()) luName.trim() else (parsedConfig.luName ?: "").trim()
|
||||||
|
|
||||||
_isTlsActive.value = effectiveTls
|
_isTlsActive.value = effectiveTls
|
||||||
_isTlsVerified.value = effectiveVerify
|
_isTlsVerified.value = effectiveVerify
|
||||||
_oiaText.value = "Connecting to $effectiveHost:$effectivePort" + (if (effectiveTls) " [TLS]" else "") + "..."
|
_oiaText.value = "Connecting to " +
|
||||||
|
(if (effectiveLu.isNotBlank()) "$effectiveLu@" else "") +
|
||||||
|
"$effectiveHost:$effectivePort" +
|
||||||
|
(if (effectiveTls) " [TLS]" else "") + "..."
|
||||||
|
|
||||||
val config = ConnectionConfig(effectiveHost, effectivePort, model).apply {
|
val config = ConnectionConfig(effectiveHost, effectivePort, model).apply {
|
||||||
|
this.hostType = hostType
|
||||||
|
this.isSavedConnection = savedConnection
|
||||||
|
if (savedProfileId != null) this.savedProfileId = savedProfileId
|
||||||
|
if (savedProfileName != null) this.savedProfileName = savedProfileName
|
||||||
|
|
||||||
if (model == TerminalModel.IBM_DYNAMIC) {
|
if (model == TerminalModel.IBM_DYNAMIC) {
|
||||||
setDynamicDimensions(dynamicRows, dynamicCols)
|
setDynamicDimensions(dynamicRows, dynamicCols)
|
||||||
}
|
}
|
||||||
if (luName.isNotBlank()) {
|
if (effectiveLu.isNotBlank()) {
|
||||||
setLuName(luName)
|
setLuName(effectiveLu)
|
||||||
}
|
}
|
||||||
isUseTls = effectiveTls
|
isUseTls = effectiveTls
|
||||||
isTlsVerifyCert = effectiveVerify
|
isTlsVerifyCert = effectiveVerify
|
||||||
isTn3270eEnabled = effectiveTn3270e
|
isTn3270eEnabled = effectiveTn3270e
|
||||||
|
isAutoSysUnlock = true
|
||||||
graphicsMode = gMode
|
graphicsMode = gMode
|
||||||
codePage = codePageStr
|
codePage = codePageStr
|
||||||
|
|
||||||
@@ -769,6 +1077,20 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_isKeyboardLocked.value = locked
|
_isKeyboardLocked.value = locked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newClient.inputProcessor.setBellListener {
|
||||||
|
playBellSound()
|
||||||
|
}
|
||||||
|
|
||||||
|
newClient.oia.registerOIAListener(object : haus.nightmare.lib3270j.ecl.ECLOIAListener {
|
||||||
|
override fun oiaChanged(event: haus.nightmare.lib3270j.ecl.ECLOIAEvent) {
|
||||||
|
_isInsertMode.value = newClient.isInsertMode
|
||||||
|
_isDocMode.value = newClient.isDocMode
|
||||||
|
_isWordWrap.value = newClient.isWordWrap
|
||||||
|
_isAplMode.value = newClient.isAplMode
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
newClient.addScreenUpdateListener(object : ScreenUpdateListener {
|
newClient.addScreenUpdateListener(object : ScreenUpdateListener {
|
||||||
override fun onScreenUpdated() {
|
override fun onScreenUpdated() {
|
||||||
val buf = newClient.screenBuffer
|
val buf = newClient.screenBuffer
|
||||||
@@ -779,6 +1101,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_cursorAddress.value = buf.cursorAddress
|
_cursorAddress.value = buf.cursorAddress
|
||||||
}
|
}
|
||||||
_isKeyboardLocked.value = newClient.inputProcessor.isKeyboardLocked
|
_isKeyboardLocked.value = newClient.inputProcessor.isKeyboardLocked
|
||||||
|
_isInsertMode.value = newClient.isInsertMode
|
||||||
|
_isDocMode.value = newClient.isDocMode
|
||||||
|
_isWordWrap.value = newClient.isWordWrap
|
||||||
|
_isAplMode.value = newClient.isAplMode
|
||||||
_screenVersion.value = System.currentTimeMillis()
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
|
||||||
// Drive CUT mode file transfers if active
|
// Drive CUT mode file transfers if active
|
||||||
@@ -804,6 +1130,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
override fun onSoundAlarm() {
|
override fun onSoundAlarm() {
|
||||||
log.fine("Sound Alarm")
|
log.fine("Sound Alarm")
|
||||||
|
playBellSound()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -811,6 +1138,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_cols.value = model.defaultCols
|
_cols.value = model.defaultCols
|
||||||
_screenBuffer.value = newClient.screenBuffer
|
_screenBuffer.value = newClient.screenBuffer
|
||||||
|
|
||||||
|
applyModeSettings()
|
||||||
|
|
||||||
newClient.connect()
|
newClient.connect()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
log.severe("Failed to connect: ${e.message}")
|
log.severe("Failed to connect: ${e.message}")
|
||||||
@@ -820,6 +1149,38 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun connect(config: ConnectionConfig) {
|
||||||
|
val modelNum = when (config.model) {
|
||||||
|
TerminalModel.IBM_DYNAMIC -> 0
|
||||||
|
TerminalModel.IBM_3279_3, TerminalModel.IBM_3278_3 -> 3
|
||||||
|
TerminalModel.IBM_3279_4, TerminalModel.IBM_3278_4 -> 4
|
||||||
|
TerminalModel.IBM_3279_5, TerminalModel.IBM_3278_5 -> 5
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
connect(
|
||||||
|
host = config.host,
|
||||||
|
port = config.port,
|
||||||
|
modelNum = modelNum,
|
||||||
|
dynamicRows = config.dynamicRows,
|
||||||
|
dynamicCols = config.dynamicCols,
|
||||||
|
luName = config.luName ?: "",
|
||||||
|
hostType = config.hostType ?: "TSO",
|
||||||
|
useTls = config.isUseTls,
|
||||||
|
tlsVerifyCert = config.isTlsVerifyCert,
|
||||||
|
tn3270e = config.isTn3270eEnabled,
|
||||||
|
graphicsModeStr = config.graphicsMode?.name ?: "BOTH",
|
||||||
|
codePageStr = config.codePage ?: "037",
|
||||||
|
proxyTypeStr = config.proxyType?.name ?: "NONE",
|
||||||
|
proxyHost = config.proxyHost ?: "",
|
||||||
|
proxyPort = config.proxyPort,
|
||||||
|
proxyUser = config.proxyUsername ?: "",
|
||||||
|
proxyPass = config.proxyPassword ?: "",
|
||||||
|
savedConnection = config.isSavedConnection,
|
||||||
|
savedProfileId = config.savedProfileId,
|
||||||
|
savedProfileName = config.savedProfileName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -949,7 +1310,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
fun pasteString(text: String) {
|
fun pasteString(text: String) {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
inputChannel.trySend(TerminalInputAction.TypeText(text))
|
inputChannel.trySend(TerminalInputAction.PasteText(text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package haus.nightmare.a3270.graphics
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import haus.nightmare.lib3270j.graphics.PixelBuffer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android bridge for translating lib3270j PixelBuffer into Android Bitmaps and rendering onto Canvas.
|
||||||
|
*/
|
||||||
|
object AndroidCanvasRenderer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a PixelBuffer into an Android Bitmap using ARGB_8888 configuration.
|
||||||
|
*/
|
||||||
|
fun toBitmap(buffer: PixelBuffer?): Bitmap? {
|
||||||
|
if (buffer == null) return null
|
||||||
|
val w = buffer.width
|
||||||
|
val h = buffer.height
|
||||||
|
if (w <= 0 || h <= 0) return null
|
||||||
|
val pixels = buffer.pixels ?: return null
|
||||||
|
return Bitmap.createBitmap(pixels, w, h, Bitmap.Config.ARGB_8888)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the given PixelBuffer onto an Android Canvas scaled to the destination rectangle.
|
||||||
|
*/
|
||||||
|
fun drawPixelBuffer(
|
||||||
|
canvas: Canvas,
|
||||||
|
buffer: PixelBuffer?,
|
||||||
|
dstRect: RectF,
|
||||||
|
paint: Paint? = null
|
||||||
|
) {
|
||||||
|
val bmp = toBitmap(buffer) ?: return
|
||||||
|
canvas.drawBitmap(bmp, null, dstRect, paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the PixelBuffer contents into an existing mutable Bitmap.
|
||||||
|
*/
|
||||||
|
fun copyIntoBitmap(buffer: PixelBuffer, target: Bitmap) {
|
||||||
|
val pixels = buffer.pixels ?: return
|
||||||
|
val w = minOf(buffer.width, target.width)
|
||||||
|
val h = minOf(buffer.height, target.height)
|
||||||
|
if (w <= 0 || h <= 0) return
|
||||||
|
target.setPixels(pixels, 0, buffer.width, 0, 0, w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package haus.nightmare.a3270.storage
|
package haus.nightmare.a3270.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.Reader
|
import java.io.Reader
|
||||||
import java.io.Writer
|
import java.io.Writer
|
||||||
@@ -19,16 +21,42 @@ object AppSettings {
|
|||||||
private const val KEY_VERIFY_CERTS = "verify_certs"
|
private const val KEY_VERIFY_CERTS = "verify_certs"
|
||||||
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
|
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
|
||||||
private const val KEY_DEFAULT_CODE_PAGE = "default_code_page"
|
private const val KEY_DEFAULT_CODE_PAGE = "default_code_page"
|
||||||
|
private const val KEY_DEFAULT_HOST_TYPE = "default_host_type"
|
||||||
|
|
||||||
// New settings aligned with j3270
|
// New settings aligned with j3270
|
||||||
private const val KEY_UI_THEME = "ui_theme"
|
private const val KEY_UI_THEME = "ui_theme"
|
||||||
private const val KEY_CROSSHAIR_RULER = "crosshair_ruler"
|
private const val KEY_CROSSHAIR_RULER = "crosshair_ruler"
|
||||||
private const val KEY_CURSOR_STYLE = "cursor_style"
|
private const val KEY_CURSOR_STYLE = "cursor_style"
|
||||||
private const val KEY_BLOCK_SELECT_MODE = "block_select_mode"
|
private const val KEY_BLOCK_SELECT_MODE = "block_select_mode"
|
||||||
|
private const val KEY_BLOCK_PASTE = "block_paste"
|
||||||
private const val KEY_STARTUP_BEHAVIOR = "startup_behavior"
|
private const val KEY_STARTUP_BEHAVIOR = "startup_behavior"
|
||||||
private const val KEY_DYNAMIC_ROWS = "dynamic_rows"
|
private const val KEY_DYNAMIC_ROWS = "dynamic_rows"
|
||||||
private const val KEY_DYNAMIC_COLS = "dynamic_cols"
|
private const val KEY_DYNAMIC_COLS = "dynamic_cols"
|
||||||
|
|
||||||
|
// Entry Assist & Modes aligned with j3270
|
||||||
|
private const val KEY_ENTRY_ASSIST_DOC_MODE = "entry_assist_doc_mode"
|
||||||
|
private const val KEY_ENTRY_ASSIST_WORD_WRAP = "entry_assist_word_wrap"
|
||||||
|
private const val KEY_ENTRY_ASSIST_START_COL = "entry_assist_start_col"
|
||||||
|
private const val KEY_ENTRY_ASSIST_END_COL = "entry_assist_end_col"
|
||||||
|
private const val KEY_ENTRY_ASSIST_BELL = "entry_assist_bell"
|
||||||
|
private const val KEY_ENTRY_ASSIST_BELL_COL = "entry_assist_bell_col"
|
||||||
|
private const val KEY_ENTRY_ASSIST_TAB_STOPS = "entry_assist_tab_stops"
|
||||||
|
private const val KEY_INSERT_OFF_ON_AID = "insert_off_on_aid"
|
||||||
|
private const val KEY_FOUR_COLOR_OVERRIDE = "four_color_override"
|
||||||
|
private const val KEY_NUMERIC_FIELD_LOCK = "numeric_field_lock"
|
||||||
|
private const val KEY_AUTO_SKIP_ENABLED = "auto_skip_enabled"
|
||||||
|
|
||||||
|
// Hotspots aligned with j3270 v1.2.2
|
||||||
|
private const val KEY_HOTSPOTS_ENABLED = "hotspots_enabled"
|
||||||
|
private const val KEY_HOTSPOTS_URL_ENABLED = "hotspots_url_enabled"
|
||||||
|
private const val KEY_HOTSPOTS_PF_ENABLED = "hotspots_pf_enabled"
|
||||||
|
private const val KEY_HOTSPOTS_MENU_CHOICES_ENABLED = "hotspots_menu_choices_enabled"
|
||||||
|
|
||||||
|
// Selection & Trim aligned with j3270 v1.2.2
|
||||||
|
private const val KEY_TRIM_RECT_SIZING_HANDLES = "trim_rect_sizing_handles"
|
||||||
|
private const val KEY_TRIM_RECT_REMAIN_AFTER_EDIT = "trim_rect_remain_after_edit"
|
||||||
|
private const val KEY_PASTE_WORD_BREAK = "paste_word_break"
|
||||||
|
|
||||||
enum class StartupBehavior {
|
enum class StartupBehavior {
|
||||||
SHOW_CONNECT,
|
SHOW_CONNECT,
|
||||||
DO_NOTHING,
|
DO_NOTHING,
|
||||||
@@ -87,6 +115,15 @@ object AppSettings {
|
|||||||
getPrefs(context).edit().putString(KEY_DEFAULT_CODE_PAGE, codePage).apply()
|
getPrefs(context).edit().putString(KEY_DEFAULT_CODE_PAGE, codePage).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDefaultHostType(context: Context): String {
|
||||||
|
return getPrefs(context).getString(KEY_DEFAULT_HOST_TYPE, "TSO") ?: "TSO"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultHostType(context: Context, hostType: String) {
|
||||||
|
val normalized = if (hostType.isNotBlank()) hostType.trim().uppercase(Locale.US) else "TSO"
|
||||||
|
getPrefs(context).edit().putString(KEY_DEFAULT_HOST_TYPE, normalized).apply()
|
||||||
|
}
|
||||||
|
|
||||||
// ========== UI Theme ==========
|
// ========== UI Theme ==========
|
||||||
fun getUiTheme(context: Context): String {
|
fun getUiTheme(context: Context): String {
|
||||||
return getPrefs(context).getString(KEY_UI_THEME, "DARK") ?: "DARK"
|
return getPrefs(context).getString(KEY_UI_THEME, "DARK") ?: "DARK"
|
||||||
@@ -124,6 +161,15 @@ object AppSettings {
|
|||||||
getPrefs(context).edit().putBoolean(KEY_BLOCK_SELECT_MODE, enabled).apply()
|
getPrefs(context).edit().putBoolean(KEY_BLOCK_SELECT_MODE, enabled).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Block Paste Mode ==========
|
||||||
|
fun isBlockPasteEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_BLOCK_PASTE, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBlockPasteEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_BLOCK_PASTE, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Startup Behavior ==========
|
// ========== Startup Behavior ==========
|
||||||
fun getStartupBehavior(context: Context): StartupBehavior {
|
fun getStartupBehavior(context: Context): StartupBehavior {
|
||||||
val s = getPrefs(context).getString(KEY_STARTUP_BEHAVIOR, StartupBehavior.SHOW_CONNECT.name)
|
val s = getPrefs(context).getString(KEY_STARTUP_BEHAVIOR, StartupBehavior.SHOW_CONNECT.name)
|
||||||
@@ -155,6 +201,169 @@ object AppSettings {
|
|||||||
getPrefs(context).edit().putInt(KEY_DYNAMIC_COLS, cols.coerceIn(80, 255)).apply()
|
getPrefs(context).edit().putInt(KEY_DYNAMIC_COLS, cols.coerceIn(80, 255)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Entry Assist (Document Mode) ==========
|
||||||
|
fun getEntryAssistDocMode(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_ENTRY_ASSIST_DOC_MODE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistDocMode(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_ENTRY_ASSIST_DOC_MODE, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistWordWrap(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_ENTRY_ASSIST_WORD_WRAP, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistWordWrap(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_ENTRY_ASSIST_WORD_WRAP, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistStartCol(context: Context): Int {
|
||||||
|
return getPrefs(context).getInt(KEY_ENTRY_ASSIST_START_COL, 1).coerceIn(1, 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistStartCol(context: Context, col: Int) {
|
||||||
|
getPrefs(context).edit().putInt(KEY_ENTRY_ASSIST_START_COL, col.coerceIn(1, 80)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistEndCol(context: Context): Int {
|
||||||
|
return getPrefs(context).getInt(KEY_ENTRY_ASSIST_END_COL, 80).coerceIn(1, 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistEndCol(context: Context, col: Int) {
|
||||||
|
getPrefs(context).edit().putInt(KEY_ENTRY_ASSIST_END_COL, col.coerceIn(1, 80)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistBell(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_ENTRY_ASSIST_BELL, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistBell(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_ENTRY_ASSIST_BELL, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistBellCol(context: Context): Int {
|
||||||
|
return getPrefs(context).getInt(KEY_ENTRY_ASSIST_BELL_COL, 75).coerceIn(1, 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistBellCol(context: Context, col: Int) {
|
||||||
|
getPrefs(context).edit().putInt(KEY_ENTRY_ASSIST_BELL_COL, col.coerceIn(1, 80)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistTabStops(context: Context): String {
|
||||||
|
return getPrefs(context).getString(KEY_ENTRY_ASSIST_TAB_STOPS, "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75")
|
||||||
|
?: "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistTabStops(context: Context, stops: String) {
|
||||||
|
getPrefs(context).edit().putString(KEY_ENTRY_ASSIST_TAB_STOPS, stops).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistTabStopsArray(context: Context): IntArray {
|
||||||
|
val s = getEntryAssistTabStops(context)
|
||||||
|
if (s.isBlank()) return intArrayOf()
|
||||||
|
val parts = s.split(",")
|
||||||
|
val list = mutableListOf<Int>()
|
||||||
|
for (p in parts) {
|
||||||
|
val v = p.trim().toIntOrNull()
|
||||||
|
if (v != null && v > 0) {
|
||||||
|
list.add(v - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Terminal Operational Modes ==========
|
||||||
|
fun getInsertOffOnAid(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_INSERT_OFF_ON_AID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInsertOffOnAid(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_INSERT_OFF_ON_AID, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFourColorOverride(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_FOUR_COLOR_OVERRIDE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFourColorOverride(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_FOUR_COLOR_OVERRIDE, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNumericFieldLock(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_NUMERIC_FIELD_LOCK, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNumericFieldLock(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_NUMERIC_FIELD_LOCK, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAutoSkipEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_AUTO_SKIP_ENABLED, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutoSkipEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_AUTO_SKIP_ENABLED, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Hotspots (Default DISABLED for touch screens) ==========
|
||||||
|
fun isHotspotsEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_HOTSPOTS_ENABLED, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHotspotsEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_HOTSPOTS_ENABLED, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isHotspotsUrlEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_HOTSPOTS_URL_ENABLED, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHotspotsUrlEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_HOTSPOTS_URL_ENABLED, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isHotspotsPfEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_HOTSPOTS_PF_ENABLED, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHotspotsPfEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_HOTSPOTS_PF_ENABLED, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isHotspotsMenuChoicesEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_HOTSPOTS_MENU_CHOICES_ENABLED, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHotspotsMenuChoicesEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_HOTSPOTS_MENU_CHOICES_ENABLED, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Selection Handles & Trim ==========
|
||||||
|
fun isTrimRectSizingHandlesEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_TRIM_RECT_SIZING_HANDLES, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTrimRectSizingHandlesEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_TRIM_RECT_SIZING_HANDLES, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTrimRectRemainAfterEditEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_TRIM_RECT_REMAIN_AFTER_EDIT, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTrimRectRemainAfterEditEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_TRIM_RECT_REMAIN_AFTER_EDIT, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPasteWordBreakEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_PASTE_WORD_BREAK, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPasteWordBreakEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_PASTE_WORD_BREAK, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
// ========== INI Config Export / Import ==========
|
// ========== INI Config Export / Import ==========
|
||||||
|
|
||||||
fun exportToIni(context: Context, writer: Writer) {
|
fun exportToIni(context: Context, writer: Writer) {
|
||||||
@@ -181,9 +390,38 @@ object AppSettings {
|
|||||||
p.println("autoConnectTn3270e = ${autoHost.tn3270e}")
|
p.println("autoConnectTn3270e = ${autoHost.tn3270e}")
|
||||||
}
|
}
|
||||||
p.println("codePage = ${getDefaultCodePage(context)}")
|
p.println("codePage = ${getDefaultCodePage(context)}")
|
||||||
|
p.println("hostType = ${getDefaultHostType(context)}")
|
||||||
p.println("dynamicRows = ${getDynamicRows(context)}")
|
p.println("dynamicRows = ${getDynamicRows(context)}")
|
||||||
p.println("dynamicCols = ${getDynamicCols(context)}")
|
p.println("dynamicCols = ${getDynamicCols(context)}")
|
||||||
p.println("blockSelectMode = ${isBlockSelectModeEnabled(context)}")
|
p.println("blockSelectMode = ${isBlockSelectModeEnabled(context)}")
|
||||||
|
p.println("blockPaste = ${isBlockPasteEnabled(context)}")
|
||||||
|
p.println("trimRectSizingHandles = ${isTrimRectSizingHandlesEnabled(context)}")
|
||||||
|
p.println("trimRectRemainAfterEdit = ${isTrimRectRemainAfterEditEnabled(context)}")
|
||||||
|
p.println("pasteWordBreak = ${isPasteWordBreakEnabled(context)}")
|
||||||
|
p.println()
|
||||||
|
|
||||||
|
p.println("[hotspots]")
|
||||||
|
p.println("hotspotsEnabled = ${isHotspotsEnabled(context)}")
|
||||||
|
p.println("hotspotsUrlEnabled = ${isHotspotsUrlEnabled(context)}")
|
||||||
|
p.println("hotspotsPfEnabled = ${isHotspotsPfEnabled(context)}")
|
||||||
|
p.println("hotspotsMenuChoicesEnabled = ${isHotspotsMenuChoicesEnabled(context)}")
|
||||||
|
p.println()
|
||||||
|
|
||||||
|
p.println("[entryassist]")
|
||||||
|
p.println("docMode = ${getEntryAssistDocMode(context)}")
|
||||||
|
p.println("wordWrap = ${getEntryAssistWordWrap(context)}")
|
||||||
|
p.println("startCol = ${getEntryAssistStartCol(context)}")
|
||||||
|
p.println("endCol = ${getEntryAssistEndCol(context)}")
|
||||||
|
p.println("bell = ${getEntryAssistBell(context)}")
|
||||||
|
p.println("bellCol = ${getEntryAssistBellCol(context)}")
|
||||||
|
p.println("tabStops = ${getEntryAssistTabStops(context)}")
|
||||||
|
p.println()
|
||||||
|
|
||||||
|
p.println("[modes]")
|
||||||
|
p.println("insertOffOnAid = ${getInsertOffOnAid(context)}")
|
||||||
|
p.println("fourColorOverride = ${getFourColorOverride(context)}")
|
||||||
|
p.println("numericFieldLock = ${getNumericFieldLock(context)}")
|
||||||
|
p.println("autoSkip = ${getAutoSkipEnabled(context)}")
|
||||||
p.println()
|
p.println()
|
||||||
|
|
||||||
p.println("[graphics]")
|
p.println("[graphics]")
|
||||||
@@ -193,7 +431,31 @@ object AppSettings {
|
|||||||
p.flush()
|
p.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFromIni(context: Context, reader: Reader) {
|
fun shareSettingsIni(context: Context) {
|
||||||
|
try {
|
||||||
|
val cacheDir = File(context.cacheDir, "exports").apply { mkdirs() }
|
||||||
|
val file = File(cacheDir, "a3270_settings.ini")
|
||||||
|
file.bufferedWriter().use { writer ->
|
||||||
|
exportToIni(context, writer)
|
||||||
|
}
|
||||||
|
val uri = androidx.core.content.FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "a3270 Terminal Settings")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(intent, "Share Terminal Settings"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.widget.Toast.makeText(context, "Export failed: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseIni(reader: Reader, callback: (section: String, key: String, value: String) -> Unit) {
|
||||||
val bufferedReader = BufferedReader(reader)
|
val bufferedReader = BufferedReader(reader)
|
||||||
var currentSection = ""
|
var currentSection = ""
|
||||||
|
|
||||||
@@ -207,13 +469,19 @@ object AppSettings {
|
|||||||
if (eq >= 0) {
|
if (eq >= 0) {
|
||||||
val key = line.substring(0, eq).trim()
|
val key = line.substring(0, eq).trim()
|
||||||
val value = line.substring(eq + 1).trim()
|
val value = line.substring(eq + 1).trim()
|
||||||
applyConfigEntry(context, currentSection, key, value)
|
callback(currentSection, key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadFromIni(context: Context, reader: Reader) {
|
||||||
|
parseIni(reader) { section, key, value ->
|
||||||
|
applyConfigEntry(context, section, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun applyConfigEntry(context: Context, section: String, key: String, value: String) {
|
private fun applyConfigEntry(context: Context, section: String, key: String, value: String) {
|
||||||
when (section) {
|
when (section) {
|
||||||
"appearance" -> {
|
"appearance" -> {
|
||||||
@@ -231,9 +499,50 @@ object AppSettings {
|
|||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
"codepage", "charset" -> setDefaultCodePage(context, value)
|
"codepage", "charset" -> setDefaultCodePage(context, value)
|
||||||
|
"hosttype", "host_type" -> setDefaultHostType(context, value)
|
||||||
"dynamicrows", "dynamic_rows" -> value.toIntOrNull()?.let { setDynamicRows(context, it) }
|
"dynamicrows", "dynamic_rows" -> value.toIntOrNull()?.let { setDynamicRows(context, it) }
|
||||||
"dynamiccols", "dynamic_cols" -> value.toIntOrNull()?.let { setDynamicCols(context, it) }
|
"dynamiccols", "dynamic_cols" -> value.toIntOrNull()?.let { setDynamicCols(context, it) }
|
||||||
"blockselectmode" -> setBlockSelectModeEnabled(context, value.toBoolean())
|
"blockselectmode" -> setBlockSelectModeEnabled(context, value.toBoolean())
|
||||||
|
"blockpaste", "blockpastemode" -> setBlockPasteEnabled(context, value.toBoolean())
|
||||||
|
"trimrectsizinghandles", "sizinghandles" -> setTrimRectSizingHandlesEnabled(context, value.toBoolean())
|
||||||
|
"trimrectremainafteredit", "remainafteredit" -> setTrimRectRemainAfterEditEnabled(context, value.toBoolean())
|
||||||
|
"pastewordbreak", "wordbreak" -> setPasteWordBreakEnabled(context, value.toBoolean())
|
||||||
|
"hotspotsenabled" -> setHotspotsEnabled(context, value.toBoolean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"clipboard" -> {
|
||||||
|
when (key.lowercase(Locale.US)) {
|
||||||
|
"blockpaste", "blockpastemode" -> setBlockPasteEnabled(context, value.toBoolean())
|
||||||
|
"trimrectsizinghandles", "sizinghandles" -> setTrimRectSizingHandlesEnabled(context, value.toBoolean())
|
||||||
|
"trimrectremainafteredit", "remainafteredit" -> setTrimRectRemainAfterEditEnabled(context, value.toBoolean())
|
||||||
|
"pastewordbreak", "wordbreak" -> setPasteWordBreakEnabled(context, value.toBoolean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"hotspots" -> {
|
||||||
|
when (key.lowercase(Locale.US)) {
|
||||||
|
"hotspotsenabled", "enabled" -> setHotspotsEnabled(context, value.toBoolean())
|
||||||
|
"hotspotsurlenabled", "urlenabled", "url" -> setHotspotsUrlEnabled(context, value.toBoolean())
|
||||||
|
"hotspotspfenabled", "pfenabled", "pf" -> setHotspotsPfEnabled(context, value.toBoolean())
|
||||||
|
"hotspotsmenuchoicesenabled", "menuchoicesenabled", "menuchoices", "menu" -> setHotspotsMenuChoicesEnabled(context, value.toBoolean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"entryassist" -> {
|
||||||
|
when (key.lowercase(Locale.US)) {
|
||||||
|
"docmode", "entryassistdocmode" -> setEntryAssistDocMode(context, value.toBoolean())
|
||||||
|
"wordwrap", "entryassistwordwrap" -> setEntryAssistWordWrap(context, value.toBoolean())
|
||||||
|
"startcol", "entryassiststartcol" -> value.toIntOrNull()?.let { setEntryAssistStartCol(context, it) }
|
||||||
|
"endcol", "entryassistendcol" -> value.toIntOrNull()?.let { setEntryAssistEndCol(context, it) }
|
||||||
|
"bell", "entryassistbell" -> setEntryAssistBell(context, value.toBoolean())
|
||||||
|
"bellcol", "entryassistbellcol" -> value.toIntOrNull()?.let { setEntryAssistBellCol(context, it) }
|
||||||
|
"tabstops", "entryassisttabstops" -> setEntryAssistTabStops(context, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"modes" -> {
|
||||||
|
when (key.lowercase(Locale.US)) {
|
||||||
|
"insertoffonaid" -> setInsertOffOnAid(context, value.toBoolean())
|
||||||
|
"fourcoloroverride" -> setFourColorOverride(context, value.toBoolean())
|
||||||
|
"numericfieldlock" -> setNumericFieldLock(context, value.toBoolean())
|
||||||
|
"autoskip", "autoskipenabled" -> setAutoSkipEnabled(context, value.toBoolean())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"graphics" -> {
|
"graphics" -> {
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
package haus.nightmare.a3270.storage
|
package haus.nightmare.a3270.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.graphics.GraphicsMode
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.io.Reader
|
||||||
|
import java.io.Writer
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class SavedHost(
|
data class SavedHost(
|
||||||
@@ -28,6 +40,47 @@ data class SavedHost(
|
|||||||
val proxyUsername: String = "",
|
val proxyUsername: String = "",
|
||||||
val proxyPassword: String = ""
|
val proxyPassword: String = ""
|
||||||
) {
|
) {
|
||||||
|
fun toConnectionConfig(): ConnectionConfig {
|
||||||
|
val tm = when (model) {
|
||||||
|
3 -> TerminalModel.IBM_3279_3
|
||||||
|
4 -> TerminalModel.IBM_3279_4
|
||||||
|
5 -> TerminalModel.IBM_3279_5
|
||||||
|
0 -> TerminalModel.IBM_DYNAMIC
|
||||||
|
else -> TerminalModel.IBM_3279_2
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = ConnectionConfig(host, port, tm, useTls)
|
||||||
|
config.isSavedConnection = true
|
||||||
|
config.savedProfileId = this.id
|
||||||
|
config.savedProfileName = this.name
|
||||||
|
config.hostType = if (this.hostType.isNotBlank()) this.hostType else "TSO"
|
||||||
|
if (model == 0 || tm.isDynamic) {
|
||||||
|
config.setDynamicDimensions(dynamicRows, dynamicCols)
|
||||||
|
}
|
||||||
|
if (luName.isNotBlank()) {
|
||||||
|
config.luName = luName.trim()
|
||||||
|
}
|
||||||
|
config.isTlsVerifyCert = tlsVerifyCert
|
||||||
|
config.isTn3270eEnabled = tn3270e
|
||||||
|
config.graphicsMode = GraphicsMode.fromString(graphicsMode)
|
||||||
|
if (codePage.isNotBlank()) {
|
||||||
|
config.codePage = codePage
|
||||||
|
}
|
||||||
|
if (!proxyType.equals("NONE", ignoreCase = true) && proxyHost.isNotBlank()) {
|
||||||
|
try {
|
||||||
|
val pt = ConnectionConfig.ProxyType.valueOf(proxyType.uppercase())
|
||||||
|
config.setProxy(
|
||||||
|
pt,
|
||||||
|
proxyHost,
|
||||||
|
if (proxyPort > 0) proxyPort else (if (pt == ConnectionConfig.ProxyType.HTTP) 8080 else 1080),
|
||||||
|
proxyUsername.ifBlank { null },
|
||||||
|
proxyPassword.ifBlank { null }
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
fun toJson(): JSONObject {
|
fun toJson(): JSONObject {
|
||||||
return JSONObject().apply {
|
return JSONObject().apply {
|
||||||
put("id", id)
|
put("id", id)
|
||||||
@@ -176,6 +229,10 @@ object HostStorage {
|
|||||||
"1149" to "1149 - Iceland (Euro €)",
|
"1149" to "1149 - Iceland (Euro €)",
|
||||||
"930" to "930 - Japanese Katakana Mixed DBCS",
|
"930" to "930 - Japanese Katakana Mixed DBCS",
|
||||||
"939" to "939 - Japanese Latin Mixed DBCS",
|
"939" to "939 - Japanese Latin Mixed DBCS",
|
||||||
|
"290" to "290 - Japanese Katakana SBCS",
|
||||||
|
"1027" to "1027 - Japanese Latin Extended SBCS",
|
||||||
|
"1390" to "1390 - Japanese Katakana DBCS Euro",
|
||||||
|
"1399" to "1399 - Japanese Latin DBCS Euro",
|
||||||
"935" to "935 - Simplified Chinese Mixed DBCS",
|
"935" to "935 - Simplified Chinese Mixed DBCS",
|
||||||
"937" to "937 - Traditional Chinese Mixed DBCS",
|
"937" to "937 - Traditional Chinese Mixed DBCS",
|
||||||
"1388" to "1388 - Simplified Chinese Extended DBCS",
|
"1388" to "1388 - Simplified Chinese Extended DBCS",
|
||||||
@@ -192,4 +249,156 @@ object HostStorage {
|
|||||||
hosts.forEach { array.put(it.toJson()) }
|
hosts.forEach { array.put(it.toJson()) }
|
||||||
getPrefs(context).edit().putString(KEY_SAVED_HOSTS, array.toString()).apply()
|
getPrefs(context).edit().putString(KEY_SAVED_HOSTS, array.toString()).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= INI Export / Import =================
|
||||||
|
|
||||||
|
fun exportToIni(hosts: List<SavedHost>, writer: Writer) {
|
||||||
|
val p = PrintWriter(writer)
|
||||||
|
val timeStamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
||||||
|
p.println("; a3270 Saved Connections Configuration File")
|
||||||
|
p.println("; Exported on $timeStamp")
|
||||||
|
p.println("; Connections count: ${hosts.size}")
|
||||||
|
p.println()
|
||||||
|
|
||||||
|
for (h in hosts) {
|
||||||
|
val section = h.name.trim().ifBlank { h.host.trim().ifBlank { "Connection" } }
|
||||||
|
p.println("[$section]")
|
||||||
|
if (h.id.isNotBlank()) {
|
||||||
|
p.println("id = ${h.id}")
|
||||||
|
}
|
||||||
|
p.println("name = ${h.name}")
|
||||||
|
p.println("host = ${h.host}")
|
||||||
|
p.println("port = ${h.port}")
|
||||||
|
p.println("model = ${h.model}")
|
||||||
|
p.println("dynamicRows = ${h.dynamicRows}")
|
||||||
|
p.println("dynamicCols = ${h.dynamicCols}")
|
||||||
|
p.println("luName = ${h.luName}")
|
||||||
|
p.println("autoConnect = ${h.autoConnect}")
|
||||||
|
p.println("hostType = ${h.hostType.ifBlank { "TSO" }}")
|
||||||
|
p.println("useTls = ${h.useTls}")
|
||||||
|
p.println("tlsVerifyCert = ${h.tlsVerifyCert}")
|
||||||
|
p.println("tn3270e = ${h.tn3270e}")
|
||||||
|
p.println("graphicsMode = ${h.graphicsMode.ifBlank { "BOTH" }}")
|
||||||
|
p.println("codePage = ${h.codePage.ifBlank { "037" }}")
|
||||||
|
p.println("proxyType = ${h.proxyType.ifBlank { "NONE" }}")
|
||||||
|
p.println("proxyHost = ${h.proxyHost}")
|
||||||
|
p.println("proxyPort = ${h.proxyPort}")
|
||||||
|
p.println("proxyUsername = ${h.proxyUsername}")
|
||||||
|
p.println("proxyPassword = ${h.proxyPassword}")
|
||||||
|
p.println()
|
||||||
|
}
|
||||||
|
p.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFromIni(reader: Reader): List<SavedHost> {
|
||||||
|
val bufferedReader = BufferedReader(reader)
|
||||||
|
val hosts = mutableListOf<SavedHost>()
|
||||||
|
var current: SavedHost? = null
|
||||||
|
|
||||||
|
bufferedReader.forEachLine { rawLine ->
|
||||||
|
val line = rawLine.trim()
|
||||||
|
if (line.isNotEmpty() && !line.startsWith(";") && !line.startsWith("#")) {
|
||||||
|
if (line.startsWith("[") && line.endsWith("]")) {
|
||||||
|
current?.let { hosts.add(it) }
|
||||||
|
var section = line.substring(1, line.length - 1).trim()
|
||||||
|
if (section.startsWith("connection:", ignoreCase = true)) {
|
||||||
|
section = section.substring("connection:".length).trim()
|
||||||
|
} else if (section.startsWith("host:", ignoreCase = true)) {
|
||||||
|
section = section.substring("host:".length).trim()
|
||||||
|
}
|
||||||
|
current = SavedHost(id = UUID.randomUUID().toString(), name = section, host = "")
|
||||||
|
} else {
|
||||||
|
val eq = line.indexOf('=')
|
||||||
|
if (eq >= 0) {
|
||||||
|
val key = line.substring(0, eq).trim().lowercase(Locale.US)
|
||||||
|
val value = line.substring(eq + 1).trim()
|
||||||
|
if (current == null) {
|
||||||
|
current = SavedHost(id = UUID.randomUUID().toString(), name = "Default", host = "")
|
||||||
|
}
|
||||||
|
current = applyIniProperty(current!!, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current?.let { hosts.add(it) }
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyIniProperty(h: SavedHost, key: String, value: String): SavedHost {
|
||||||
|
return when (key) {
|
||||||
|
"id" -> if (value.isNotBlank()) h.copy(id = value) else h
|
||||||
|
"name", "profilename", "profile" -> if (value.isNotBlank()) h.copy(name = value) else h
|
||||||
|
"host", "hostname", "address" -> h.copy(host = value)
|
||||||
|
"port" -> h.copy(port = value.toIntOrNull() ?: 23)
|
||||||
|
"model", "terminalmodel" -> h.copy(model = value.toIntOrNull() ?: 2)
|
||||||
|
"dynamicrows", "dynamic_rows", "rows" -> h.copy(dynamicRows = value.toIntOrNull() ?: 62)
|
||||||
|
"dynamiccols", "dynamic_cols", "cols" -> h.copy(dynamicCols = value.toIntOrNull() ?: 160)
|
||||||
|
"luname", "lu_name", "lu" -> h.copy(luName = value)
|
||||||
|
"autoconnect", "auto_connect" -> h.copy(autoConnect = value.toBoolean())
|
||||||
|
"hosttype", "host_type", "type" -> h.copy(hostType = value.uppercase(Locale.US))
|
||||||
|
"usetls", "use_tls", "tls", "ssl" -> h.copy(useTls = value.toBoolean())
|
||||||
|
"tlsverifycert", "verifycert", "tls_verify_cert" -> h.copy(tlsVerifyCert = value.toBoolean())
|
||||||
|
"tn3270e", "enabletn3270e" -> h.copy(tn3270e = value.toBoolean())
|
||||||
|
"graphicsmode", "graphics_mode", "graphics" -> h.copy(graphicsMode = value.uppercase(Locale.US))
|
||||||
|
"codepage", "code_page", "charset" -> h.copy(codePage = value)
|
||||||
|
"proxytype", "proxy_type" -> h.copy(proxyType = value.uppercase(Locale.US))
|
||||||
|
"proxyhost", "proxy_host" -> h.copy(proxyHost = value)
|
||||||
|
"proxyport", "proxy_port" -> h.copy(proxyPort = value.toIntOrNull() ?: 0)
|
||||||
|
"proxyusername", "proxy_username", "proxyuser" -> h.copy(proxyUsername = value)
|
||||||
|
"proxypassword", "proxy_password", "proxypass" -> h.copy(proxyPassword = value)
|
||||||
|
else -> h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importFromIni(context: Context, reader: Reader): List<SavedHost> {
|
||||||
|
val imported = loadFromIni(reader)
|
||||||
|
if (imported.isEmpty()) return getSavedHosts(context)
|
||||||
|
|
||||||
|
val current = getSavedHosts(context).toMutableList()
|
||||||
|
for (host in imported) {
|
||||||
|
val index = current.indexOfFirst {
|
||||||
|
(it.id.isNotBlank() && it.id == host.id) ||
|
||||||
|
(it.name.isNotBlank() && it.name.equals(host.name, ignoreCase = true))
|
||||||
|
}
|
||||||
|
if (host.autoConnect) {
|
||||||
|
current.indices.forEach { i ->
|
||||||
|
current[i] = current[i].copy(autoConnect = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= 0) {
|
||||||
|
val existingId = current[index].id
|
||||||
|
val toSet = if (host.id.isBlank()) host.copy(id = existingId) else host
|
||||||
|
current[index] = toSet
|
||||||
|
} else {
|
||||||
|
current.add(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveAll(context, current)
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareConnectionsIni(context: Context) {
|
||||||
|
try {
|
||||||
|
val hosts = getSavedHosts(context)
|
||||||
|
val cacheDir = File(context.cacheDir, "exports").apply { mkdirs() }
|
||||||
|
val file = File(cacheDir, "a3270_connections.ini")
|
||||||
|
file.bufferedWriter().use { writer ->
|
||||||
|
exportToIni(hosts, writer)
|
||||||
|
}
|
||||||
|
val uri = androidx.core.content.FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "a3270 Saved Connections")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(intent, "Share Saved Connections"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.widget.Toast.makeText(context, "Export failed: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
|
enum class ConnectChoice {
|
||||||
|
NEW_WINDOW,
|
||||||
|
ABORT_CURRENT,
|
||||||
|
CANCEL
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConnectChoiceDialog(
|
||||||
|
currentTarget: String,
|
||||||
|
onChoice: (ConnectChoice) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val targetText = currentTarget.ifBlank { "the host" }
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
onChoice(ConnectChoice.CANCEL)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = Color(0xFF1E1E1E),
|
||||||
|
tonalElevation = 6.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Connection Already Active",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "a3270 is currently connected to $targetText.",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Would you like to open the connection in a new window or abort the current connection?",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.End),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onChoice(ConnectChoice.CANCEL)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Cancel", color = Color.Gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onChoice(ConnectChoice.ABORT_CURRENT)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFC92A2A))
|
||||||
|
) {
|
||||||
|
Text("Abort Current Connection", fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onChoice(ConnectChoice.NEW_WINDOW)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
||||||
|
) {
|
||||||
|
Text("New Window", fontSize = 11.sp, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,12 +55,16 @@ fun ConnectDialog(
|
|||||||
proxyHost: String,
|
proxyHost: String,
|
||||||
proxyPort: Int,
|
proxyPort: Int,
|
||||||
proxyUser: String,
|
proxyUser: String,
|
||||||
proxyPass: String
|
proxyPass: String,
|
||||||
|
savedConnection: Boolean,
|
||||||
|
savedProfileId: String?,
|
||||||
|
savedProfileName: String?
|
||||||
) -> Unit
|
) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var savedHosts by remember { mutableStateOf(HostStorage.getSavedHosts(context)) }
|
var savedHosts by remember { mutableStateOf(HostStorage.getSavedHosts(context)) }
|
||||||
var selectedHostId by remember { mutableStateOf<String?>(null) }
|
var selectedHostId by remember { mutableStateOf<String?>(null) }
|
||||||
|
var showImportConnectionsDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var profileName by remember { mutableStateOf("") }
|
var profileName by remember { mutableStateOf("") }
|
||||||
var host by remember { mutableStateOf("") }
|
var host by remember { mutableStateOf("") }
|
||||||
@@ -70,7 +74,7 @@ fun ConnectDialog(
|
|||||||
var dynamicColsStr by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()) }
|
var dynamicColsStr by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()) }
|
||||||
var luName by remember { mutableStateOf("") }
|
var luName by remember { mutableStateOf("") }
|
||||||
var autoConnect by remember { mutableStateOf(false) }
|
var autoConnect by remember { mutableStateOf(false) }
|
||||||
var hostType by remember { mutableStateOf("TSO") }
|
var hostType by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDefaultHostType(context)) }
|
||||||
var useTls by remember { mutableStateOf(false) }
|
var useTls by remember { mutableStateOf(false) }
|
||||||
var tlsVerifyCert by remember { mutableStateOf(true) }
|
var tlsVerifyCert by remember { mutableStateOf(true) }
|
||||||
var tn3270e by remember { mutableStateOf(true) }
|
var tn3270e by remember { mutableStateOf(true) }
|
||||||
@@ -117,7 +121,7 @@ fun ConnectDialog(
|
|||||||
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
||||||
luName = ""
|
luName = ""
|
||||||
autoConnect = false
|
autoConnect = false
|
||||||
hostType = "TSO"
|
hostType = haus.nightmare.a3270.storage.AppSettings.getDefaultHostType(context)
|
||||||
useTls = false
|
useTls = false
|
||||||
tlsVerifyCert = true
|
tlsVerifyCert = true
|
||||||
tn3270e = true
|
tn3270e = true
|
||||||
@@ -231,13 +235,36 @@ fun ConnectDialog(
|
|||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
// Saved Profiles List Section
|
// Saved Profiles List Section
|
||||||
if (savedHosts.isNotEmpty()) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Saved Host Profiles:",
|
if (savedHosts.isNotEmpty()) "Saved Host Profiles (${savedHosts.size}):" else "Saved Host Profiles:",
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.Gray
|
color = Color.Gray
|
||||||
)
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
if (savedHosts.isNotEmpty()) {
|
||||||
|
TextButton(
|
||||||
|
onClick = { HostStorage.shareConnectionsIni(context) },
|
||||||
|
contentPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
) {
|
||||||
|
Text("Export (Share)", fontSize = 11.sp, color = Color(0xFF339AF0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = { showImportConnectionsDialog = true },
|
||||||
|
contentPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
) {
|
||||||
|
Text("Import", fontSize = 11.sp, color = Color(0xFF339AF0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedHosts.isNotEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
@@ -301,7 +328,10 @@ fun ConnectDialog(
|
|||||||
hostToConn.proxyHost,
|
hostToConn.proxyHost,
|
||||||
hostToConn.proxyPort,
|
hostToConn.proxyPort,
|
||||||
hostToConn.proxyUsername,
|
hostToConn.proxyUsername,
|
||||||
hostToConn.proxyPassword
|
hostToConn.proxyPassword,
|
||||||
|
true,
|
||||||
|
hostToConn.id,
|
||||||
|
hostToConn.name
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
@@ -804,7 +834,10 @@ fun ConnectDialog(
|
|||||||
saved.proxyHost,
|
saved.proxyHost,
|
||||||
saved.proxyPort,
|
saved.proxyPort,
|
||||||
saved.proxyUsername,
|
saved.proxyUsername,
|
||||||
saved.proxyPassword
|
saved.proxyPassword,
|
||||||
|
true,
|
||||||
|
saved.id,
|
||||||
|
saved.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -816,4 +849,64 @@ fun ConnectDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showImportConnectionsDialog) {
|
||||||
|
var importText by remember { mutableStateOf("") }
|
||||||
|
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showImportConnectionsDialog = false },
|
||||||
|
title = { Text("Import Host Connections") },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text("Paste j3270 / a3270 INI connections below:", fontSize = 12.sp)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = importText,
|
||||||
|
onValueChange = { importText = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(180.dp),
|
||||||
|
maxLines = 10
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val clip = clipboardManager.getText()?.text ?: ""
|
||||||
|
if (clip.isNotBlank()) {
|
||||||
|
importText = clip
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Paste Clipboard", fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showImportConnectionsDialog = false
|
||||||
|
if (importText.isNotBlank()) {
|
||||||
|
try {
|
||||||
|
val countBefore = savedHosts.size
|
||||||
|
val merged = HostStorage.importFromIni(context, java.io.StringReader(importText))
|
||||||
|
savedHosts = merged
|
||||||
|
val countAfter = merged.size
|
||||||
|
val diff = countAfter - countBefore
|
||||||
|
val msg = if (diff > 0) "Imported $diff new connection(s)" else "Updated ${merged.size} connection(s)"
|
||||||
|
android.widget.Toast.makeText(context, msg, android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.widget.Toast.makeText(context, "Error importing: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Import")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showImportConnectionsDialog = false }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,61 @@ private val TerminalKeyboardOptions = KeyboardOptions(
|
|||||||
keyboardType = KeyboardType.Ascii
|
keyboardType = KeyboardType.Ascii
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class LastTransferState(
|
||||||
|
var hostFile: String = "",
|
||||||
|
var localFile: String = "",
|
||||||
|
var isReceive: Boolean = true,
|
||||||
|
var isAscii: Boolean = true,
|
||||||
|
var hostType: FTConfig.HostType = FTConfig.HostType.TSO,
|
||||||
|
var recfm: FTConfig.RecordFormat = FTConfig.RecordFormat.DEFAULT,
|
||||||
|
var lreclStr: String = "",
|
||||||
|
var blksizeStr: String = "",
|
||||||
|
var units: FTConfig.AllocationUnit = FTConfig.AllocationUnit.TRACKS,
|
||||||
|
var spaceStr: String = "",
|
||||||
|
var dirBlocks: String = "",
|
||||||
|
var cmsOptions: String = ""
|
||||||
|
)
|
||||||
|
private var lastTransferState: LastTransferState? = null
|
||||||
|
|
||||||
|
fun getLastTransferState(): LastTransferState? = lastTransferState
|
||||||
|
fun resetSessionState() { lastTransferState = null }
|
||||||
|
|
||||||
|
fun formatHostFilename(localName: String, hostType: FTConfig.HostType): String {
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
val lastDot = localName.lastIndexOf('.')
|
||||||
|
return if (lastDot > 0) {
|
||||||
|
val fn = localName.substring(0, lastDot).replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
|
||||||
|
val ft = localName.substring(lastDot + 1).replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
|
||||||
|
"${fn.ifBlank { "FILE" }} ${ft.ifBlank { "TEXT" }} A"
|
||||||
|
} else {
|
||||||
|
val clean = localName.replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
|
||||||
|
"${clean.ifBlank { "FILE" }} FILE A"
|
||||||
|
}
|
||||||
|
} else if (hostType == FTConfig.HostType.TSO) {
|
||||||
|
val cleanBase = localName.substringBeforeLast('.').replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
|
||||||
|
return "'${cleanBase.ifBlank { "DATA" }}'"
|
||||||
|
}
|
||||||
|
return localName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatLocalFilenameFromHost(hostFile: String, hostType: FTConfig.HostType): String {
|
||||||
|
val cleanName = hostFile.trim('\'', '"')
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
val tokens = cleanName.trim().split(Regex("\\s+"))
|
||||||
|
if (tokens.size >= 2) {
|
||||||
|
return "${tokens[0].lowercase()}.${tokens[1].lowercase()}"
|
||||||
|
} else if (tokens.size == 1 && tokens[0].isNotBlank()) {
|
||||||
|
return "${tokens[0].lowercase()}.txt"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val lastDot = cleanName.lastIndexOf('.')
|
||||||
|
if (lastDot > 0) {
|
||||||
|
return "${cleanName.substring(lastDot + 1).lowercase()}.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (cleanName.isNotBlank()) "${cleanName.lowercase()}.txt" else "download.txt"
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FileTransferDialog(
|
fun FileTransferDialog(
|
||||||
initialHostType: String = "TSO",
|
initialHostType: String = "TSO",
|
||||||
@@ -54,15 +109,27 @@ fun FileTransferDialog(
|
|||||||
else -> FTConfig.HostType.TSO
|
else -> FTConfig.HostType.TSO
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostFile by remember { mutableStateOf("") }
|
val clientConfig = client?.config
|
||||||
var localFile by remember { mutableStateOf("") }
|
val configuredFtType = clientConfig?.ftHostType
|
||||||
|
val effectiveHostType = if (configuredFtType != null && (clientConfig.isSavedConnection || lastTransferState == null)) {
|
||||||
|
configuredFtType
|
||||||
|
} else {
|
||||||
|
lastTransferState?.hostType ?: initialTypeEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostFile by remember { mutableStateOf(lastTransferState?.hostFile ?: "") }
|
||||||
|
var localFile by remember { mutableStateOf(lastTransferState?.localFile ?: "") }
|
||||||
var localDisplayName by remember { mutableStateOf("") }
|
var localDisplayName by remember { mutableStateOf("") }
|
||||||
var isReceive by remember { mutableStateOf(true) }
|
var isReceive by remember { mutableStateOf(lastTransferState?.isReceive ?: true) }
|
||||||
var isAscii by remember { mutableStateOf(true) }
|
var isAscii by remember { mutableStateOf(lastTransferState?.isAscii ?: true) }
|
||||||
var hostType by remember { mutableStateOf(initialTypeEnum) }
|
var hostType by remember { mutableStateOf(effectiveHostType) }
|
||||||
var recfm by remember { mutableStateOf(FTConfig.RecordFormat.DEFAULT) }
|
var recfm by remember { mutableStateOf(lastTransferState?.recfm ?: FTConfig.RecordFormat.DEFAULT) }
|
||||||
var lreclStr by remember { mutableStateOf("") }
|
var lreclStr by remember { mutableStateOf(lastTransferState?.lreclStr ?: "") }
|
||||||
var blksizeStr by remember { mutableStateOf("") }
|
var blksizeStr by remember { mutableStateOf(lastTransferState?.blksizeStr ?: "") }
|
||||||
|
var units by remember { mutableStateOf(lastTransferState?.units ?: FTConfig.AllocationUnit.TRACKS) }
|
||||||
|
var spaceStr by remember { mutableStateOf(lastTransferState?.spaceStr ?: "") }
|
||||||
|
var dirBlocks by remember { mutableStateOf(lastTransferState?.dirBlocks ?: "") }
|
||||||
|
var cmsOptions by remember { mutableStateOf(lastTransferState?.cmsOptions ?: "") }
|
||||||
var overwrite by remember { mutableStateOf(true) }
|
var overwrite by remember { mutableStateOf(true) }
|
||||||
var showBrowseDialog by remember { mutableStateOf(false) }
|
var showBrowseDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -86,8 +153,7 @@ fun FileTransferDialog(
|
|||||||
localFile = tempFile.absolutePath
|
localFile = tempFile.absolutePath
|
||||||
localDisplayName = displayName
|
localDisplayName = displayName
|
||||||
if (hostFile.isBlank()) {
|
if (hostFile.isBlank()) {
|
||||||
val cleanBase = displayName.substringBeforeLast('.').replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
|
hostFile = formatHostFilename(displayName, hostType)
|
||||||
hostFile = if (hostType == FTConfig.HostType.TSO) "'$cleanBase'" else "$cleanBase FILE A"
|
|
||||||
}
|
}
|
||||||
Toast.makeText(context, "Selected: $displayName", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Selected: $displayName", Toast.LENGTH_SHORT).show()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -135,16 +201,34 @@ fun FileTransferDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showBrowseDialog) {
|
if (showBrowseDialog) {
|
||||||
|
val initialQuery = resolveDefaultQuery(hostType, hostFile)
|
||||||
HostDirectoryDialog(
|
HostDirectoryDialog(
|
||||||
client = client,
|
client = client,
|
||||||
initialHostType = hostType,
|
initialHostType = hostType,
|
||||||
initialQuery = hostFile,
|
initialQuery = initialQuery,
|
||||||
onDismiss = { showBrowseDialog = false },
|
onDismiss = { showBrowseDialog = false },
|
||||||
onSelectDataset = { selected ->
|
onSelectEntry = { selectedName, selectedEntry ->
|
||||||
hostFile = selected
|
hostFile = selectedName
|
||||||
if (localFile.isBlank()) {
|
if (selectedEntry != null) {
|
||||||
val base = selected.trim('\'').substringAfterLast('.').substringAfterLast(' ')
|
val r = selectedEntry.recfm
|
||||||
val safeName = if (base.isNotBlank()) "$base.txt" else "download.txt"
|
if (!r.isNullOrBlank()) {
|
||||||
|
val mappedRecfm = when (r.trim().uppercase(Locale.US)) {
|
||||||
|
"F", "FB", "FA", "FBA", "FBM" -> FTConfig.RecordFormat.FIXED
|
||||||
|
"V", "VB", "VA", "VBA", "VBM" -> FTConfig.RecordFormat.VARIABLE
|
||||||
|
else -> FTConfig.RecordFormat.DEFAULT
|
||||||
|
}
|
||||||
|
recfm = mappedRecfm
|
||||||
|
}
|
||||||
|
if (selectedEntry.lrecl > 0) {
|
||||||
|
lreclStr = selectedEntry.lrecl.toString()
|
||||||
|
}
|
||||||
|
if (selectedEntry.blksize > 0) {
|
||||||
|
blksizeStr = selectedEntry.blksize.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val shouldAutofill = localFile.isBlank() || (lastTransferState != null && localFile == lastTransferState?.localFile)
|
||||||
|
if (shouldAutofill) {
|
||||||
|
val safeName = formatLocalFilenameFromHost(selectedName, hostType)
|
||||||
val tempDir = File(context.cacheDir, "ft_download").apply { if (!exists()) mkdirs() }
|
val tempDir = File(context.cacheDir, "ft_download").apply { if (!exists()) mkdirs() }
|
||||||
val tempFile = File(tempDir, safeName)
|
val tempFile = File(tempDir, safeName)
|
||||||
localFile = tempFile.absolutePath
|
localFile = tempFile.absolutePath
|
||||||
@@ -361,8 +445,7 @@ fun FileTransferDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (isReceive) {
|
if (isReceive) {
|
||||||
val defName = if (hostFile.isNotBlank()) {
|
val defName = if (hostFile.isNotBlank()) {
|
||||||
val base = hostFile.trim('\'').substringAfterLast('.').substringAfterLast(' ')
|
formatLocalFilenameFromHost(hostFile, hostType)
|
||||||
if (base.isNotBlank()) "$base.txt" else "download.txt"
|
|
||||||
} else "download.txt"
|
} else "download.txt"
|
||||||
createDocumentLauncher.launch(defName)
|
createDocumentLauncher.launch(defName)
|
||||||
} else {
|
} else {
|
||||||
@@ -467,10 +550,71 @@ fun FileTransferDialog(
|
|||||||
onValueChange = { blksizeStr = it.filter { c -> c.isDigit() } },
|
onValueChange = { blksizeStr = it.filter { c -> c.isDigit() } },
|
||||||
label = { Text("BLKSIZE", fontSize = 11.sp) },
|
label = { Text("BLKSIZE", fontSize = 11.sp) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done),
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("Units:", fontSize = 11.sp)
|
||||||
|
Row {
|
||||||
|
listOf(
|
||||||
|
FTConfig.AllocationUnit.TRACKS to "Tracks",
|
||||||
|
FTConfig.AllocationUnit.CYLINDERS to "Cylinders",
|
||||||
|
FTConfig.AllocationUnit.AVBLOCK to "Avblock",
|
||||||
|
FTConfig.AllocationUnit.DEFAULT to "Default"
|
||||||
|
).forEach { (u, label) ->
|
||||||
|
FilterChip(
|
||||||
|
selected = (units == u),
|
||||||
|
onClick = { units = u },
|
||||||
|
label = { Text(label, fontSize = 11.sp) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = spaceStr,
|
||||||
|
onValueChange = { spaceStr = it },
|
||||||
|
label = { Text("SPACE (Pri, Sec)", fontSize = 11.sp) },
|
||||||
|
placeholder = { Text("e.g. 5,2 or 1,1") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||||
|
modifier = Modifier.weight(0.62f)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = dirBlocks,
|
||||||
|
onValueChange = { dirBlocks = it.filter { c -> c.isDigit() } },
|
||||||
|
label = { Text("Dir (PDS)", fontSize = 11.sp) },
|
||||||
|
placeholder = { Text("e.g. 10") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done),
|
||||||
|
modifier = Modifier.weight(0.38f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CMS Options
|
||||||
|
if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = cmsOptions,
|
||||||
|
onValueChange = { cmsOptions = it },
|
||||||
|
label = { Text("CMS Options", fontSize = 11.sp) },
|
||||||
|
placeholder = { Text("e.g. ASCII CRLF or RECFM F LRECL 80") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -497,8 +641,28 @@ fun FileTransferDialog(
|
|||||||
setRecfm(recfm)
|
setRecfm(recfm)
|
||||||
lreclStr.trim().toIntOrNull()?.let { setLrecl(it) }
|
lreclStr.trim().toIntOrNull()?.let { setLrecl(it) }
|
||||||
blksizeStr.trim().toIntOrNull()?.let { setBlksize(it) }
|
blksizeStr.trim().toIntOrNull()?.let { setBlksize(it) }
|
||||||
|
setUnits(units)
|
||||||
|
if (spaceStr.isNotBlank()) setSpace(spaceStr.trim())
|
||||||
|
if (dirBlocks.isNotBlank()) setDirectoryBlocks(dirBlocks.trim())
|
||||||
|
}
|
||||||
|
if (hostType == FTConfig.HostType.CMS && cmsOptions.isNotBlank()) {
|
||||||
|
setOptions(cmsOptions.trim())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lastTransferState = LastTransferState(
|
||||||
|
hostFile = hostFile.trim(),
|
||||||
|
localFile = resolvedLocal,
|
||||||
|
isReceive = isReceive,
|
||||||
|
isAscii = isAscii,
|
||||||
|
hostType = hostType,
|
||||||
|
recfm = recfm,
|
||||||
|
lreclStr = lreclStr,
|
||||||
|
blksizeStr = blksizeStr,
|
||||||
|
units = units,
|
||||||
|
spaceStr = spaceStr,
|
||||||
|
dirBlocks = dirBlocks,
|
||||||
|
cmsOptions = cmsOptions
|
||||||
|
)
|
||||||
onStartTransfer(config)
|
onStartTransfer(config)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import haus.nightmare.lib3270j.ConnectionState
|
import haus.nightmare.lib3270j.ConnectionState
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
|
||||||
@@ -23,6 +25,11 @@ fun TwoRowKeyBar(
|
|||||||
connectionState: ConnectionState,
|
connectionState: ConnectionState,
|
||||||
isShiftPressed: Boolean = false,
|
isShiftPressed: Boolean = false,
|
||||||
isLightPenMode: Boolean = false,
|
isLightPenMode: Boolean = false,
|
||||||
|
isInsertMode: Boolean = false,
|
||||||
|
isDocMode: Boolean = false,
|
||||||
|
isWordWrap: Boolean = false,
|
||||||
|
isAplMode: Boolean = false,
|
||||||
|
fourColorOverride: Boolean = false,
|
||||||
hapticFeedbackEnabled: Boolean = true,
|
hapticFeedbackEnabled: Boolean = true,
|
||||||
onClearShift: () -> Unit = {},
|
onClearShift: () -> Unit = {},
|
||||||
onConnectClick: () -> Unit,
|
onConnectClick: () -> Unit,
|
||||||
@@ -31,9 +38,16 @@ fun TwoRowKeyBar(
|
|||||||
onFieldInspectorClick: () -> Unit = {},
|
onFieldInspectorClick: () -> Unit = {},
|
||||||
onFindClick: () -> Unit = {},
|
onFindClick: () -> Unit = {},
|
||||||
onScreenExportClick: () -> Unit = {},
|
onScreenExportClick: () -> Unit = {},
|
||||||
|
onScreenHistoryClick: () -> Unit = {},
|
||||||
onScriptClick: () -> Unit = {},
|
onScriptClick: () -> Unit = {},
|
||||||
|
onMacroRuntimeClick: () -> Unit = {},
|
||||||
onPrinterSessionClick: () -> Unit = {},
|
onPrinterSessionClick: () -> Unit = {},
|
||||||
onToggleLightPen: () -> Unit = {},
|
onToggleLightPen: () -> Unit = {},
|
||||||
|
onToggleInsert: () -> Unit = {},
|
||||||
|
onToggleDocMode: () -> Unit = {},
|
||||||
|
onToggleWordWrap: () -> Unit = {},
|
||||||
|
onToggleAplMode: () -> Unit = {},
|
||||||
|
onToggleFourColorOverride: () -> Unit = {},
|
||||||
onSettingsClick: () -> Unit = {},
|
onSettingsClick: () -> Unit = {},
|
||||||
onSendAid: (Int) -> Unit,
|
onSendAid: (Int) -> Unit,
|
||||||
onReset: () -> Unit,
|
onReset: () -> Unit,
|
||||||
@@ -51,6 +65,11 @@ fun TwoRowKeyBar(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var menuExpanded by remember { mutableStateOf(false) }
|
var menuExpanded by remember { mutableStateOf(false) }
|
||||||
|
var lastDismissTime by remember { mutableLongStateOf(0L) }
|
||||||
|
|
||||||
|
BackHandler(enabled = menuExpanded) {
|
||||||
|
menuExpanded = false
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -73,11 +92,22 @@ fun TwoRowKeyBar(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
hapticFeedbackEnabled = hapticFeedbackEnabled,
|
hapticFeedbackEnabled = hapticFeedbackEnabled,
|
||||||
innerPaddingHorizontal = 0.dp,
|
innerPaddingHorizontal = 0.dp,
|
||||||
onClick = { menuExpanded = true }
|
onClick = {
|
||||||
|
if (System.currentTimeMillis() - lastDismissTime > 200L) {
|
||||||
|
menuExpanded = !menuExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = menuExpanded,
|
expanded = menuExpanded,
|
||||||
onDismissRequest = { menuExpanded = false },
|
onDismissRequest = {
|
||||||
|
menuExpanded = false
|
||||||
|
lastDismissTime = System.currentTimeMillis()
|
||||||
|
},
|
||||||
|
properties = PopupProperties(
|
||||||
|
focusable = false,
|
||||||
|
dismissOnClickOutside = true
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(Color(0xFF2C2D30))
|
.background(Color(0xFF2C2D30))
|
||||||
.heightIn(max = 420.dp)
|
.heightIn(max = 420.dp)
|
||||||
@@ -134,6 +164,13 @@ fun TwoRowKeyBar(
|
|||||||
onScreenExportClick()
|
onScreenExportClick()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Screen History", color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onScreenHistoryClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Run Keystroke Script", color = Color.White) },
|
text = { Text("Run Keystroke Script", color = Color.White) },
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -141,6 +178,13 @@ fun TwoRowKeyBar(
|
|||||||
onScriptClick()
|
onScriptClick()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Macro Runtime (<HASCRIPT>)", color = Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onMacroRuntimeClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("3287 Printer Session", color = Color.White) },
|
text = { Text("3287 Printer Session", color = Color.White) },
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -178,6 +222,42 @@ fun TwoRowKeyBar(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
HorizontalDivider(color = Color(0xFF3E4044))
|
HorizontalDivider(color = Color(0xFF3E4044))
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Insert Mode: ${if (isInsertMode) "ON" else "OFF"}", color = if (isInsertMode) Color(0xFF7890F0) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleInsert()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Document Mode (DOC): ${if (isDocMode) "ON" else "OFF"}", color = if (isDocMode) Color(0xFF38D9A9) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleDocMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Word Wrap: ${if (isWordWrap) "ON" else "OFF"}", color = if (isWordWrap) Color(0xFF38D9A9) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleWordWrap()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("APL Keyboard Mode: ${if (isAplMode) "ON" else "OFF"}", color = if (isAplMode) Color(0xFFFFA94D) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleAplMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Base 4-Color Override: ${if (fourColorOverride) "ON" else "OFF"}", color = if (fourColorOverride) Color(0xFFFFD43B) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleFourColorOverride()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(color = Color(0xFF3E4044))
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package haus.nightmare.a3270.ui
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import android.content.ClipDescription
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
@@ -15,6 +18,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -28,8 +32,139 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import haus.nightmare.lib3270j.Telnet3270Client
|
import haus.nightmare.lib3270j.Telnet3270Client
|
||||||
import haus.nightmare.lib3270j.ft.FTConfig
|
import haus.nightmare.lib3270j.ft.FTConfig
|
||||||
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry
|
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.HostDirectoryEntry
|
||||||
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry
|
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
fun resolveDefaultQuery(hostType: FTConfig.HostType, initialQuery: String?): String {
|
||||||
|
val defaultQuery = initialQuery?.trim() ?: ""
|
||||||
|
if (hostType == FTConfig.HostType.CMS && (defaultQuery.isEmpty() || defaultQuery == "* * *" || defaultQuery == "*")) {
|
||||||
|
return "* * A"
|
||||||
|
}
|
||||||
|
return defaultQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatCMSFilter(pattern: String?): String {
|
||||||
|
if (pattern.isNullOrBlank()) {
|
||||||
|
return "* * A"
|
||||||
|
}
|
||||||
|
val clean = pattern.replace("'", "").replace("\"", "").trim()
|
||||||
|
if (clean == "* * *" || clean == "*") {
|
||||||
|
return "* * A"
|
||||||
|
} else if (clean.contains('.')) {
|
||||||
|
val dot = clean.indexOf('.')
|
||||||
|
return clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A"
|
||||||
|
} else {
|
||||||
|
val parts = clean.split(Regex("\\s+"))
|
||||||
|
return when (parts.size) {
|
||||||
|
1 -> "${parts[0]} * A"
|
||||||
|
2 -> "${parts[0]} ${parts[1]} A"
|
||||||
|
else -> "${parts[0]} ${parts[1]} ${parts[2]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matchesFilter(entry: HostDirectoryEntry, filter: String?): Boolean {
|
||||||
|
if (filter.isNullOrBlank()) return true
|
||||||
|
val cleanFilter = filter.replace("'", "").replace("\"", "").trim()
|
||||||
|
if (cleanFilter.isEmpty() || cleanFilter == "*" || cleanFilter == "* * *" || cleanFilter == "*.*") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return when (entry) {
|
||||||
|
is CMSDirectoryEntry -> matchesCmsFilter(entry, cleanFilter)
|
||||||
|
is TSODirectoryEntry -> matchesTsoFilter(entry, cleanFilter)
|
||||||
|
else -> matchesWildcard(entry.name, cleanFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matchesCmsFilter(cms: CMSDirectoryEntry, filter: String): Boolean {
|
||||||
|
val fn = cms.filename?.trim() ?: ""
|
||||||
|
val ft = cms.filetype?.trim() ?: ""
|
||||||
|
val fm = cms.filemode?.trim() ?: ""
|
||||||
|
|
||||||
|
val tokens = filter.split(Regex("\\s+"))
|
||||||
|
return if (tokens.size >= 3) {
|
||||||
|
matchesWildcard(fn, tokens[0]) && matchesWildcard(ft, tokens[1]) && matchesCmsFilemode(fm, tokens[2])
|
||||||
|
} else if (tokens.size == 2) {
|
||||||
|
matchesWildcard(fn, tokens[0]) && matchesWildcard(ft, tokens[1])
|
||||||
|
} else {
|
||||||
|
val tok = tokens[0]
|
||||||
|
if (tok.contains('.')) {
|
||||||
|
val dot = tok.indexOf('.')
|
||||||
|
matchesWildcard(fn, tok.substring(0, dot)) && matchesWildcard(ft, tok.substring(dot + 1))
|
||||||
|
} else if (matchesWildcard(fn, tok) || matchesWildcard(ft, tok) || matchesCmsFilemode(fm, tok)) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
matchesWildcard(cms.name, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matchesCmsFilemode(actualFm: String?, fmPattern: String?): Boolean {
|
||||||
|
if (fmPattern.isNullOrEmpty() || fmPattern == "*") return true
|
||||||
|
if (actualFm.isNullOrEmpty()) return false
|
||||||
|
if (fmPattern.length == 1 && fmPattern[0].isLetter()) {
|
||||||
|
return actualFm.startsWith(fmPattern, ignoreCase = true)
|
||||||
|
}
|
||||||
|
return matchesWildcard(actualFm, fmPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matchesTsoFilter(tso: TSODirectoryEntry, filter: String): Boolean {
|
||||||
|
val dsn = tso.datasetName?.trim() ?: ""
|
||||||
|
if (filter == "*" || filter == "* * *" || filter == "*.*") return true
|
||||||
|
if (filter.contains('*') || filter.contains('?')) {
|
||||||
|
return matchesWildcard(dsn, filter)
|
||||||
|
}
|
||||||
|
return dsn.contains(filter, ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun matchesWildcard(text: String?, pattern: String?): Boolean {
|
||||||
|
val t = text ?: ""
|
||||||
|
if (pattern.isNullOrEmpty() || pattern == "*") return true
|
||||||
|
|
||||||
|
val regex = buildString {
|
||||||
|
append("^")
|
||||||
|
for (c in pattern) {
|
||||||
|
when (c) {
|
||||||
|
'*' -> append(".*")
|
||||||
|
'?' -> append(".")
|
||||||
|
in "\\.[]{}()+^$|" -> append("\\").append(c)
|
||||||
|
else -> append(Regex.escape(c.uppercaseChar().toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append("$")
|
||||||
|
}
|
||||||
|
return t.uppercase(Locale.US).matches(Regex(regex))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun waitForKeyboardUnlock(client: Telnet3270Client?, timeoutMs: Long): Boolean {
|
||||||
|
if (client == null) return false
|
||||||
|
val deadline = System.currentTimeMillis() + timeoutMs
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
var locked = false
|
||||||
|
if (client.inputProcessor != null && client.inputProcessor.isKeyboardLocked) {
|
||||||
|
locked = true
|
||||||
|
}
|
||||||
|
if (client.oia != null && (client.oia.isKeyLocked || client.oia.isXSystem)) {
|
||||||
|
locked = true
|
||||||
|
}
|
||||||
|
if (!locked) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(40)
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
Thread.currentThread().interrupt()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HostDirectoryDialog(
|
fun HostDirectoryDialog(
|
||||||
@@ -37,71 +172,475 @@ fun HostDirectoryDialog(
|
|||||||
initialHostType: FTConfig.HostType = FTConfig.HostType.TSO,
|
initialHostType: FTConfig.HostType = FTConfig.HostType.TSO,
|
||||||
initialQuery: String = "",
|
initialQuery: String = "",
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSelectDataset: (String) -> Unit
|
onSelectDataset: (String) -> Unit = {},
|
||||||
|
onSelectEntry: ((String, HostDirectoryEntry?) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
var hostType by remember { mutableStateOf(initialHostType) }
|
var hostType by remember { mutableStateOf(initialHostType) }
|
||||||
var query by remember { mutableStateOf(initialQuery) }
|
var query by remember { mutableStateOf(resolveDefaultQuery(initialHostType, initialQuery)) }
|
||||||
var entries by remember { mutableStateOf<List<HostDirectoryEntry>>(emptyList()) }
|
var allEntries by remember { mutableStateOf<List<HostDirectoryEntry>>(emptyList()) }
|
||||||
var selectedIndex by remember { mutableIntStateOf(-1) }
|
var selectedIndex by remember { mutableIntStateOf(-1) }
|
||||||
var statusMessage by remember { mutableStateOf("") }
|
var statusMessage by remember { mutableStateOf("") }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun runQuery() {
|
var inspectionEntry by remember { mutableStateOf<TSODirectoryEntry?>(null) }
|
||||||
val c = client
|
var showInspectionDialog by remember { mutableStateOf(false) }
|
||||||
if (c == null || !c.connectionState.isFullSession) {
|
|
||||||
statusMessage = "Terminal not connected."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val xfer = c.xfer
|
|
||||||
if (xfer == null) {
|
|
||||||
statusMessage = "ECL transfer interface not available."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val q = query.trim()
|
// Client-side filtering
|
||||||
if (hostType == FTConfig.HostType.CMS) {
|
val currentEntries by remember(allEntries, query, hostType) {
|
||||||
val list = xfer.getCmsDirectory(q)
|
derivedStateOf {
|
||||||
entries = list ?: emptyList()
|
val filter = query.trim()
|
||||||
|
if (filter.isEmpty()) {
|
||||||
|
allEntries
|
||||||
|
} else {
|
||||||
|
allEntries.filter { matchesFilter(it, filter) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseTextData(text: String, source: String) {
|
||||||
|
allEntries = if (hostType == FTConfig.HostType.CMS) {
|
||||||
|
CMSDirectoryParser.parse(text)
|
||||||
} else {
|
} else {
|
||||||
val list = xfer.getTsoDirectory(q)
|
TSODirectoryParser.parse(text)
|
||||||
entries = list ?: emptyList()
|
|
||||||
}
|
}
|
||||||
selectedIndex = -1
|
selectedIndex = -1
|
||||||
statusMessage = "${entries.size} entries found."
|
statusMessage = if (allEntries.isEmpty()) {
|
||||||
|
"0 entries found from $source. Use 'Query Host' or 'Paste Clipboard'."
|
||||||
|
} else {
|
||||||
|
"Parsed ${allEntries.size} entries from $source."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseScreen() {
|
fun parseScreen() {
|
||||||
val c = client
|
val c = client
|
||||||
if (c == null || !c.connectionState.isFullSession) {
|
if (c == null || !c.connectionState.isFullSession || c.ps == null) {
|
||||||
statusMessage = "No active screen to parse."
|
statusMessage = "No active session to parse."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val xfer = c.xfer
|
val rows = c.ps.rows
|
||||||
if (xfer == null) {
|
val cols = c.ps.cols
|
||||||
statusMessage = "ECL transfer interface not available."
|
val screenText = c.ps.copyString(0, 0, rows - 1, cols - 1)
|
||||||
|
parseTextData(screenText, "presentation space")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseClipboard() {
|
||||||
|
try {
|
||||||
|
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||||
|
if (cm != null && cm.hasPrimaryClip()) {
|
||||||
|
val clip = cm.primaryClip
|
||||||
|
if (clip != null && clip.itemCount > 0) {
|
||||||
|
val clipText = clip.getItemAt(0).coerceToText(context)?.toString()
|
||||||
|
if (!clipText.isNullOrBlank()) {
|
||||||
|
parseTextData(clipText, "clipboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statusMessage = "Clipboard contains no text."
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
statusMessage = "Error reading clipboard: ${ex.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryHost() {
|
||||||
|
val c = client
|
||||||
|
if (c == null || !c.connectionState.isFullSession || c.ps == null) {
|
||||||
|
statusMessage = "No active session to query."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val ps = c.ps
|
val pattern = query.trim()
|
||||||
val screenText = ps.getString(0, ps.size)
|
val curHostType = hostType
|
||||||
if (hostType == FTConfig.HostType.CMS) {
|
statusMessage = "Querying host ($curHostType)..."
|
||||||
val list = xfer.getCmsDirectory(screenText)
|
isLoading = true
|
||||||
entries = list ?: emptyList()
|
|
||||||
} else {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
val list = xfer.getTsoDirectory(screenText)
|
try {
|
||||||
entries = list ?: emptyList()
|
val allPages = StringBuilder()
|
||||||
|
val maxPages = 500
|
||||||
|
|
||||||
|
if (curHostType == FTConfig.HostType.CMS) {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
waitForKeyboardUnlock(c, 3000)
|
||||||
|
|
||||||
|
val cmsFilter = formatCMSFilter(pattern)
|
||||||
|
c.ps.SendKeys("LISTFILE $cmsFilter ( nohead alloc date[enter]")
|
||||||
|
Thread.sleep(100)
|
||||||
|
|
||||||
|
for (page in 0 until maxPages) {
|
||||||
|
val startWait = System.currentTimeMillis()
|
||||||
|
val timeoutMs = 10000L
|
||||||
|
var screen = ""
|
||||||
|
var isMore = false
|
||||||
|
|
||||||
|
while (System.currentTimeMillis() - startWait < timeoutMs) {
|
||||||
|
waitForKeyboardUnlock(c, 500)
|
||||||
|
val rows = c.ps.rows
|
||||||
|
val cols = c.ps.cols
|
||||||
|
screen = c.ps.copyString(0, 0, rows - 1, cols - 1)
|
||||||
|
val upper = screen.uppercase(Locale.US)
|
||||||
|
|
||||||
|
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
|
||||||
|
isMore = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (upper.contains("READY;") || upper.contains("READY(")) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Thread.sleep(150)
|
||||||
|
}
|
||||||
|
|
||||||
|
allPages.append(screen).append("\n")
|
||||||
|
|
||||||
|
if (isMore) {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
Thread.sleep(100)
|
||||||
|
waitForKeyboardUnlock(c, 3000)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.ps != null) {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
waitForKeyboardUnlock(c, 3000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val rows = c.ps.rows
|
||||||
|
val cols = c.ps.cols
|
||||||
|
val initialScreen = c.ps.copyString(0, 0, rows - 1, cols - 1)
|
||||||
|
val isFormatted = c.screenBuffer != null && c.screenBuffer.isFormatted
|
||||||
|
val hasCmdArrow = initialScreen.contains("===>")
|
||||||
|
val isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF")
|
||||||
|
|
||||||
|
var tsoCmd = "TSO LISTCAT ALL"
|
||||||
|
if (pattern.isNotEmpty()) {
|
||||||
|
var dsnPattern = pattern.trim()
|
||||||
|
if (dsnPattern.startsWith("'") && dsnPattern.endsWith("'") && dsnPattern.length > 2) {
|
||||||
|
dsnPattern = dsnPattern.substring(1, dsnPattern.length - 1)
|
||||||
|
}
|
||||||
|
val paren = dsnPattern.indexOf('(')
|
||||||
|
if (paren > 0) {
|
||||||
|
dsnPattern = dsnPattern.substring(0, paren)
|
||||||
|
}
|
||||||
|
if (dsnPattern == "*" || dsnPattern == "* * *" || dsnPattern == "*.*") {
|
||||||
|
tsoCmd = "TSO LISTCAT ALL"
|
||||||
|
} else {
|
||||||
|
var level = dsnPattern
|
||||||
|
if (level.endsWith(".*")) {
|
||||||
|
level = level.substring(0, level.length - 2)
|
||||||
|
} else if (level.endsWith("*")) {
|
||||||
|
level = level.substring(0, level.length - 1)
|
||||||
|
}
|
||||||
|
tsoCmd = if (level.isEmpty()) "TSO LISTCAT ALL" else "TSO LISTCAT LEVEL($level) ALL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIspf) {
|
||||||
|
val arrowPos = initialScreen.indexOf("===>")
|
||||||
|
if (arrowPos >= 0 && c.screenBuffer != null) {
|
||||||
|
val inputAddr = c.screenBuffer.findNextUnprotected(arrowPos)
|
||||||
|
if (inputAddr > 0) {
|
||||||
|
c.screenBuffer.cursorAddress = inputAddr
|
||||||
|
c.screenBuffer.updateDisplaySnapshot()
|
||||||
|
} else {
|
||||||
|
c.ps.SendKeys("[home]")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.ps.SendKeys("[home]")
|
||||||
|
}
|
||||||
|
c.ps.SendKeys("[erase_eof]")
|
||||||
|
c.ps.SendKeys("$tsoCmd[enter]")
|
||||||
|
} else {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
waitForKeyboardUnlock(c, 3000)
|
||||||
|
c.ps.SendKeys("$tsoCmd[enter]")
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(100)
|
||||||
|
|
||||||
|
val startCmd = System.currentTimeMillis()
|
||||||
|
while (System.currentTimeMillis() - startCmd < 4000) {
|
||||||
|
waitForKeyboardUnlock(c, 200)
|
||||||
|
val r = c.ps.rows
|
||||||
|
val col = c.ps.cols
|
||||||
|
val s = c.ps.copyString(0, 0, r - 1, col - 1)
|
||||||
|
val u = s.uppercase(Locale.US)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevScreen = ""
|
||||||
|
for (page in 0 until maxPages) {
|
||||||
|
waitForKeyboardUnlock(c, 300)
|
||||||
|
val r = c.ps.rows
|
||||||
|
val col = c.ps.cols
|
||||||
|
val screen = c.ps.copyString(0, 0, r - 1, col - 1)
|
||||||
|
val upper = screen.uppercase(Locale.US)
|
||||||
|
|
||||||
|
if (screen == prevScreen || screen.isBlank()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
prevScreen = screen
|
||||||
|
allPages.append(screen).append("\n")
|
||||||
|
|
||||||
|
if (upper.contains("***")) {
|
||||||
|
c.ps.SendKeys("[enter]")
|
||||||
|
Thread.sleep(100)
|
||||||
|
waitForKeyboardUnlock(c, 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--")) {
|
||||||
|
c.ps.SendKeys("[enter]")
|
||||||
|
Thread.sleep(100)
|
||||||
|
waitForKeyboardUnlock(c, 1000)
|
||||||
|
|
||||||
|
val r2 = c.ps.rows
|
||||||
|
val col2 = c.ps.cols
|
||||||
|
val s2 = c.ps.copyString(0, 0, r2 - 1, col2 - 1)
|
||||||
|
if (s2.uppercase(Locale.US).contains("***")) {
|
||||||
|
prevScreen = s2
|
||||||
|
allPages.append(s2).append("\n")
|
||||||
|
c.ps.SendKeys("[enter]")
|
||||||
|
Thread.sleep(100)
|
||||||
|
waitForKeyboardUnlock(c, 2000)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if (s2 != screen && s2.isNotBlank()) {
|
||||||
|
allPages.append(s2).append("\n")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.ps != null) {
|
||||||
|
if (isIspf) {
|
||||||
|
c.ps.SendKeys("[enter]")
|
||||||
|
Thread.sleep(150)
|
||||||
|
waitForKeyboardUnlock(c, 1500)
|
||||||
|
|
||||||
|
val r = c.ps.rows
|
||||||
|
val col = c.ps.cols
|
||||||
|
val currentScr = c.ps.copyString(0, 0, r - 1, col - 1)
|
||||||
|
if (currentScr.uppercase(Locale.US).contains("NONVSAM") || currentScr.uppercase(Locale.US).contains("IDCAMS")
|
||||||
|
|| currentScr.contains(" --- ") || currentScr.uppercase(Locale.US).contains("--RECFM-")
|
||||||
|
|| (c.screenBuffer != null && !c.screenBuffer.isFormatted)) {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
waitForKeyboardUnlock(c, 2000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
waitForKeyboardUnlock(c, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val collectedText = allPages.toString()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
parseTextData(collectedText, "host query")
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
statusMessage = "Host query failed: ${e.message}"
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inspectSelectedDataset() {
|
||||||
|
val selected = currentEntries.getOrNull(selectedIndex)
|
||||||
|
if (selected == null || selected !is TSODirectoryEntry) {
|
||||||
|
statusMessage = "Please select a TSO dataset entry to inspect."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val c = client
|
||||||
|
if (c == null || !c.connectionState.isFullSession || c.ps == null) {
|
||||||
|
statusMessage = "No active session to query host."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val dsn = selected.datasetName
|
||||||
|
if (dsn.isNullOrBlank()) return
|
||||||
|
|
||||||
|
statusMessage = "Inspecting $dsn via TSO LISTDS..."
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val output = StringBuilder()
|
||||||
|
val rows = c.ps.rows
|
||||||
|
val cols = c.ps.cols
|
||||||
|
val initialScreen = c.ps.copyString(0, 0, rows - 1, cols - 1)
|
||||||
|
val isFormatted = c.screenBuffer != null && c.screenBuffer.isFormatted
|
||||||
|
val hasCmdArrow = initialScreen.contains("===>")
|
||||||
|
val isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF")
|
||||||
|
|
||||||
|
val tsoCmd = "TSO LISTDS '$dsn' STATUS"
|
||||||
|
|
||||||
|
if (isIspf) {
|
||||||
|
val arrowPos = initialScreen.indexOf("===>")
|
||||||
|
if (arrowPos >= 0 && c.screenBuffer != null) {
|
||||||
|
val inputAddr = c.screenBuffer.findNextUnprotected(arrowPos)
|
||||||
|
if (inputAddr > 0) {
|
||||||
|
c.screenBuffer.cursorAddress = inputAddr
|
||||||
|
c.screenBuffer.updateDisplaySnapshot()
|
||||||
|
} else {
|
||||||
|
c.ps.SendKeys("[home]")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.ps.SendKeys("[home]")
|
||||||
|
}
|
||||||
|
c.ps.SendKeys("[erase_eof]")
|
||||||
|
c.ps.SendKeys("$tsoCmd[enter]")
|
||||||
|
} else {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
waitForKeyboardUnlock(c, 3000)
|
||||||
|
c.ps.SendKeys("$tsoCmd[enter]")
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(100)
|
||||||
|
|
||||||
|
val startCmd = System.currentTimeMillis()
|
||||||
|
while (System.currentTimeMillis() - startCmd < 4000) {
|
||||||
|
waitForKeyboardUnlock(c, 200)
|
||||||
|
val r = c.ps.rows
|
||||||
|
val col = c.ps.cols
|
||||||
|
val s = c.ps.copyString(0, 0, r - 1, col - 1)
|
||||||
|
val u = s.uppercase(Locale.US)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevScreen = ""
|
||||||
|
for (page in 0 until 5) {
|
||||||
|
waitForKeyboardUnlock(c, 300)
|
||||||
|
val r = c.ps.rows
|
||||||
|
val col = c.ps.cols
|
||||||
|
val screen = c.ps.copyString(0, 0, r - 1, col - 1)
|
||||||
|
val upper = screen.uppercase(Locale.US)
|
||||||
|
|
||||||
|
if (screen == prevScreen || screen.isBlank()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
prevScreen = screen
|
||||||
|
output.append(screen).append("\n")
|
||||||
|
|
||||||
|
if (upper.contains("***")) {
|
||||||
|
c.ps.SendKeys("[enter]")
|
||||||
|
Thread.sleep(100)
|
||||||
|
waitForKeyboardUnlock(c, 2000)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.ps != null) {
|
||||||
|
if (isIspf) {
|
||||||
|
c.ps.SendKeys("[enter]")
|
||||||
|
Thread.sleep(150)
|
||||||
|
waitForKeyboardUnlock(c, 1500)
|
||||||
|
|
||||||
|
val r = c.ps.rows
|
||||||
|
val col = c.ps.cols
|
||||||
|
val currentScr = c.ps.copyString(0, 0, r - 1, col - 1)
|
||||||
|
if (currentScr.uppercase(Locale.US).contains("--RECFM-") || (c.screenBuffer != null && !c.screenBuffer.isFormatted)) {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
waitForKeyboardUnlock(c, 2000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.ps.SendKeys("[clear]")
|
||||||
|
waitForKeyboardUnlock(c, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val outText = output.toString()
|
||||||
|
val parsed = TSODirectoryParser.parse(outText)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
isLoading = false
|
||||||
|
if (parsed.isNotEmpty()) {
|
||||||
|
val updated = parsed[0]
|
||||||
|
if (updated.recfm != null) selected.recfm = updated.recfm
|
||||||
|
if (updated.lrecl > 0) selected.lrecl = updated.lrecl
|
||||||
|
if (updated.blksize > 0) selected.blksize = updated.blksize
|
||||||
|
if (updated.dsorg != null) selected.dsorg = updated.dsorg
|
||||||
|
if (updated.volume != null && (selected.volume == null || selected.volume == "******" || selected.volume.isEmpty())) {
|
||||||
|
selected.volume = updated.volume
|
||||||
|
}
|
||||||
|
if (updated.tracksAllocated > 0) selected.tracksAllocated = updated.tracksAllocated
|
||||||
|
if (updated.extents > 0) selected.extents = updated.extents
|
||||||
|
|
||||||
|
statusMessage = "Attributes for $dsn: RECFM=${selected.recfm}, LRECL=${selected.lrecl}, BLKSIZE=${selected.blksize}, VOL=${selected.volume}"
|
||||||
|
inspectionEntry = selected
|
||||||
|
showInspectionDialog = true
|
||||||
|
} else {
|
||||||
|
statusMessage = "No VTOC attributes found for $dsn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
isLoading = false
|
||||||
|
statusMessage = "Inspection failed: ${ex.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
selectedIndex = -1
|
|
||||||
statusMessage = "Parsed ${entries.size} entries from screen."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (initialQuery.isNotBlank()) {
|
if (client != null && client.connectionState.isFullSession) {
|
||||||
runQuery()
|
queryHost()
|
||||||
} else {
|
} else {
|
||||||
parseScreen()
|
parseScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showInspectionDialog && inspectionEntry != null) {
|
||||||
|
val entry = inspectionEntry!!
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showInspectionDialog = false },
|
||||||
|
title = { Text("Dataset Attributes (LISTDS)", fontWeight = FontWeight.Bold, fontSize = 16.sp) },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
Text("Dataset: ${entry.datasetName}", fontWeight = FontWeight.Bold, fontSize = 13.sp, color = Color.White)
|
||||||
|
HorizontalDivider(color = Color(0xFF373A40))
|
||||||
|
Text("Volume: ${entry.volume ?: "N/A"}", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Text("Record Format (RECFM): ${entry.recfm ?: "N/A"}", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Text("Logical Record Length (LRECL): ${if (entry.lrecl > 0) entry.lrecl.toString() else "N/A"}", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Text("Block Size (BLKSIZE): ${if (entry.blksize > 0) entry.blksize.toString() else "N/A"}", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Text("Dataset Organization (DSORG): ${entry.dsorg ?: "N/A"}", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
if (entry.tracksAllocated > 0) {
|
||||||
|
Text("Allocated Tracks: ${entry.tracksAllocated}", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
if (entry.extents > 0) {
|
||||||
|
Text("Extents: ${entry.extents}", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = { showInspectionDialog = false }) {
|
||||||
|
Text("OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
@@ -135,13 +674,33 @@ fun HostDirectoryDialog(
|
|||||||
Row {
|
Row {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = (hostType == FTConfig.HostType.TSO),
|
selected = (hostType == FTConfig.HostType.TSO),
|
||||||
onClick = { hostType = FTConfig.HostType.TSO },
|
onClick = {
|
||||||
|
hostType = FTConfig.HostType.TSO
|
||||||
|
if (query == "* * A" || query == "* * *") {
|
||||||
|
query = ""
|
||||||
|
}
|
||||||
|
if (client != null && client.connectionState.isFullSession) {
|
||||||
|
queryHost()
|
||||||
|
} else {
|
||||||
|
parseScreen()
|
||||||
|
}
|
||||||
|
},
|
||||||
label = { Text("TSO", fontSize = 11.sp) }
|
label = { Text("TSO", fontSize = 11.sp) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = (hostType == FTConfig.HostType.CMS),
|
selected = (hostType == FTConfig.HostType.CMS),
|
||||||
onClick = { hostType = FTConfig.HostType.CMS },
|
onClick = {
|
||||||
|
hostType = FTConfig.HostType.CMS
|
||||||
|
if (query.isBlank() || query == "* * *" || query == "*") {
|
||||||
|
query = "* * A"
|
||||||
|
}
|
||||||
|
if (client != null && client.connectionState.isFullSession) {
|
||||||
|
queryHost()
|
||||||
|
} else {
|
||||||
|
parseScreen()
|
||||||
|
}
|
||||||
|
},
|
||||||
label = { Text("CMS", fontSize = 11.sp) }
|
label = { Text("CMS", fontSize = 11.sp) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -155,8 +714,8 @@ fun HostDirectoryDialog(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = query,
|
value = query,
|
||||||
onValueChange = { query = it },
|
onValueChange = { query = it },
|
||||||
label = { Text("Query Pattern") },
|
label = { Text("Filter / Pattern") },
|
||||||
placeholder = { Text(if (hostType == FTConfig.HostType.TSO) "USER.*" else "* EXEC *") },
|
placeholder = { Text(if (hostType == FTConfig.HostType.TSO) "USER.*" else "* * A") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
@@ -164,14 +723,29 @@ fun HostDirectoryDialog(
|
|||||||
keyboardType = KeyboardType.Ascii,
|
keyboardType = KeyboardType.Ascii,
|
||||||
imeAction = ImeAction.Search
|
imeAction = ImeAction.Search
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(onSearch = { runQuery() }),
|
keyboardActions = KeyboardActions(onSearch = {
|
||||||
|
if (query.contains("\n") || query.contains("\r") || query.uppercase(Locale.US).contains("NONVSAM")
|
||||||
|
|| query.uppercase(Locale.US).contains("IN-CAT") || query.uppercase(Locale.US).contains("FILENAME")) {
|
||||||
|
parseTextData(query, "query text")
|
||||||
|
} else if (allEntries.isEmpty() && client != null && client.connectionState.isFullSession) {
|
||||||
|
queryHost()
|
||||||
|
}
|
||||||
|
}),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
onClick = { runQuery() },
|
onClick = {
|
||||||
|
if (query.contains("\n") || query.contains("\r") || query.uppercase(Locale.US).contains("NONVSAM")
|
||||||
|
|| query.uppercase(Locale.US).contains("IN-CAT") || query.uppercase(Locale.US).contains("FILENAME")) {
|
||||||
|
parseTextData(query, "query text")
|
||||||
|
} else {
|
||||||
|
queryHost()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !isLoading,
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1C7ED6))
|
||||||
) {
|
) {
|
||||||
Text("Query")
|
Text(if (isLoading) "Querying..." else "Query Host")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,26 +755,36 @@ fun HostDirectoryDialog(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = statusMessage.ifBlank { "Enter query or parse screen." },
|
text = if (statusMessage.isNotBlank()) statusMessage else "${currentEntries.size} entries shown (${allEntries.size} total).",
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
color = Color.Gray
|
color = Color.Gray,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
TextButton(onClick = { parseScreen() }) {
|
Row {
|
||||||
Text("Parse Screen", fontSize = 11.sp, color = Color(0xFF51CF66))
|
TextButton(onClick = { parseClipboard() }, enabled = !isLoading) {
|
||||||
|
Text("Paste Clipboard", fontSize = 11.sp, color = Color(0xFF339AF0))
|
||||||
|
}
|
||||||
|
TextButton(onClick = { parseScreen() }, enabled = !isLoading) {
|
||||||
|
Text("Parse Screen", fontSize = 11.sp, color = Color(0xFF51CF66))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
// Table View
|
// Table View
|
||||||
if (entries.isEmpty()) {
|
if (currentEntries.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text("No entries found.", color = Color.Gray, fontSize = 12.sp)
|
Text(
|
||||||
|
if (isLoading) "Loading entries from host..." else "No entries found.",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val horizScroll = rememberScrollState()
|
val horizScroll = rememberScrollState()
|
||||||
@@ -227,7 +811,7 @@ fun HostDirectoryDialog(
|
|||||||
Text("Records", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
Text("Records", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
||||||
Text("Date / Time", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(140.dp))
|
Text("Date / Time", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(140.dp))
|
||||||
} else {
|
} else {
|
||||||
Text("Dataset Name", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(220.dp))
|
Text("Dataset Name", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(240.dp))
|
||||||
Text("Volume", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(70.dp))
|
Text("Volume", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(70.dp))
|
||||||
Text("Recfm", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(44.dp), textAlign = TextAlign.Center)
|
Text("Recfm", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(44.dp), textAlign = TextAlign.Center)
|
||||||
Text("Lrecl", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
Text("Lrecl", color = Color.LightGray, fontSize = 11.sp, fontWeight = FontWeight.Bold, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
@@ -242,7 +826,7 @@ fun HostDirectoryDialog(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(entries) { idx, item ->
|
itemsIndexed(currentEntries) { idx, item ->
|
||||||
val isSelected = (idx == selectedIndex)
|
val isSelected = (idx == selectedIndex)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
@@ -257,21 +841,21 @@ fun HostDirectoryDialog(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (item is CMSDirectoryEntry) {
|
if (item is CMSDirectoryEntry) {
|
||||||
Text(item.filename, color = Color.White, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
|
Text(item.filename ?: "", color = Color.White, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
|
||||||
Text(item.filetype, color = Color.Cyan, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(80.dp))
|
Text(item.filetype ?: "", color = Color.Cyan, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(80.dp))
|
||||||
Text(item.filemode, color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
Text(item.filemode ?: "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
Text(item.recfm, color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
Text(item.recfm ?: "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
Text("${item.lrecl}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
Text(if (item.lrecl > 0) "${item.lrecl}" else "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
Text("${item.numRecords}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
Text(if (item.numRecords > 0) "${item.numRecords}" else "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
||||||
Text("${item.date} ${item.time}", color = Color.Gray, fontSize = 10.sp, modifier = Modifier.width(140.dp))
|
Text("${item.date ?: ""} ${item.time ?: ""}".trim(), color = Color.Gray, fontSize = 10.sp, modifier = Modifier.width(140.dp))
|
||||||
} else if (item is TSODirectoryEntry) {
|
} else if (item is TSODirectoryEntry) {
|
||||||
Text(item.datasetName, color = Color.White, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(220.dp))
|
Text(item.datasetName ?: "", color = Color.White, fontSize = 11.sp, fontFamily = FontFamily.Monospace, modifier = Modifier.width(240.dp))
|
||||||
Text(item.volume, color = Color.Cyan, fontSize = 11.sp, modifier = Modifier.width(70.dp))
|
Text(item.volume ?: "", color = Color.Cyan, fontSize = 11.sp, modifier = Modifier.width(70.dp))
|
||||||
Text(item.recfm, color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(44.dp), textAlign = TextAlign.Center)
|
Text(item.recfm ?: "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(44.dp), textAlign = TextAlign.Center)
|
||||||
Text("${item.lrecl}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
Text(if (item.lrecl > 0) "${item.lrecl}" else "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
Text("${item.blksize}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
Text(if (item.blksize > 0) "${item.blksize}" else "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(60.dp), textAlign = TextAlign.Center)
|
||||||
Text("${item.tracksAllocated}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
Text(if (item.tracksAllocated > 0) "${item.tracksAllocated}" else "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(50.dp), textAlign = TextAlign.Center)
|
||||||
Text("${item.extents}", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
Text(if (item.extents > 0) "${item.extents}" else "", color = Color.LightGray, fontSize = 11.sp, modifier = Modifier.width(36.dp), textAlign = TextAlign.Center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,25 +867,41 @@ fun HostDirectoryDialog(
|
|||||||
// Bottom Action Buttons
|
// Bottom Action Buttons
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onDismiss) {
|
Row {
|
||||||
Text("Cancel", color = Color.LightGray)
|
if (hostType == FTConfig.HostType.TSO) {
|
||||||
}
|
OutlinedButton(
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
onClick = { inspectSelectedDataset() },
|
||||||
Button(
|
enabled = selectedIndex in currentEntries.indices && !isLoading,
|
||||||
onClick = {
|
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)
|
||||||
if (selectedIndex in entries.indices) {
|
) {
|
||||||
val selectedName = entries[selectedIndex].name
|
Text("Inspect (LISTDS)", fontSize = 11.sp)
|
||||||
onSelectDataset(selectedName)
|
|
||||||
onDismiss()
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
enabled = selectedIndex in entries.indices,
|
}
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
|
||||||
) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Select Dataset")
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text("Cancel", color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (selectedIndex in currentEntries.indices) {
|
||||||
|
val entry = currentEntries[selectedIndex]
|
||||||
|
val selectedName = entry.name
|
||||||
|
onSelectEntry?.invoke(selectedName, entry)
|
||||||
|
onSelectDataset(selectedName)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = selectedIndex in currentEntries.indices,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
|
) {
|
||||||
|
Text("Select Dataset")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import java.util.Collections
|
||||||
|
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 touch/mouse taps and generates keystroke actions.
|
||||||
|
*/
|
||||||
|
class HotspotEngine {
|
||||||
|
|
||||||
|
enum class HotspotType {
|
||||||
|
URL,
|
||||||
|
PF_KEY,
|
||||||
|
ENTER,
|
||||||
|
MENU_CHOICE
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Hotspot(
|
||||||
|
val type: HotspotType,
|
||||||
|
val startRow: Int,
|
||||||
|
val startCol: Int,
|
||||||
|
val endRow: Int,
|
||||||
|
val endCol: Int,
|
||||||
|
val displayText: String,
|
||||||
|
val action: String,
|
||||||
|
val tooltip: String
|
||||||
|
) {
|
||||||
|
val text: String get() = displayText
|
||||||
|
/**
|
||||||
|
* Test whether the given row/col coordinate falls within this hotspot.
|
||||||
|
*/
|
||||||
|
fun contains(row: Int, col: Int): Boolean {
|
||||||
|
if (row < startRow || row > endRow) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (startRow == endRow) {
|
||||||
|
return col in startCol..endCol
|
||||||
|
}
|
||||||
|
if (row == startRow) {
|
||||||
|
return col >= startCol
|
||||||
|
}
|
||||||
|
if (row == endRow) {
|
||||||
|
return col <= endCol
|
||||||
|
}
|
||||||
|
return true // Intermediate rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var urlEnabled: Boolean = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
var pfKeysEnabled: Boolean = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
var menuChoicesEnabled: Boolean = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val URL_PATTERN = Pattern.compile(
|
||||||
|
"(?:https?|ftp|file)://[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|www\\.[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|mailto:[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+",
|
||||||
|
Pattern.CASE_INSENSITIVE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val 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
|
||||||
|
)
|
||||||
|
|
||||||
|
private val MENU_NUMBER_PATTERN = Pattern.compile(
|
||||||
|
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
|
||||||
|
)
|
||||||
|
|
||||||
|
private val MENU_PROMPT_PATTERN = Pattern.compile(
|
||||||
|
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun markOccupied(arr: BooleanArray, start: Int, end: Int) {
|
||||||
|
val s = start.coerceAtLeast(0)
|
||||||
|
val e = end.coerceAtMost(arr.size)
|
||||||
|
for (i in s until e) {
|
||||||
|
arr[i] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isRangeOccupied(arr: BooleanArray, start: Int, end: Int): Boolean {
|
||||||
|
val s = start.coerceAtLeast(0)
|
||||||
|
val e = end.coerceAtMost(arr.size)
|
||||||
|
for (i in s until e) {
|
||||||
|
if (arr[i]) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cachedScreenText: String = ""
|
||||||
|
private var cachedRows: Int = 0
|
||||||
|
private var cachedCols: Int = 0
|
||||||
|
private var cachedHotspots: List<Hotspot> = emptyList()
|
||||||
|
|
||||||
|
fun invalidateCache() {
|
||||||
|
synchronized(this) {
|
||||||
|
cachedScreenText = ""
|
||||||
|
cachedHotspots = emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan the given ScreenBuffer for interactive hotspots.
|
||||||
|
*/
|
||||||
|
fun scan(sb: ScreenBuffer?): List<Hotspot> {
|
||||||
|
if (sb == null) return emptyList()
|
||||||
|
val rows = sb.displayRows
|
||||||
|
val cols = sb.displayCols
|
||||||
|
|
||||||
|
val fullText = StringBuilder(rows * (cols + 1))
|
||||||
|
for (r in 0 until rows) {
|
||||||
|
for (c in 0 until cols) {
|
||||||
|
val baddr = r * cols + c
|
||||||
|
val ea = sb.getDisplayCell(baddr)
|
||||||
|
val ch = ea?.ucs4 ?: ' '
|
||||||
|
if (ea == null || ea.isFieldAttribute || ch <= ' ' || ch == '\u00FF') {
|
||||||
|
fullText.append(' ')
|
||||||
|
} else {
|
||||||
|
fullText.append(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r < rows - 1) {
|
||||||
|
fullText.append('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanText(fullText.toString(), rows, cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan formatted screen text (lines separated by '\n') for hotspots.
|
||||||
|
*/
|
||||||
|
fun scanText(screenText: String?, rows: Int, cols: Int, enabled: Boolean = true): List<Hotspot> {
|
||||||
|
if (!enabled || screenText == null || rows <= 0 || cols <= 0) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(this) {
|
||||||
|
if (screenText == cachedScreenText && rows == cachedRows && cols == cachedCols) {
|
||||||
|
return cachedHotspots
|
||||||
|
}
|
||||||
|
|
||||||
|
val hotspots = mutableListOf<Hotspot>()
|
||||||
|
val lines = screenText.split("\n")
|
||||||
|
val actualRows = minOf(rows, lines.size)
|
||||||
|
|
||||||
|
for (r in 0 until actualRows) {
|
||||||
|
val line = lines[r]
|
||||||
|
val occupied = BooleanArray(line.length + 1)
|
||||||
|
|
||||||
|
// 1. Scan URLs
|
||||||
|
if (urlEnabled) {
|
||||||
|
val mUrl = URL_PATTERN.matcher(line)
|
||||||
|
while (mUrl.find()) {
|
||||||
|
val startCol = mUrl.start()
|
||||||
|
var endCol = mUrl.end()
|
||||||
|
var match = mUrl.group()
|
||||||
|
|
||||||
|
while (match.length > 1) {
|
||||||
|
val last = match[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
|
||||||
|
|
||||||
|
var actionUrl = match
|
||||||
|
if (match.startsWith("www.", ignoreCase = true)) {
|
||||||
|
actionUrl = "http://$match"
|
||||||
|
}
|
||||||
|
|
||||||
|
val h = Hotspot(
|
||||||
|
type = HotspotType.URL,
|
||||||
|
startRow = r,
|
||||||
|
startCol = startCol,
|
||||||
|
endRow = r,
|
||||||
|
endCol = endCol - 1,
|
||||||
|
displayText = match,
|
||||||
|
action = actionUrl,
|
||||||
|
tooltip = "Open URL: $actionUrl"
|
||||||
|
)
|
||||||
|
hotspots.add(h)
|
||||||
|
markOccupied(occupied, startCol, endCol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Scan PF / Function keys
|
||||||
|
if (pfKeysEnabled) {
|
||||||
|
val mPf = PF_KEY_PATTERN.matcher(line)
|
||||||
|
while (mPf.find()) {
|
||||||
|
val startCol = mPf.start(1)
|
||||||
|
val endCol = mPf.end(1)
|
||||||
|
if (isRangeOccupied(occupied, startCol, endCol)) continue
|
||||||
|
|
||||||
|
val matchStr = mPf.group(1) ?: continue
|
||||||
|
val keyText = matchStr.uppercase()
|
||||||
|
val action: String
|
||||||
|
val tooltip: String
|
||||||
|
|
||||||
|
if (keyText == "ENTER" || keyText == "[ENTER]") {
|
||||||
|
action = "[enter]"
|
||||||
|
tooltip = "Send [ENTER]"
|
||||||
|
} else if (keyText.startsWith("PF")) {
|
||||||
|
val numStr = keyText.substring(2)
|
||||||
|
action = "[pf$numStr]"
|
||||||
|
tooltip = "Send ${action.uppercase()}"
|
||||||
|
} else if (keyText.startsWith("F")) {
|
||||||
|
val numStr = keyText.substring(1)
|
||||||
|
action = "[pf$numStr]"
|
||||||
|
tooltip = "Send ${action.uppercase()}"
|
||||||
|
} else if (keyText.startsWith("PA")) {
|
||||||
|
val numStr = keyText.substring(2)
|
||||||
|
action = "[pa$numStr]"
|
||||||
|
tooltip = "Send ${action.uppercase()}"
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val h = Hotspot(
|
||||||
|
type = if (keyText.contains("ENTER")) HotspotType.ENTER else HotspotType.PF_KEY,
|
||||||
|
startRow = r,
|
||||||
|
startCol = startCol,
|
||||||
|
endRow = r,
|
||||||
|
endCol = endCol - 1,
|
||||||
|
displayText = matchStr,
|
||||||
|
action = action,
|
||||||
|
tooltip = tooltip
|
||||||
|
)
|
||||||
|
hotspots.add(h)
|
||||||
|
markOccupied(occupied, startCol, endCol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Scan Menu choices ("1.", "2.", "===> 3", "=> 4")
|
||||||
|
if (menuChoicesEnabled) {
|
||||||
|
val mNum = MENU_NUMBER_PATTERN.matcher(line)
|
||||||
|
while (mNum.find()) {
|
||||||
|
val startCol = mNum.start(1)
|
||||||
|
val endCol = mNum.end()
|
||||||
|
if (isRangeOccupied(occupied, startCol, endCol)) continue
|
||||||
|
|
||||||
|
val choiceNum = mNum.group(1)
|
||||||
|
val action = "$choiceNum[enter]"
|
||||||
|
val tooltip = "Select option $choiceNum"
|
||||||
|
|
||||||
|
val h = Hotspot(
|
||||||
|
type = HotspotType.MENU_CHOICE,
|
||||||
|
startRow = r,
|
||||||
|
startCol = startCol,
|
||||||
|
endRow = r,
|
||||||
|
endCol = endCol - 1,
|
||||||
|
displayText = "$choiceNum.",
|
||||||
|
action = action,
|
||||||
|
tooltip = tooltip
|
||||||
|
)
|
||||||
|
hotspots.add(h)
|
||||||
|
markOccupied(occupied, startCol, endCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mPrompt = MENU_PROMPT_PATTERN.matcher(line)
|
||||||
|
while (mPrompt.find()) {
|
||||||
|
val startCol = mPrompt.start()
|
||||||
|
val endCol = mPrompt.end()
|
||||||
|
if (isRangeOccupied(occupied, startCol, endCol)) continue
|
||||||
|
|
||||||
|
val choiceNum = mPrompt.group(1)
|
||||||
|
val action = "$choiceNum[enter]"
|
||||||
|
val tooltip = "Execute choice $choiceNum"
|
||||||
|
|
||||||
|
val h = Hotspot(
|
||||||
|
type = HotspotType.MENU_CHOICE,
|
||||||
|
startRow = r,
|
||||||
|
startCol = startCol,
|
||||||
|
endRow = r,
|
||||||
|
endCol = endCol - 1,
|
||||||
|
displayText = mPrompt.group(),
|
||||||
|
action = action,
|
||||||
|
tooltip = 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.
|
||||||
|
*/
|
||||||
|
fun findHotspot(row: Int, col: Int): Hotspot? {
|
||||||
|
synchronized(this) {
|
||||||
|
for (h in cachedHotspots) {
|
||||||
|
if (h.contains(row, col)) {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHotspots(): List<Hotspot> {
|
||||||
|
synchronized(this) {
|
||||||
|
return cachedHotspots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findHotspotAt(hotspots: List<Hotspot>, row: Int, col: Int): Hotspot? {
|
||||||
|
for (h in hotspots) {
|
||||||
|
if (h.contains(row, col)) {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import haus.nightmare.lib3270j.Telnet3270Client
|
||||||
|
import haus.nightmare.lib3270j.macro.Macro
|
||||||
|
import haus.nightmare.lib3270j.macro.MacroScreen
|
||||||
|
import haus.nightmare.lib3270j.macro.action.MacroAction
|
||||||
|
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
private const val DEFAULT_MACRO_XML = """<HASCRIPT name="SampleLogon" timeout="15000" usevars="true">
|
||||||
|
<vars>
|
||||||
|
<create name="${'$'}userId" type="string" value="USER01" />
|
||||||
|
<create name="${'$'}count" type="integer" value="0" />
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<screen name="LogonScreen" entryscreen="true">
|
||||||
|
<description>
|
||||||
|
<oia status="NOTINHIBITED" />
|
||||||
|
</description>
|
||||||
|
<actions>
|
||||||
|
<input value="${'$'}userId + '[enter]'" movecursor="true" xlatehostkeys="true" />
|
||||||
|
</actions>
|
||||||
|
<nextscreens timeout="10000">
|
||||||
|
<nextscreen name="ReadyScreen" />
|
||||||
|
</nextscreens>
|
||||||
|
</screen>
|
||||||
|
|
||||||
|
<screen name="ReadyScreen" exitscreen="true">
|
||||||
|
<description>
|
||||||
|
<oia status="NOTINHIBITED" />
|
||||||
|
</description>
|
||||||
|
<actions>
|
||||||
|
<message title="Success" value="Logon macro completed successfully!" />
|
||||||
|
</actions>
|
||||||
|
</screen>
|
||||||
|
</HASCRIPT>"""
|
||||||
|
|
||||||
|
data class MacroPromptState(
|
||||||
|
val prompt: MacroActionPrompt,
|
||||||
|
val deferred: CompletableDeferred<String?>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MacroDialog(
|
||||||
|
client: Telnet3270Client?,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
var macroXml by remember { mutableStateOf(DEFAULT_MACRO_XML) }
|
||||||
|
var executionLog by remember { mutableStateOf("") }
|
||||||
|
var statusText by remember { mutableStateOf("Status: Stopped") }
|
||||||
|
var isPlaying by remember { mutableStateOf(false) }
|
||||||
|
var isPaused by remember { mutableStateOf(false) }
|
||||||
|
var activePrompt by remember { mutableStateOf<MacroPromptState?>(null) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val macroRef = remember { AtomicReference<Macro?>(null) }
|
||||||
|
|
||||||
|
fun appendLog(msg: String) {
|
||||||
|
val line = "[${java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.US).format(java.util.Date())}] $msg\n"
|
||||||
|
executionLog += line
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(client) {
|
||||||
|
val macro = if (client != null) Macro(client) else null
|
||||||
|
macroRef.set(macro)
|
||||||
|
|
||||||
|
if (macro != null) {
|
||||||
|
val listener = object : Macro.MacroListener {
|
||||||
|
override fun macroStateChanged(m: Macro, oldState: Int, newState: Int) {
|
||||||
|
when (newState) {
|
||||||
|
Macro.STATE_PLAYING -> {
|
||||||
|
isPlaying = true
|
||||||
|
isPaused = false
|
||||||
|
statusText = "Status: Playing"
|
||||||
|
appendLog("Macro playing...")
|
||||||
|
}
|
||||||
|
Macro.STATE_PAUSED -> {
|
||||||
|
isPaused = true
|
||||||
|
statusText = "Status: Paused"
|
||||||
|
appendLog("Macro paused.")
|
||||||
|
}
|
||||||
|
Macro.STATE_ERROR -> {
|
||||||
|
isPlaying = false
|
||||||
|
isPaused = false
|
||||||
|
statusText = "Status: Error"
|
||||||
|
appendLog("Macro error.")
|
||||||
|
}
|
||||||
|
Macro.STATE_STOPPED -> {
|
||||||
|
isPlaying = false
|
||||||
|
isPaused = false
|
||||||
|
statusText = "Status: Stopped"
|
||||||
|
appendLog("Macro stopped.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun macroScreenMatching(screen: MacroScreen) {
|
||||||
|
appendLog("Matched screen: [${screen.name}]")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun macroActionExecuting(action: MacroAction) {
|
||||||
|
appendLog(" -> Executing: ${action.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun macroMessage(title: String, message: String) {
|
||||||
|
appendLog("MESSAGE [$title]: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun macroError(error: String) {
|
||||||
|
isPlaying = false
|
||||||
|
isPaused = false
|
||||||
|
statusText = "Status: Error - $error"
|
||||||
|
appendLog("ERROR: $error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro.addMacroListener(listener)
|
||||||
|
macro.setPromptHandler { prompt ->
|
||||||
|
val deferred = CompletableDeferred<String?>()
|
||||||
|
activePrompt = MacroPromptState(prompt, deferred)
|
||||||
|
kotlinx.coroutines.runBlocking {
|
||||||
|
deferred.await() ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
macro?.stop()
|
||||||
|
macroRef.set(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
if (!isPlaying) onDismiss()
|
||||||
|
},
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.fillMaxHeight(0.90f)
|
||||||
|
.padding(vertical = 10.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(14.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"XML Macro Runtime (<HASCRIPT>)",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
statusText,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = if (isPlaying) Color(0xFF51CF66) else Color.LightGray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
|
// Macro XML Editor Pane
|
||||||
|
Text("Macro Definition XML:", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = macroXml,
|
||||||
|
onValueChange = { macroXml = it },
|
||||||
|
textStyle = LocalTextStyle.current.copy(
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.White
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(0.55f),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = Color(0xFF339AF0),
|
||||||
|
unfocusedBorderColor = Color(0xFF495057),
|
||||||
|
focusedContainerColor = Color(0xFF121212),
|
||||||
|
unfocusedContainerColor = Color(0xFF121212)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execution Log Pane
|
||||||
|
Text("Execution Log:", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(0.45f)
|
||||||
|
.background(Color(0xFF0F0F0F), RoundedCornerShape(6.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
val logScroll = rememberScrollState()
|
||||||
|
Text(
|
||||||
|
text = executionLog.ifBlank { "Ready to execute macro..." },
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = if (executionLog.isBlank()) Color.Gray else Color(0xFF69DB7C),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(logScroll)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control Buttons: Play, Pause, Stop, Close
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val macro = macroRef.get()
|
||||||
|
if (macro != null) {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val screens = haus.nightmare.lib3270j.macro.MacroXmlParser.parse(macroXml)
|
||||||
|
macro.setScreens(screens)
|
||||||
|
macro.setClient(client)
|
||||||
|
macro.play()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
appendLog("Execution exception: ${ex.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !isPlaying && client != null && client.connectionState?.isFullSession == true,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E)),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("▶ Play", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val macro = macroRef.get()
|
||||||
|
if (macro != null) {
|
||||||
|
if (macro.state == Macro.STATE_PLAYING) {
|
||||||
|
macro.pause()
|
||||||
|
} else if (macro.state == Macro.STATE_PAUSED) {
|
||||||
|
macro.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = isPlaying,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE67700)),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(if (isPaused) "Resume" else "Pause", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val macro = macroRef.get()
|
||||||
|
macro?.stop()
|
||||||
|
},
|
||||||
|
enabled = isPlaying,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFC92A2A)),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("■ Stop", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val macro = macroRef.get()
|
||||||
|
macro?.stop()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF495057)),
|
||||||
|
modifier = Modifier.width(75.dp)
|
||||||
|
) {
|
||||||
|
Text("Close", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal prompt dialog when macro requests <prompt> parameter input
|
||||||
|
val promptState = activePrompt
|
||||||
|
if (promptState != null) {
|
||||||
|
var inputValue by remember { mutableStateOf(promptState.prompt.defaultValue ?: "") }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
promptState.deferred.complete(null)
|
||||||
|
activePrompt = null
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = promptState.prompt.title.ifBlank { "Macro Input Prompt" },
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
Text(
|
||||||
|
text = promptState.prompt.description.ifBlank { "Enter parameter:" },
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = inputValue,
|
||||||
|
onValueChange = { inputValue = it },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
promptState.deferred.complete(inputValue)
|
||||||
|
activePrompt = null
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("OK")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
promptState.deferred.complete(null)
|
||||||
|
activePrompt = null
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package haus.nightmare.a3270.ui
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -27,15 +28,30 @@ fun OiaStatusBar(
|
|||||||
isTls: Boolean = false,
|
isTls: Boolean = false,
|
||||||
isTlsVerified: Boolean = true,
|
isTlsVerified: Boolean = true,
|
||||||
graphicsMode: String = "NONE",
|
graphicsMode: String = "NONE",
|
||||||
codePage: String = "037",
|
|
||||||
isLightPenMode: Boolean = false,
|
isLightPenMode: Boolean = false,
|
||||||
isInsertMode: Boolean = false,
|
isInsertMode: Boolean = false,
|
||||||
|
isDocMode: Boolean = false,
|
||||||
|
isWordWrap: Boolean = false,
|
||||||
|
isAplMode: Boolean = false,
|
||||||
|
onToggleInsert: (() -> Unit)? = null,
|
||||||
|
onToggleDocMode: (() -> Unit)? = null,
|
||||||
|
onToggleWordWrap: (() -> Unit)? = null,
|
||||||
|
onToggleAplMode: (() -> Unit)? = null,
|
||||||
inhibitReason: Int = 0,
|
inhibitReason: Int = 0,
|
||||||
luName: String = "",
|
luName: String = "",
|
||||||
isNumericField: Boolean = false,
|
isNumericField: Boolean = false,
|
||||||
modelName: String = "",
|
fieldStatus: String = "",
|
||||||
|
uiTheme: String = "DARK",
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val isLight = uiTheme.equals("LIGHT", ignoreCase = true)
|
||||||
|
val barBg = if (isLight) Color(0xFFE9ECEF) else Color(0xFF161719)
|
||||||
|
val defaultTextClr = if (isLight) Color(0xFF212529) else Color.White
|
||||||
|
val sysAvailClr = if (isLight) Color(0xFF005AC8) else Color(0xFF7890F0)
|
||||||
|
val inputInhibitedClr = if (isLight) Color(0xFF141414) else Color(0xFFFFFFFF)
|
||||||
|
val attentionClr = if (isLight) Color(0xFFB46400) else Color(0xFFFFFF00)
|
||||||
|
val commCheckClr = if (isLight) Color(0xFFBE1414) else Color(0xFFFF0000)
|
||||||
|
|
||||||
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
|
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
|
||||||
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
||||||
val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr)
|
val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr)
|
||||||
@@ -66,24 +82,24 @@ fun OiaStatusBar(
|
|||||||
else -> "X LOCKED"
|
else -> "X LOCKED"
|
||||||
}
|
}
|
||||||
val clr = when (inhibitReason) {
|
val clr = when (inhibitReason) {
|
||||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> Color(0xFFFF0000)
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> commCheckClr
|
||||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> Color(0xFFFFFF00)
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> attentionClr
|
||||||
else -> Color(0xFFFFFFFF)
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NUMERIC_ONLY -> attentionClr
|
||||||
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OVERFLOW -> attentionClr
|
||||||
|
else -> inputInhibitedClr
|
||||||
}
|
}
|
||||||
txt to clr
|
txt to clr
|
||||||
} else if (isKeyboardLocked) {
|
} else if (isKeyboardLocked) {
|
||||||
"X SYSTEM" to Color(0xFFFFFFFF)
|
"X SYSTEM" to inputInhibitedClr
|
||||||
} else if (isInsertMode) {
|
|
||||||
"INSERT" to Color(0xFF7890F0)
|
|
||||||
} else {
|
} else {
|
||||||
"READY" to Color(0xFF7890F0)
|
"READY" to sysAvailClr
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(26.dp)
|
.height(26.dp)
|
||||||
.background(Color(0xFF161719))
|
.background(barBg)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
@@ -101,7 +117,7 @@ fun OiaStatusBar(
|
|||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = hostText,
|
text = hostText,
|
||||||
color = Color.White,
|
color = defaultTextClr,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -148,52 +164,22 @@ fun OiaStatusBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodePage Badge
|
// Field Status: DBCS vs NUM vs ALPHA
|
||||||
if (connectionState.isConnected() && codePage.isNotBlank()) {
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Surface(
|
|
||||||
color = Color(0xFFFA5252).copy(alpha = 0.2f),
|
|
||||||
shape = RoundedCornerShape(4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "CP$codePage",
|
|
||||||
color = Color(0xFFFF8787),
|
|
||||||
fontSize = 9.sp,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field Status: NUM vs ALPHA
|
|
||||||
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
val effectiveFieldStatus = if (fieldStatus.isNotBlank()) fieldStatus else (if (isNumericField) "NUM" else "ALPHA")
|
||||||
Surface(
|
val badgeColor = when (effectiveFieldStatus) {
|
||||||
color = Color(0xFF339AF0).copy(alpha = 0.2f),
|
"DBCS" -> sysAvailClr
|
||||||
shape = RoundedCornerShape(4.dp)
|
"NUM" -> Color(0xFFFFD43B)
|
||||||
) {
|
else -> Color(0xFF74C0FC)
|
||||||
Text(
|
|
||||||
text = if (isNumericField) "NUM" else "ALPHA",
|
|
||||||
color = if (isNumericField) Color(0xFFFFD43B) else Color(0xFF74C0FC),
|
|
||||||
fontSize = 9.sp,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Model / Dimensions Badge
|
|
||||||
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Surface(
|
Surface(
|
||||||
color = Color(0xFF495057).copy(alpha = 0.3f),
|
color = badgeColor.copy(alpha = 0.2f),
|
||||||
shape = RoundedCornerShape(4.dp)
|
shape = RoundedCornerShape(4.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (modelName.isNotBlank()) modelName else "${rows}x${cols}",
|
text = effectiveFieldStatus,
|
||||||
color = Color(0xFFCED4DA),
|
color = badgeColor,
|
||||||
fontSize = 9.sp,
|
fontSize = 9.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -257,6 +243,78 @@ fun OiaStatusBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Operational Mode Badges (^ INS, APL, DOC, V)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 2.dp)
|
||||||
|
) {
|
||||||
|
if (connectionState.isConnected() && isInsertMode) {
|
||||||
|
Surface(
|
||||||
|
color = Color(0xFF7890F0).copy(alpha = 0.2f),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
modifier = Modifier.then(if (onToggleInsert != null) Modifier.clickable { onToggleInsert() } else Modifier)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "^ INS",
|
||||||
|
color = Color(0xFF7890F0),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (connectionState.isConnected() && isAplMode) {
|
||||||
|
Surface(
|
||||||
|
color = Color(0xFFFF922B).copy(alpha = 0.2f),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
modifier = Modifier.then(if (onToggleAplMode != null) Modifier.clickable { onToggleAplMode() } else Modifier)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "APL",
|
||||||
|
color = Color(0xFFFFA94D),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (connectionState.isConnected() && isDocMode) {
|
||||||
|
Surface(
|
||||||
|
color = Color(0xFF20C997).copy(alpha = 0.2f),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
modifier = Modifier.then(if (onToggleDocMode != null) Modifier.clickable { onToggleDocMode() } else Modifier)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "DOC",
|
||||||
|
color = Color(0xFF38D9A9),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (connectionState.isConnected() && isWordWrap) {
|
||||||
|
Surface(
|
||||||
|
color = Color(0xFF20C997).copy(alpha = 0.2f),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
modifier = Modifier.then(if (onToggleWordWrap != null) Modifier.clickable { onToggleWordWrap() } else Modifier)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "V",
|
||||||
|
color = Color(0xFF38D9A9),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Terminal Keyboard Lock Status Badge (X SYSTEM vs READY)
|
// Terminal Keyboard Lock Status Badge (X SYSTEM vs READY)
|
||||||
Surface(
|
Surface(
|
||||||
color = lockStatusColor.copy(alpha = 0.2f),
|
color = lockStatusColor.copy(alpha = 0.2f),
|
||||||
@@ -276,7 +334,7 @@ fun OiaStatusBar(
|
|||||||
// Cursor Position & Buffer Address
|
// Cursor Position & Buffer Address
|
||||||
Text(
|
Text(
|
||||||
text = posStr,
|
text = posStr,
|
||||||
color = Color(0xFF22B8CF),
|
color = if (isLight) Color(0xFF0C8599) else Color(0xFF22B8CF),
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.core.content.FileProvider
|
|||||||
import haus.nightmare.lib3270j.graphics.GraphicsPlane
|
import haus.nightmare.lib3270j.graphics.GraphicsPlane
|
||||||
import haus.nightmare.lib3270j.graphics.ProgramSymbolManager
|
import haus.nightmare.lib3270j.graphics.ProgramSymbolManager
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -296,6 +297,10 @@ private fun generatePlainText(screenBuffer: ScreenBuffer?, rows: Int, cols: Int,
|
|||||||
val faVal = cell.fa.toInt() and 0xFF
|
val faVal = cell.fa.toInt() and 0xFF
|
||||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
rowSb.append(' ')
|
rowSb.append(' ')
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_RIGHT) {
|
||||||
|
if (c > 0) continue else rowSb.append(' ')
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_SO || cell.db == ExtendedAttribute.DB_SI) {
|
||||||
|
rowSb.append(' ')
|
||||||
} else {
|
} else {
|
||||||
if (currentHidden && maskHidden) {
|
if (currentHidden && maskHidden) {
|
||||||
rowSb.append(' ')
|
rowSb.append(' ')
|
||||||
@@ -346,6 +351,10 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
|
|||||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||||
currentFieldEa = cell
|
currentFieldEa = cell
|
||||||
sb.append(' ')
|
sb.append(' ')
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_RIGHT) {
|
||||||
|
if (c > 0) continue else sb.append(' ')
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_SO || cell.db == ExtendedAttribute.DB_SI) {
|
||||||
|
sb.append(' ')
|
||||||
} else {
|
} else {
|
||||||
if (currentHidden && maskHidden) {
|
if (currentHidden && maskHidden) {
|
||||||
sb.append(' ')
|
sb.append(' ')
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenSnapshot
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScreenHistoryDialog(
|
||||||
|
historyBuffer: ScreenHistoryBuffer?,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) }
|
||||||
|
|
||||||
|
var historySize by remember { mutableIntStateOf(historyBuffer?.size() ?: 0) }
|
||||||
|
var selectedIndex by remember { mutableIntStateOf((historyBuffer?.size() ?: 1) - 1) }
|
||||||
|
|
||||||
|
// Listen for live updates if host writes new screens
|
||||||
|
DisposableEffect(historyBuffer) {
|
||||||
|
if (historyBuffer == null) return@DisposableEffect onDispose {}
|
||||||
|
|
||||||
|
val listener = ScreenHistoryBuffer.ScreenHistoryListener { buf, _ ->
|
||||||
|
val newSize = buf.size()
|
||||||
|
historySize = newSize
|
||||||
|
if (selectedIndex >= newSize - 2) {
|
||||||
|
selectedIndex = newSize - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
historyBuffer.addListener(listener)
|
||||||
|
historySize = historyBuffer.size()
|
||||||
|
if (historySize > 0 && selectedIndex < 0) {
|
||||||
|
selectedIndex = historySize - 1
|
||||||
|
}
|
||||||
|
onDispose {
|
||||||
|
historyBuffer.removeListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentSnapshot: ScreenSnapshot? = remember(historyBuffer, historySize, selectedIndex) {
|
||||||
|
if (historyBuffer != null && historySize > 0 && selectedIndex in 0 until historySize) {
|
||||||
|
try {
|
||||||
|
historyBuffer.get(selectedIndex)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.95f)
|
||||||
|
.fillMaxHeight(0.85f)
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
// Top Header: Title & Info
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Screen History Buffer",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
if (currentSnapshot != null) {
|
||||||
|
Text(
|
||||||
|
dateFormat.format(Date(currentSnapshot.timestamp)),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.LightGray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
|
||||||
|
// Scrub navigation slider & buttons
|
||||||
|
if (historySize > 0 && currentSnapshot != null) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { if (selectedIndex > 0) selectedIndex-- },
|
||||||
|
enabled = selectedIndex > 0,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF343A40)),
|
||||||
|
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp),
|
||||||
|
modifier = Modifier.height(36.dp)
|
||||||
|
) {
|
||||||
|
Text("◄ Prev", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Screen ${selectedIndex + 1} of $historySize",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
if (historySize > 1) {
|
||||||
|
Slider(
|
||||||
|
value = (selectedIndex + 1).toFloat(),
|
||||||
|
onValueChange = { selectedIndex = (it.toInt() - 1).coerceIn(0, historySize - 1) },
|
||||||
|
valueRange = 1f..historySize.toFloat(),
|
||||||
|
steps = if (historySize > 2) historySize - 2 else 0,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { if (selectedIndex < historySize - 1) selectedIndex++ },
|
||||||
|
enabled = selectedIndex < historySize - 1,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF343A40)),
|
||||||
|
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp),
|
||||||
|
modifier = Modifier.height(36.dp)
|
||||||
|
) {
|
||||||
|
Text("Next ►", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal Preview Pane
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.Black, RoundedCornerShape(8.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
if (currentSnapshot != null) {
|
||||||
|
val verticalScroll = rememberScrollState()
|
||||||
|
val horizontalScroll = rememberScrollState()
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = currentSnapshot.text,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color(0xFF00FF41), // Classic 3270 Green Phosphor
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(verticalScroll)
|
||||||
|
.horizontalScroll(horizontalScroll)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"No history frames captured yet.",
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (currentSnapshot != null) {
|
||||||
|
clipboardManager.setText(AnnotatedString(currentSnapshot.text))
|
||||||
|
Toast.makeText(context, "Full screen copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = currentSnapshot != null,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2C2D30)),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Copy Screen", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (currentSnapshot != null) {
|
||||||
|
clipboardManager.setText(AnnotatedString(currentSnapshot.formattedTable))
|
||||||
|
Toast.makeText(context, "Table text copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = currentSnapshot != null,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2C2D30)),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Copy Table", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onDismiss,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF495057)),
|
||||||
|
modifier = Modifier.width(80.dp)
|
||||||
|
) {
|
||||||
|
Text("Close", fontSize = 12.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,9 +30,29 @@ fun SettingsDialog(
|
|||||||
initialCrosshairRuler: Boolean = false,
|
initialCrosshairRuler: Boolean = false,
|
||||||
initialCursorStyle: String = "BLOCK",
|
initialCursorStyle: String = "BLOCK",
|
||||||
initialBlockSelectMode: Boolean = false,
|
initialBlockSelectMode: Boolean = false,
|
||||||
|
initialBlockPaste: Boolean = true,
|
||||||
|
initialTrimRectSizingHandles: Boolean = false,
|
||||||
|
initialTrimRectRemainAfterEdit: Boolean = false,
|
||||||
|
initialPasteWordBreak: Boolean = true,
|
||||||
|
initialHotspotsEnabled: Boolean = false,
|
||||||
|
initialHotspotsUrlEnabled: Boolean = true,
|
||||||
|
initialHotspotsPfEnabled: Boolean = true,
|
||||||
|
initialHotspotsMenuChoicesEnabled: Boolean = true,
|
||||||
initialStartupBehavior: String = "SHOW_CONNECT",
|
initialStartupBehavior: String = "SHOW_CONNECT",
|
||||||
initialDynamicRows: Int = 62,
|
initialDynamicRows: Int = 62,
|
||||||
initialDynamicCols: Int = 160,
|
initialDynamicCols: Int = 160,
|
||||||
|
initialEntryAssistDocMode: Boolean = false,
|
||||||
|
initialEntryAssistWordWrap: Boolean = false,
|
||||||
|
initialEntryAssistStartCol: Int = 1,
|
||||||
|
initialEntryAssistEndCol: Int = 80,
|
||||||
|
initialEntryAssistBell: Boolean = false,
|
||||||
|
initialEntryAssistBellCol: Int = 75,
|
||||||
|
initialEntryAssistTabStops: String = "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75",
|
||||||
|
initialInsertOffOnAid: Boolean = true,
|
||||||
|
initialFourColorOverride: Boolean = false,
|
||||||
|
initialNumericFieldLock: Boolean = true,
|
||||||
|
initialAutoSkipEnabled: Boolean = true,
|
||||||
|
initialDefaultHostType: String = "TSO",
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSave: (
|
onSave: (
|
||||||
maskHiddenInput: Boolean,
|
maskHiddenInput: Boolean,
|
||||||
@@ -45,9 +65,29 @@ fun SettingsDialog(
|
|||||||
crosshairRuler: Boolean,
|
crosshairRuler: Boolean,
|
||||||
cursorStyle: String,
|
cursorStyle: String,
|
||||||
blockSelectMode: Boolean,
|
blockSelectMode: Boolean,
|
||||||
|
blockPaste: Boolean,
|
||||||
|
trimRectSizingHandles: Boolean,
|
||||||
|
trimRectRemainAfterEdit: Boolean,
|
||||||
|
pasteWordBreak: Boolean,
|
||||||
|
hotspotsEnabled: Boolean,
|
||||||
|
hotspotsUrlEnabled: Boolean,
|
||||||
|
hotspotsPfEnabled: Boolean,
|
||||||
|
hotspotsMenuChoicesEnabled: Boolean,
|
||||||
startupBehavior: String,
|
startupBehavior: String,
|
||||||
dynamicRows: Int,
|
dynamicRows: Int,
|
||||||
dynamicCols: Int
|
dynamicCols: Int,
|
||||||
|
entryAssistDocMode: Boolean,
|
||||||
|
entryAssistWordWrap: Boolean,
|
||||||
|
entryAssistStartCol: Int,
|
||||||
|
entryAssistEndCol: Int,
|
||||||
|
entryAssistBell: Boolean,
|
||||||
|
entryAssistBellCol: Int,
|
||||||
|
entryAssistTabStops: String,
|
||||||
|
insertOffOnAid: Boolean,
|
||||||
|
fourColorOverride: Boolean,
|
||||||
|
numericFieldLock: Boolean,
|
||||||
|
autoSkipEnabled: Boolean,
|
||||||
|
defaultHostType: String
|
||||||
) -> Unit
|
) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
@@ -64,10 +104,35 @@ fun SettingsDialog(
|
|||||||
var crosshairRuler by remember { mutableStateOf(initialCrosshairRuler) }
|
var crosshairRuler by remember { mutableStateOf(initialCrosshairRuler) }
|
||||||
var cursorStyle by remember { mutableStateOf(initialCursorStyle) }
|
var cursorStyle by remember { mutableStateOf(initialCursorStyle) }
|
||||||
var blockSelectMode by remember { mutableStateOf(initialBlockSelectMode) }
|
var blockSelectMode by remember { mutableStateOf(initialBlockSelectMode) }
|
||||||
|
var blockPaste by remember { mutableStateOf(initialBlockPaste) }
|
||||||
|
var trimRectSizingHandles by remember { mutableStateOf(initialTrimRectSizingHandles) }
|
||||||
|
var trimRectRemainAfterEdit by remember { mutableStateOf(initialTrimRectRemainAfterEdit) }
|
||||||
|
var pasteWordBreak by remember { mutableStateOf(initialPasteWordBreak) }
|
||||||
|
var hotspotsEnabled by remember { mutableStateOf(initialHotspotsEnabled) }
|
||||||
|
var hotspotsUrlEnabled by remember { mutableStateOf(initialHotspotsUrlEnabled) }
|
||||||
|
var hotspotsPfEnabled by remember { mutableStateOf(initialHotspotsPfEnabled) }
|
||||||
|
var hotspotsMenuChoicesEnabled by remember { mutableStateOf(initialHotspotsMenuChoicesEnabled) }
|
||||||
var startupBehavior by remember { mutableStateOf(initialStartupBehavior) }
|
var startupBehavior by remember { mutableStateOf(initialStartupBehavior) }
|
||||||
|
val savedHosts = remember { haus.nightmare.a3270.storage.HostStorage.getSavedHosts(context) }
|
||||||
|
var selectedAutoHostId by remember {
|
||||||
|
mutableStateOf(haus.nightmare.a3270.storage.HostStorage.getAutoConnectHost(context)?.id ?: savedHosts.firstOrNull()?.id ?: "")
|
||||||
|
}
|
||||||
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
|
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
|
||||||
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
|
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
|
||||||
|
|
||||||
|
var entryAssistDocMode by remember { mutableStateOf(initialEntryAssistDocMode) }
|
||||||
|
var entryAssistWordWrap by remember { mutableStateOf(initialEntryAssistWordWrap) }
|
||||||
|
var entryAssistStartColStr by remember { mutableStateOf(initialEntryAssistStartCol.toString()) }
|
||||||
|
var entryAssistEndColStr by remember { mutableStateOf(initialEntryAssistEndCol.toString()) }
|
||||||
|
var entryAssistBell by remember { mutableStateOf(initialEntryAssistBell) }
|
||||||
|
var entryAssistBellColStr by remember { mutableStateOf(initialEntryAssistBellCol.toString()) }
|
||||||
|
var entryAssistTabStops by remember { mutableStateOf(initialEntryAssistTabStops) }
|
||||||
|
var insertOffOnAid by remember { mutableStateOf(initialInsertOffOnAid) }
|
||||||
|
var fourColorOverride by remember { mutableStateOf(initialFourColorOverride) }
|
||||||
|
var numericFieldLock by remember { mutableStateOf(initialNumericFieldLock) }
|
||||||
|
var autoSkipEnabled by remember { mutableStateOf(initialAutoSkipEnabled) }
|
||||||
|
var defaultHostType by remember { mutableStateOf(initialDefaultHostType) }
|
||||||
|
|
||||||
var showImportDialog by remember { mutableStateOf(false) }
|
var showImportDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
@@ -228,6 +293,166 @@ fun SettingsDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Block Paste Mode
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Block Paste Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Align multi-line clipboard pastes with cursor start column.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = blockPaste,
|
||||||
|
onCheckedChange = { blockPaste = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Trim Rectangle Sizing Handles
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Trim Rectangle Sizing Handles", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Show interactive drag handles around block selection.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = trimRectSizingHandles,
|
||||||
|
onCheckedChange = { trimRectSizingHandles = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Trim Rectangle Remains After Edit
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Keep Selection After Edit", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Trim rectangle remains after cut/copy/paste.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = trimRectRemainAfterEdit,
|
||||||
|
onCheckedChange = { trimRectRemainAfterEdit = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Paste Word-Break
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Paste Without Splitting Words", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Avoid splitting words across fields when pasting.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = pasteWordBreak,
|
||||||
|
onCheckedChange = { pasteWordBreak = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// SECTION: INTERACTIVE HOTSPOTS
|
||||||
|
Text("Interactive Hotspots", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Master Hotspots Switch (Default DISABLED)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Enable Interactive Hotspots", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Tappable URLs, function keys, and menu choices (disabled by default for touch screens).", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = hotspotsEnabled,
|
||||||
|
onCheckedChange = { hotspotsEnabled = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hotspotsEnabled) {
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Hotspots: URL Detection
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Auto-Detect URLs", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = Color.White)
|
||||||
|
Text("http://, https://, ftp://, www., mailto:", fontSize = 11.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = hotspotsUrlEnabled,
|
||||||
|
onCheckedChange = { hotspotsUrlEnabled = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Hotspots: Function Key Detection
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Auto-Detect Function Keys", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = Color.White)
|
||||||
|
Text("PF1..PF24, F1..F24, PA1..PA3, [ENTER]", fontSize = 11.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = hotspotsPfEnabled,
|
||||||
|
onCheckedChange = { hotspotsPfEnabled = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Hotspots: Menu Choice Detection
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Auto-Detect Menu Choices", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = Color.White)
|
||||||
|
Text("\"1.\", \"2.\", \"===> 3\", \"=> 4\"", fontSize = 11.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = hotspotsMenuChoicesEnabled,
|
||||||
|
onCheckedChange = { hotspotsMenuChoicesEnabled = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
// Haptic Feedback
|
// Haptic Feedback
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -270,6 +495,47 @@ fun SettingsDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startupBehavior.equals("AUTO_CONNECT", ignoreCase = true) && savedHosts.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text("Auto-Connect Target Profile", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
var hostDropdownExpanded by remember { mutableStateOf(false) }
|
||||||
|
val selectedHost = savedHosts.find { it.id == selectedAutoHostId } ?: savedHosts.first()
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { hostDropdownExpanded = true },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${selectedHost.name} (${selectedHost.host}:${selectedHost.port})",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = hostDropdownExpanded,
|
||||||
|
onDismissRequest = { hostDropdownExpanded = false }
|
||||||
|
) {
|
||||||
|
savedHosts.forEach { host ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = "${host.name} (${host.host}:${host.port})",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = if (host.id == selectedAutoHostId) FontWeight.Bold else FontWeight.Normal
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
selectedAutoHostId = host.id
|
||||||
|
hostDropdownExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -395,6 +661,29 @@ fun SettingsDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Default Host Type
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Default Host Type", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Target system environment for IND\$FILE transfers and directory queries.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
listOf("TSO", "CMS", "CICS").forEach { hostTypeKey ->
|
||||||
|
val isSelected = defaultHostType.equals(hostTypeKey, ignoreCase = true)
|
||||||
|
FilterChip(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = { defaultHostType = hostTypeKey },
|
||||||
|
label = { Text(hostTypeKey, fontSize = 10.sp) },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
// Verify TLS Certs
|
// Verify TLS Certs
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -416,7 +705,207 @@ fun SettingsDialog(
|
|||||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// SECTION 4: INI CONFIGURATION BACKUP
|
// SECTION 4: ENTRY ASSIST & MODES
|
||||||
|
Text("Entry Assist & Modes", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
|
Text("Document Mode word wrap, margins, audible bell, and operational locks matching j3270.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Document Mode
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Document Mode (DOC)", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Enable word wrapping and margin tracking during text entry.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = entryAssistDocMode,
|
||||||
|
onCheckedChange = { entryAssistDocMode = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Word Wrap
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Word Wrap (V)", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Automatically wrap partial words at the right margin.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = entryAssistWordWrap,
|
||||||
|
onCheckedChange = { entryAssistWordWrap = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Margins (Left / Right)
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Entry Assist Margins", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Left margin (1-80) and Right margin (1-80).", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entryAssistStartColStr,
|
||||||
|
onValueChange = { entryAssistStartColStr = it },
|
||||||
|
label = { Text("Left Col (1-80)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entryAssistEndColStr,
|
||||||
|
onValueChange = { entryAssistEndColStr = it },
|
||||||
|
label = { Text("Right Col (1-80)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Audible EOL Bell
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Audible EOL Bell Warning", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Beep warning signal when typing reaches the bell column.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = entryAssistBell,
|
||||||
|
onCheckedChange = { entryAssistBell = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entryAssistBellColStr,
|
||||||
|
onValueChange = { entryAssistBellColStr = it },
|
||||||
|
label = { Text("Bell Column (1-80)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Tab Stops
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entryAssistTabStops,
|
||||||
|
onValueChange = { entryAssistTabStops = it },
|
||||||
|
label = { Text("Word Tab Stops") },
|
||||||
|
placeholder = { Text("5,10,15,20,25,30,35,40,45,50,55,60,65,70,75") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Operational Mode Toggles
|
||||||
|
// 1. Reset Insert on AID
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Reset Insert Mode on AID Key", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Automatically cancel insert mode upon Enter, PF, PA, or Clear.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = insertOffOnAid,
|
||||||
|
onCheckedChange = { insertOffOnAid = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// 2. Base 4-Color Override
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Base 4-Color Override Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Force 3279 4-color mapping (green/white/red/turquoise), ignoring host extended colors.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = fourColorOverride,
|
||||||
|
onCheckedChange = { fourColorOverride = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// 3. Numeric Field Lock
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Numeric Field Lock (-NUMERIC)", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Inhibit keyboard if a non-numeric character is typed in a numeric-only field.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = numericFieldLock,
|
||||||
|
onCheckedChange = { numericFieldLock = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// 4. Auto-Skip
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Auto-Skip Across Fields", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Automatically advance cursor to next unprotected field when field is filled.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = autoSkipEnabled,
|
||||||
|
onCheckedChange = { autoSkipEnabled = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// SECTION 5: INI CONFIGURATION BACKUP
|
||||||
Text("Configuration File (INI)", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
Text("Configuration File (INI)", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
Text("Compatible with j3270 desktop INI configuration files.", fontSize = 12.sp, color = Color.LightGray)
|
Text("Compatible with j3270 desktop INI configuration files.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -424,6 +913,15 @@ fun SettingsDialog(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
haus.nightmare.a3270.storage.AppSettings.shareSettingsIni(context)
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1971C2)),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Share INI", fontSize = 12.sp)
|
||||||
|
}
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val sw = java.io.StringWriter()
|
val sw = java.io.StringWriter()
|
||||||
@@ -434,13 +932,13 @@ fun SettingsDialog(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("Export to INI", fontSize = 12.sp)
|
Text("Copy", fontSize = 12.sp)
|
||||||
}
|
}
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { showImportDialog = true },
|
onClick = { showImportDialog = true },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("Import from INI", fontSize = 12.sp)
|
Text("Import", fontSize = 12.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,6 +957,9 @@ fun SettingsDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
val dRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
|
val dRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
|
||||||
val dCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
|
val dCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
|
||||||
|
val eaStartCol = entryAssistStartColStr.trim().toIntOrNull()?.coerceIn(1, 80) ?: 1
|
||||||
|
val eaEndCol = entryAssistEndColStr.trim().toIntOrNull()?.coerceIn(1, 80) ?: 80
|
||||||
|
val eaBellCol = entryAssistBellColStr.trim().toIntOrNull()?.coerceIn(1, 80) ?: 75
|
||||||
onSave(
|
onSave(
|
||||||
maskHiddenInput,
|
maskHiddenInput,
|
||||||
cursorBlink,
|
cursorBlink,
|
||||||
@@ -470,10 +971,42 @@ fun SettingsDialog(
|
|||||||
crosshairRuler,
|
crosshairRuler,
|
||||||
cursorStyle,
|
cursorStyle,
|
||||||
blockSelectMode,
|
blockSelectMode,
|
||||||
|
blockPaste,
|
||||||
|
trimRectSizingHandles,
|
||||||
|
trimRectRemainAfterEdit,
|
||||||
|
pasteWordBreak,
|
||||||
|
hotspotsEnabled,
|
||||||
|
hotspotsUrlEnabled,
|
||||||
|
hotspotsPfEnabled,
|
||||||
|
hotspotsMenuChoicesEnabled,
|
||||||
startupBehavior,
|
startupBehavior,
|
||||||
dRows,
|
dRows,
|
||||||
dCols
|
dCols,
|
||||||
|
entryAssistDocMode,
|
||||||
|
entryAssistWordWrap,
|
||||||
|
eaStartCol,
|
||||||
|
eaEndCol,
|
||||||
|
entryAssistBell,
|
||||||
|
eaBellCol,
|
||||||
|
entryAssistTabStops,
|
||||||
|
insertOffOnAid,
|
||||||
|
fourColorOverride,
|
||||||
|
numericFieldLock,
|
||||||
|
autoSkipEnabled,
|
||||||
|
defaultHostType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (startupBehavior.equals("AUTO_CONNECT", ignoreCase = true)) {
|
||||||
|
val targetHost = savedHosts.find { it.id == selectedAutoHostId }
|
||||||
|
if (targetHost != null) {
|
||||||
|
haus.nightmare.a3270.storage.HostStorage.saveHost(context, targetHost.copy(autoConnect = true))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val autoHost = haus.nightmare.a3270.storage.HostStorage.getAutoConnectHost(context)
|
||||||
|
if (autoHost != null) {
|
||||||
|
haus.nightmare.a3270.storage.HostStorage.saveHost(context, autoHost.copy(autoConnect = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
) {
|
) {
|
||||||
@@ -512,11 +1045,31 @@ fun SettingsDialog(
|
|||||||
crosshairRuler = haus.nightmare.a3270.storage.AppSettings.isCrosshairRulerEnabled(context)
|
crosshairRuler = haus.nightmare.a3270.storage.AppSettings.isCrosshairRulerEnabled(context)
|
||||||
cursorStyle = haus.nightmare.a3270.storage.AppSettings.getCursorStyle(context)
|
cursorStyle = haus.nightmare.a3270.storage.AppSettings.getCursorStyle(context)
|
||||||
blockSelectMode = haus.nightmare.a3270.storage.AppSettings.isBlockSelectModeEnabled(context)
|
blockSelectMode = haus.nightmare.a3270.storage.AppSettings.isBlockSelectModeEnabled(context)
|
||||||
|
blockPaste = haus.nightmare.a3270.storage.AppSettings.isBlockPasteEnabled(context)
|
||||||
|
trimRectSizingHandles = haus.nightmare.a3270.storage.AppSettings.isTrimRectSizingHandlesEnabled(context)
|
||||||
|
trimRectRemainAfterEdit = haus.nightmare.a3270.storage.AppSettings.isTrimRectRemainAfterEditEnabled(context)
|
||||||
|
pasteWordBreak = haus.nightmare.a3270.storage.AppSettings.isPasteWordBreakEnabled(context)
|
||||||
|
hotspotsEnabled = haus.nightmare.a3270.storage.AppSettings.isHotspotsEnabled(context)
|
||||||
|
hotspotsUrlEnabled = haus.nightmare.a3270.storage.AppSettings.isHotspotsUrlEnabled(context)
|
||||||
|
hotspotsPfEnabled = haus.nightmare.a3270.storage.AppSettings.isHotspotsPfEnabled(context)
|
||||||
|
hotspotsMenuChoicesEnabled = haus.nightmare.a3270.storage.AppSettings.isHotspotsMenuChoicesEnabled(context)
|
||||||
startupBehavior = haus.nightmare.a3270.storage.AppSettings.getStartupBehavior(context).name
|
startupBehavior = haus.nightmare.a3270.storage.AppSettings.getStartupBehavior(context).name
|
||||||
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
|
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
|
||||||
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
||||||
defaultCodePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)
|
defaultCodePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)
|
||||||
defaultGraphicsMode = haus.nightmare.a3270.storage.AppSettings.getDefaultGraphicsMode(context)
|
defaultGraphicsMode = haus.nightmare.a3270.storage.AppSettings.getDefaultGraphicsMode(context)
|
||||||
|
entryAssistDocMode = haus.nightmare.a3270.storage.AppSettings.getEntryAssistDocMode(context)
|
||||||
|
entryAssistWordWrap = haus.nightmare.a3270.storage.AppSettings.getEntryAssistWordWrap(context)
|
||||||
|
entryAssistStartColStr = haus.nightmare.a3270.storage.AppSettings.getEntryAssistStartCol(context).toString()
|
||||||
|
entryAssistEndColStr = haus.nightmare.a3270.storage.AppSettings.getEntryAssistEndCol(context).toString()
|
||||||
|
entryAssistBell = haus.nightmare.a3270.storage.AppSettings.getEntryAssistBell(context)
|
||||||
|
entryAssistBellColStr = haus.nightmare.a3270.storage.AppSettings.getEntryAssistBellCol(context).toString()
|
||||||
|
entryAssistTabStops = haus.nightmare.a3270.storage.AppSettings.getEntryAssistTabStops(context)
|
||||||
|
insertOffOnAid = haus.nightmare.a3270.storage.AppSettings.getInsertOffOnAid(context)
|
||||||
|
fourColorOverride = haus.nightmare.a3270.storage.AppSettings.getFourColorOverride(context)
|
||||||
|
numericFieldLock = haus.nightmare.a3270.storage.AppSettings.getNumericFieldLock(context)
|
||||||
|
autoSkipEnabled = haus.nightmare.a3270.storage.AppSettings.getAutoSkipEnabled(context)
|
||||||
|
defaultHostType = haus.nightmare.a3270.storage.AppSettings.getDefaultHostType(context)
|
||||||
android.widget.Toast.makeText(context, "Settings imported from INI", android.widget.Toast.LENGTH_SHORT).show()
|
android.widget.Toast.makeText(context, "Settings imported from INI", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.widget.Toast.makeText(context, "Error importing: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
|
android.widget.Toast.makeText(context, "Error importing: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ class TerminalInputView(context: Context) : View(context) {
|
|||||||
var onBackspace: () -> Unit = {}
|
var onBackspace: () -> Unit = {}
|
||||||
var onTab: () -> Unit = {}
|
var onTab: () -> Unit = {}
|
||||||
var onBackTab: () -> Unit = {}
|
var onBackTab: () -> Unit = {}
|
||||||
|
var isDbcsSession: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
imm?.restartInput(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isFocusable = true
|
isFocusable = true
|
||||||
@@ -97,9 +105,13 @@ class TerminalInputView(context: Context) : View(context) {
|
|||||||
override fun onCheckIsTextEditor(): Boolean = true
|
override fun onCheckIsTextEditor(): Boolean = true
|
||||||
|
|
||||||
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
|
||||||
outAttrs.inputType = InputType.TYPE_CLASS_TEXT or
|
outAttrs.inputType = if (isDbcsSession) {
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or
|
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||||
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
} else {
|
||||||
|
InputType.TYPE_CLASS_TEXT or
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or
|
||||||
|
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||||
|
}
|
||||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN or
|
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN or
|
||||||
EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
||||||
EditorInfo.IME_ACTION_NONE
|
EditorInfo.IME_ACTION_NONE
|
||||||
@@ -111,4 +123,10 @@ class TerminalInputView(context: Context) : View(context) {
|
|||||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideSoftKeyboard() {
|
||||||
|
clearFocus()
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,13 +70,24 @@ fun TerminalView(
|
|||||||
cursorStyle: String = "BLOCK",
|
cursorStyle: String = "BLOCK",
|
||||||
isInsertMode: Boolean = false,
|
isInsertMode: Boolean = false,
|
||||||
blockSelectMode: Boolean = false,
|
blockSelectMode: Boolean = false,
|
||||||
|
blockPaste: Boolean = true,
|
||||||
|
trimRectSizingHandles: Boolean = false,
|
||||||
|
trimRectRemainAfterEdit: Boolean = false,
|
||||||
|
hotspotsEnabled: Boolean = false,
|
||||||
|
hotspotEngine: HotspotEngine? = null,
|
||||||
|
fourColorOverride: Boolean = false,
|
||||||
searchHighlightAddr: Int = -1,
|
searchHighlightAddr: Int = -1,
|
||||||
searchHighlightLen: Int = 0,
|
searchHighlightLen: Int = 0,
|
||||||
onTapAddress: (Int) -> Unit,
|
onTapAddress: (Int) -> Unit,
|
||||||
onLightPenSelect: ((Int) -> Unit)? = null,
|
onLightPenSelect: ((Int) -> Unit)? = null,
|
||||||
|
onSendHotspotAction: ((String) -> Unit)? = null,
|
||||||
|
onOpenUrl: ((String) -> Unit)? = null,
|
||||||
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
|
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
|
||||||
onPasteText: (String) -> Unit = {},
|
onPasteText: (String) -> Unit = {},
|
||||||
onPasteLineWrap: ((String, Int, Boolean) -> Unit)? = null,
|
onPasteLineWrap: ((String, Int, Boolean) -> Unit)? = null,
|
||||||
|
onToggleBlockSelectMode: (() -> Unit)? = null,
|
||||||
|
onToggleBlockPaste: (() -> Unit)? = null,
|
||||||
|
onToggleHotspots: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -164,8 +175,10 @@ fun TerminalView(
|
|||||||
} else if (totalDistance <= slop) {
|
} else if (totalDistance <= slop) {
|
||||||
// Short Tap
|
// Short Tap
|
||||||
showContextMenu = false
|
showContextMenu = false
|
||||||
selectionStart = null
|
if (!trimRectRemainAfterEdit) {
|
||||||
selectionEnd = null
|
selectionStart = null
|
||||||
|
selectionEnd = null
|
||||||
|
}
|
||||||
val metrics = calculateGridMetrics(size.width.toFloat(), size.height.toFloat(), cols, rows)
|
val metrics = calculateGridMetrics(size.width.toFloat(), size.height.toFloat(), cols, rows)
|
||||||
if (metrics.cellWidth > 0 && metrics.cellHeight > 0) {
|
if (metrics.cellWidth > 0 && metrics.cellHeight > 0) {
|
||||||
val col = ((startPos.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1)
|
val col = ((startPos.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1)
|
||||||
@@ -184,15 +197,48 @@ fun TerminalView(
|
|||||||
} else if (isLightPenMode) {
|
} else if (isLightPenMode) {
|
||||||
onLightPenSelect?.invoke(clickAddr)
|
onLightPenSelect?.invoke(clickAddr)
|
||||||
} else {
|
} else {
|
||||||
var addr = clickAddr
|
// 1. Hotspot tap check when enabled
|
||||||
val buf = screenBuffer
|
var handledHotspot = false
|
||||||
if (buf != null && buf.isFormatted) {
|
if (hotspotsEnabled && hotspotEngine != null) {
|
||||||
val faVal = buf.getFieldAttributeAt(addr)
|
val h = hotspotEngine.findHotspot(row, col)
|
||||||
if (faIsProtected(faVal.toInt() and 0xFF) || buf.getCell(addr).isFieldAttribute) {
|
if (h != null) {
|
||||||
addr = buf.findNextUnprotected(addr)
|
if (h.type == HotspotEngine.HotspotType.URL) {
|
||||||
|
onOpenUrl?.invoke(h.action)
|
||||||
|
handledHotspot = true
|
||||||
|
} else {
|
||||||
|
onSendHotspotAction?.invoke(h.action)
|
||||||
|
handledHotspot = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handledHotspot) {
|
||||||
|
// 2. Detectable field check (HoD parity)
|
||||||
|
val buf = screenBuffer
|
||||||
|
var isDetectable = false
|
||||||
|
if (buf != null && buf.isFormatted) {
|
||||||
|
val faPos = buf.findFieldAttribute(clickAddr)
|
||||||
|
if (faPos >= 0) {
|
||||||
|
val fa = buf.getCell(faPos).fa.toInt() and 0xFF
|
||||||
|
if (faIsSelectable(fa) && !faIsZero(fa)) {
|
||||||
|
onLightPenSelect?.invoke(clickAddr)
|
||||||
|
isDetectable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Normal tap cursor positioning
|
||||||
|
if (!isDetectable) {
|
||||||
|
var addr = clickAddr
|
||||||
|
if (buf != null && buf.isFormatted) {
|
||||||
|
val faVal = buf.getFieldAttributeAt(addr)
|
||||||
|
if (faIsProtected(faVal.toInt() and 0xFF) || buf.getCell(addr).isFieldAttribute) {
|
||||||
|
addr = buf.findNextUnprotected(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTapAddress(addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onTapAddress(addr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,18 +302,17 @@ fun TerminalView(
|
|||||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||||
val gridW = metrics.gridWidth.toInt()
|
val gridW = metrics.gridWidth.toInt()
|
||||||
val gridH = metrics.gridHeight.toInt()
|
val gridH = metrics.gridHeight.toInt()
|
||||||
if (gridW > 0 && gridH > 0) {
|
val gWidth = graphicsPlane.canvasWidth
|
||||||
graphicsPlane.resize(gridW, gridH)
|
val gHeight = graphicsPlane.canvasHeight
|
||||||
val rgb = graphicsPlane.rgbBuffer
|
val rgb = graphicsPlane.rgbBuffer
|
||||||
if (rgb != null) {
|
if (gridW > 0 && gridH > 0 && rgb != null && gWidth > 0 && gHeight > 0) {
|
||||||
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
|
val bmp = android.graphics.Bitmap.createBitmap(rgb, gWidth, gHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||||
bmp,
|
bmp,
|
||||||
null,
|
null,
|
||||||
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
|
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,9 +355,9 @@ fun TerminalView(
|
|||||||
var csVal = 0
|
var csVal = 0
|
||||||
var ecVal = 0
|
var ecVal = 0
|
||||||
|
|
||||||
if (buf != null) {
|
val ea = if (buf != null) buf.getCell(addr) else null
|
||||||
val ea = buf.getCell(addr)
|
|
||||||
|
|
||||||
|
if (ea != null) {
|
||||||
if (ea.isFieldAttribute) {
|
if (ea.isFieldAttribute) {
|
||||||
currentFA = ea.fa
|
currentFA = ea.fa
|
||||||
currentFieldEa = ea
|
currentFieldEa = ea
|
||||||
@@ -320,7 +365,7 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute 3270 color and field intensity
|
// Compute 3270 color and field intensity
|
||||||
fgColor = getFgColorForAttribute(ea, currentFieldEa, currentFA)
|
fgColor = getFgColorForAttribute(ea, currentFieldEa, currentFA, fourColorOverride)
|
||||||
bgColor = getBgColorForAttribute(ea, currentFieldEa)
|
bgColor = getBgColorForAttribute(ea, currentFieldEa)
|
||||||
|
|
||||||
csVal = if (ea.cs != 0.toByte()) (ea.cs.toInt() and 0xFF)
|
csVal = if (ea.cs != 0.toByte()) (ea.cs.toInt() and 0xFF)
|
||||||
@@ -366,7 +411,10 @@ fun TerminalView(
|
|||||||
bgColor = tmp
|
bgColor = tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bgColor != COLOR_BLACK && bgColor != Color.Transparent) {
|
val tr = if (ea != null && ea.tr != 0.toByte()) ea.tr else (currentFieldEa?.tr ?: 0)
|
||||||
|
val isOpaque = ((tr.toInt() and 0xFF) == (ExtendedAttribute.TR_OPAQUE.toInt() and 0xFF))
|
||||||
|
|
||||||
|
if (isOpaque || (bgColor != COLOR_BLACK && bgColor != Color.Transparent)) {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = bgColor,
|
color = bgColor,
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
@@ -404,19 +452,22 @@ fun TerminalView(
|
|||||||
|
|
||||||
// Cursor indicator (respects blinking toggle & timer, cursor style, insert mode)
|
// Cursor indicator (respects blinking toggle & timer, cursor style, insert mode)
|
||||||
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
|
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
|
||||||
|
val isDbcsCur = ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT)
|
||||||
|
val curWidth = if (isDbcsCur) cellWidth * 2f else cellWidth
|
||||||
|
val curLeft = if (isDbcsCur && ea?.db == ExtendedAttribute.DB_RIGHT && c > 0) left - cellWidth else left
|
||||||
val isUnderlineCursor = cursorStyle.equals("UNDERLINE", ignoreCase = true) || isInsertMode
|
val isUnderlineCursor = cursorStyle.equals("UNDERLINE", ignoreCase = true) || isInsertMode
|
||||||
if (isUnderlineCursor) {
|
if (isUnderlineCursor) {
|
||||||
val ulH = maxOf(4f, cellHeight / 4f)
|
val ulH = maxOf(4f, cellHeight / 4f)
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color(255, 255, 255, 180),
|
color = Color(255, 255, 255, 180),
|
||||||
topLeft = Offset(left, top + cellHeight - ulH),
|
topLeft = Offset(curLeft, top + cellHeight - ulH),
|
||||||
size = Size(cellWidth, ulH)
|
size = Size(curWidth, ulH)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color(255, 255, 255, 180),
|
color = Color(255, 255, 255, 180),
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(curLeft, top),
|
||||||
size = Size(cellWidth, cellHeight)
|
size = Size(curWidth, cellHeight)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,12 +493,14 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val suppressText = isBlink && !textBlinkVisible
|
val isDbcsRight = ea?.db == ExtendedAttribute.DB_RIGHT
|
||||||
|
val isDbcsSoSi = ea?.db == ExtendedAttribute.DB_SO || ea?.db == ExtendedAttribute.DB_SI
|
||||||
|
val suppressText = (isBlink && !textBlinkVisible) || isDbcsRight || isDbcsSoSi
|
||||||
if (!drawnAsPs && charVal != ' ' && !suppressText) {
|
if (!drawnAsPs && charVal != ' ' && !suppressText) {
|
||||||
paint.color = fgColor.toArgb()
|
paint.color = fgColor.toArgb()
|
||||||
paint.isFakeBoldText = isBold
|
paint.isFakeBoldText = isBold
|
||||||
val fontMetrics = paint.fontMetrics
|
val fontMetrics = paint.fontMetrics
|
||||||
val textX = left + cellWidth / 2f
|
val textX = if (ea?.db == ExtendedAttribute.DB_LEFT) left + cellWidth else left + cellWidth / 2f
|
||||||
val textY = top + (cellHeight - fontMetrics.bottom + fontMetrics.top) / 2f - fontMetrics.top
|
val textY = top + (cellHeight - fontMetrics.bottom + fontMetrics.top) / 2f - fontMetrics.top
|
||||||
|
|
||||||
drawContext.canvas.nativeCanvas.drawText(
|
drawContext.canvas.nativeCanvas.drawText(
|
||||||
@@ -466,6 +519,102 @@ fun TerminalView(
|
|||||||
strokeWidth = 2f
|
strokeWidth = 2f
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field Outlining (0xC2 / TY_FOUTLINE)
|
||||||
|
val cellEa = if (buf != null) buf.getCell(addr) else null
|
||||||
|
var outline = if (cellEa != null) cellEa.ol.toInt() and 0xFF else 0
|
||||||
|
val fEa = currentFieldEa
|
||||||
|
val olUnderline = ExtendedAttribute.OL_UNDERLINE.toInt() and 0xFF
|
||||||
|
val olOverline = ExtendedAttribute.OL_OVERLINE.toInt() and 0xFF
|
||||||
|
val olLeft = ExtendedAttribute.OL_LEFT.toInt() and 0xFF
|
||||||
|
val olRight = ExtendedAttribute.OL_RIGHT.toInt() and 0xFF
|
||||||
|
|
||||||
|
if (outline == 0 && fEa != null && fEa.ol != 0.toByte()) {
|
||||||
|
val fol = fEa.ol.toInt() and 0xFF
|
||||||
|
if ((fol and olUnderline) != 0) outline = outline or olUnderline
|
||||||
|
if ((fol and olOverline) != 0) outline = outline or olOverline
|
||||||
|
|
||||||
|
val size = totalCells
|
||||||
|
val prevAddr = (addr - 1 + size) % size
|
||||||
|
if (buf != null && (buf.getDisplayCell(prevAddr)?.isFieldAttribute == true || c == 0)) {
|
||||||
|
if ((fol and olLeft) != 0) outline = outline or olLeft
|
||||||
|
}
|
||||||
|
val nextAddr = (addr + 1) % size
|
||||||
|
if (buf != null && (buf.getDisplayCell(nextAddr)?.isFieldAttribute == true || c == cols - 1)) {
|
||||||
|
if ((fol and olRight) != 0) outline = outline or olRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outline != 0) {
|
||||||
|
if ((outline and olUnderline) != 0) {
|
||||||
|
drawLine(fgColor, Offset(left, top + cellHeight - 1f), Offset(left + cellWidth, top + cellHeight - 1f), strokeWidth = 1.5f)
|
||||||
|
}
|
||||||
|
if ((outline and olRight) != 0) {
|
||||||
|
drawLine(fgColor, Offset(left + cellWidth - 1f, top), Offset(left + cellWidth - 1f, top + cellHeight), strokeWidth = 1.5f)
|
||||||
|
}
|
||||||
|
if ((outline and olOverline) != 0) {
|
||||||
|
drawLine(fgColor, Offset(left, top), Offset(left + cellWidth, top), strokeWidth = 1.5f)
|
||||||
|
}
|
||||||
|
if ((outline and olLeft) != 0) {
|
||||||
|
drawLine(fgColor, Offset(left, top), Offset(left, top + cellHeight), strokeWidth = 1.5f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rectangular Block Selection Sizing Handles & Box Outline
|
||||||
|
if (trimRectSizingHandles && selMinRow != -1 && selMinCol != -1) {
|
||||||
|
val x1 = offsetX + selMinCol * cellWidth
|
||||||
|
val y1 = offsetY + selMinRow * cellHeight
|
||||||
|
val x2 = offsetX + (selMaxCol + 1) * cellWidth
|
||||||
|
val y2 = offsetY + (selMaxRow + 1) * cellHeight
|
||||||
|
val w = x2 - x1
|
||||||
|
val h = y2 - y1
|
||||||
|
val xm = (x1 + x2) / 2f
|
||||||
|
val ym = (y1 + y2) / 2f
|
||||||
|
|
||||||
|
drawRect(
|
||||||
|
color = Color.Yellow,
|
||||||
|
topLeft = Offset(x1, y1),
|
||||||
|
size = Size(w, h),
|
||||||
|
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2f)
|
||||||
|
)
|
||||||
|
|
||||||
|
val handleSize = 8f
|
||||||
|
val handleHalf = handleSize / 2f
|
||||||
|
val handles = listOf(
|
||||||
|
Offset(x1, y1), Offset(xm, y1), Offset(x2, y1),
|
||||||
|
Offset(x2, ym), Offset(x2, y2), Offset(xm, y2),
|
||||||
|
Offset(x1, y2), Offset(x1, ym)
|
||||||
|
)
|
||||||
|
for (pt in handles) {
|
||||||
|
drawRect(
|
||||||
|
color = Color.Yellow,
|
||||||
|
topLeft = Offset(pt.x - handleHalf, pt.y - handleHalf),
|
||||||
|
size = Size(handleSize, handleSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactive Hotspot Underline Rendering
|
||||||
|
if (hotspotsEnabled && hotspotEngine != null && buf != null) {
|
||||||
|
val activeHotspots = hotspotEngine.scan(buf)
|
||||||
|
for (h in activeHotspots) {
|
||||||
|
val hr0 = h.startRow
|
||||||
|
val hr1 = h.endRow
|
||||||
|
for (r in hr0..hr1) {
|
||||||
|
val c0 = if (r == hr0) h.startCol else 0
|
||||||
|
val c1 = if (r == hr1) h.endCol else cols - 1
|
||||||
|
val x1 = offsetX + c0 * cellWidth
|
||||||
|
val x2 = offsetX + (c1 + 1) * cellWidth
|
||||||
|
val y = offsetY + (r + 1) * cellHeight - 2f
|
||||||
|
drawLine(
|
||||||
|
color = Color(0xFF00DCFF),
|
||||||
|
start = Offset(x1, y),
|
||||||
|
end = Offset(x2, y),
|
||||||
|
strokeWidth = 2f
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,6 +718,36 @@ fun TerminalView(
|
|||||||
showPasteWrapDialog = true
|
showPasteWrapDialog = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (onToggleBlockSelectMode != null) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(if (blockSelectMode) "✓ Block Select Mode" else " Block Select Mode") },
|
||||||
|
onClick = {
|
||||||
|
showContextMenu = false
|
||||||
|
onToggleBlockSelectMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onToggleBlockPaste != null) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(if (blockPaste) "✓ Block Paste" else " Block Paste") },
|
||||||
|
onClick = {
|
||||||
|
showContextMenu = false
|
||||||
|
onToggleBlockPaste()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onToggleHotspots != null) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(if (hotspotsEnabled) "✓ Interactive Hotspots" else " Interactive Hotspots") },
|
||||||
|
onClick = {
|
||||||
|
showContextMenu = false
|
||||||
|
onToggleHotspots()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showPasteWrapDialog) {
|
if (showPasteWrapDialog) {
|
||||||
@@ -669,8 +848,8 @@ fun calculateGridMetrics(
|
|||||||
|
|
||||||
val gridW = cols * cellW
|
val gridW = cols * cellW
|
||||||
val gridH = rows * cellH
|
val gridH = rows * cellH
|
||||||
val offX = (viewWidth - gridW) / 2f
|
val offX = maxOf(0f, (viewWidth - gridW) / 2f)
|
||||||
val offY = (viewHeight - gridH) / 2f
|
val offY = maxOf(0f, (viewHeight - gridH) / 2f)
|
||||||
|
|
||||||
return TerminalGridMetrics(cellW, cellH, gridW, gridH, offX, offY)
|
return TerminalGridMetrics(cellW, cellH, gridW, gridH, offX, offY)
|
||||||
}
|
}
|
||||||
@@ -711,8 +890,21 @@ private fun copySelection(
|
|||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
if (addr < (rows * cols)) {
|
if (addr < (rows * cols)) {
|
||||||
val cell = screenBuffer.getCell(addr)
|
val cell = screenBuffer.getCell(addr)
|
||||||
if (cell != null && !cell.isFieldAttribute && cell.ucs4 > ' ' && cell.ucs4.code != 0xFFFF) {
|
if (cell != null && !cell.isFieldAttribute) {
|
||||||
line.append(cell.ucs4)
|
if (cell.db == ExtendedAttribute.DB_RIGHT) {
|
||||||
|
val isSubsequent = if (blockSelectMode) c > minCol else (addr > (minRow * cols + minCol))
|
||||||
|
if (isSubsequent) {
|
||||||
|
continue // Character already added by DB_LEFT
|
||||||
|
} else {
|
||||||
|
line.append(' ')
|
||||||
|
}
|
||||||
|
} else if (cell.db == ExtendedAttribute.DB_SO || cell.db == ExtendedAttribute.DB_SI) {
|
||||||
|
line.append(' ')
|
||||||
|
} else if (cell.ucs4 > ' ' && cell.ucs4.code != 0xFFFF) {
|
||||||
|
line.append(cell.ucs4)
|
||||||
|
} else {
|
||||||
|
line.append(' ')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
line.append(' ')
|
line.append(' ')
|
||||||
}
|
}
|
||||||
@@ -728,13 +920,20 @@ private fun copySelection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFgColorForAttribute(ea: ExtendedAttribute, currentFieldEa: ExtendedAttribute?, currentFA: Byte): Color {
|
internal fun getFgColorForAttribute(
|
||||||
val fg = if (ea.fg != 0.toByte()) (ea.fg.toInt() and 0xFF)
|
ea: ExtendedAttribute,
|
||||||
else if (currentFieldEa != null && currentFieldEa.fg != 0.toByte()) (currentFieldEa.fg.toInt() and 0xFF)
|
currentFieldEa: ExtendedAttribute?,
|
||||||
else 0
|
currentFA: Byte,
|
||||||
|
fourColorOverride: Boolean = false
|
||||||
|
): Color {
|
||||||
|
if (!fourColorOverride) {
|
||||||
|
val fg = if (ea.fg != 0.toByte()) (ea.fg.toInt() and 0xFF)
|
||||||
|
else if (currentFieldEa != null && currentFieldEa.fg != 0.toByte()) (currentFieldEa.fg.toInt() and 0xFF)
|
||||||
|
else 0
|
||||||
|
|
||||||
if (fg in 0xf0..0xff) {
|
if (fg in 0xf0..0xff) {
|
||||||
return HOST_COLORS[fg - 0xf0]
|
return HOST_COLORS[fg - 0xf0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val fa = currentFA.toInt() and 0xFF
|
val fa = currentFA.toInt() and 0xFF
|
||||||
return if (faIsProtected(fa)) {
|
return if (faIsProtected(fa)) {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group
|
||||||
|
android:scaleX="0.029365"
|
||||||
|
android:scaleY="-0.029365"
|
||||||
|
android:translateX="-8.57"
|
||||||
|
android:translateY="103.38">
|
||||||
|
<!-- Green Terminal Screen Frame -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#00FF00"
|
||||||
|
android:strokeWidth="40"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="M 1177.27,2220.71 V 1142.45 H 3084.31 V 2220.71 H 1177.27" />
|
||||||
|
|
||||||
|
<!-- Blue Wave / Exponential Curve -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#0000FF"
|
||||||
|
android:strokeWidth="60"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="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
|
||||||
|
android:strokeColor="#FF0000"
|
||||||
|
android:strokeWidth="60"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="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
|
||||||
|
android:strokeColor="#FFFF00"
|
||||||
|
android:strokeWidth="50"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="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" />
|
||||||
|
|
||||||
|
<!-- Magenta Typography: lib3270j -->
|
||||||
|
<!-- 'l' -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF00FF"
|
||||||
|
android:pathData="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
|
||||||
|
android:fillColor="#FF00FF"
|
||||||
|
android:pathData="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
|
||||||
|
android:fillColor="#FF00FF"
|
||||||
|
android:pathData="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
|
||||||
|
android:fillColor="#FF00FF"
|
||||||
|
android:pathData="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
|
||||||
|
android:fillColor="#FF00FF"
|
||||||
|
android:pathData="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
|
||||||
|
android:fillColor="#FF00FF"
|
||||||
|
android:pathData="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
|
||||||
|
android:fillColor="#FF00FF"
|
||||||
|
android:pathData="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
|
||||||
|
android:fillColor="#FF00FF"
|
||||||
|
android:pathData="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" />
|
||||||
|
|
||||||
|
<!-- Smiley Details -->
|
||||||
|
<!-- Mouth -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFF00"
|
||||||
|
android:strokeWidth="50"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="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 -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#00FFFF"
|
||||||
|
android:strokeWidth="50"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="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 -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#00FFFF"
|
||||||
|
android:strokeWidth="50"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="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
|
||||||
|
android:strokeColor="#FFFF00"
|
||||||
|
android:strokeWidth="50"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="m 2892.03,1528.23 -29.21,-20.68" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFF00"
|
||||||
|
android:strokeWidth="50"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="m 2862.82,1507.55 h 32.86" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<item android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<item android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<cache-path name="exports" path="exports/" />
|
||||||
|
<cache-path name="cache" path="." />
|
||||||
|
<files-path name="files" path="." />
|
||||||
|
</paths>
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.a3270.storage.AppSettings
|
||||||
|
import haus.nightmare.a3270.storage.HostStorage
|
||||||
|
import haus.nightmare.a3270.storage.SavedHost
|
||||||
|
import haus.nightmare.a3270.ui.ConnectChoice
|
||||||
|
import haus.nightmare.a3270.ui.LastTransferState
|
||||||
|
import haus.nightmare.a3270.ui.formatCMSFilter
|
||||||
|
import haus.nightmare.a3270.ui.matchesCmsFilemode
|
||||||
|
import haus.nightmare.a3270.ui.matchesCmsFilter
|
||||||
|
import haus.nightmare.a3270.ui.matchesFilter
|
||||||
|
import haus.nightmare.a3270.ui.matchesTsoFilter
|
||||||
|
import haus.nightmare.a3270.ui.matchesWildcard
|
||||||
|
import haus.nightmare.a3270.ui.resolveDefaultQuery
|
||||||
|
import haus.nightmare.lib3270j.ConnectionConfig
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.ft.FTConfig
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry
|
||||||
|
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry
|
||||||
|
import haus.nightmare.lib3270j.graphics.GraphicsMode
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class ConnectionAndHostDirectoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSavedHostToConnectionConfigPreservesMetadata() {
|
||||||
|
val hostId = UUID.randomUUID().toString()
|
||||||
|
val host = SavedHost(
|
||||||
|
id = hostId,
|
||||||
|
name = "MVS Production LPAR",
|
||||||
|
host = "mvs.prod.corp",
|
||||||
|
port = 992,
|
||||||
|
model = 4,
|
||||||
|
dynamicRows = 62,
|
||||||
|
dynamicCols = 160,
|
||||||
|
luName = "LU3270A",
|
||||||
|
autoConnect = false,
|
||||||
|
hostType = "TSO",
|
||||||
|
useTls = true,
|
||||||
|
tlsVerifyCert = true,
|
||||||
|
tn3270e = true,
|
||||||
|
graphicsMode = "VECTOR_GRAPHICS",
|
||||||
|
codePage = "1047",
|
||||||
|
proxyType = "SOCKS5",
|
||||||
|
proxyHost = "proxy.corp",
|
||||||
|
proxyPort = 1080,
|
||||||
|
proxyUsername = "mvsadmin",
|
||||||
|
proxyPassword = "mypassword"
|
||||||
|
)
|
||||||
|
|
||||||
|
val config = host.toConnectionConfig()
|
||||||
|
|
||||||
|
assertEquals("mvs.prod.corp", config.host)
|
||||||
|
assertEquals(992, config.port)
|
||||||
|
assertEquals(TerminalModel.IBM_3279_4, config.model)
|
||||||
|
assertEquals("LU3270A", config.luName)
|
||||||
|
assertTrue(config.isUseTls)
|
||||||
|
assertTrue(config.isTlsVerifyCert)
|
||||||
|
assertTrue(config.isTn3270eEnabled)
|
||||||
|
assertEquals(GraphicsMode.VECTOR_GRAPHICS, config.graphicsMode)
|
||||||
|
assertEquals("1047", config.codePage)
|
||||||
|
assertEquals(ConnectionConfig.ProxyType.SOCKS5, config.proxyType)
|
||||||
|
assertEquals("proxy.corp", config.proxyHost)
|
||||||
|
assertEquals(1080, config.proxyPort)
|
||||||
|
assertEquals("mvsadmin", config.proxyUsername)
|
||||||
|
assertEquals("mypassword", config.proxyPassword)
|
||||||
|
|
||||||
|
// Metadata preservation added in connection hardening
|
||||||
|
assertTrue("savedConnection flag must be true", config.isSavedConnection)
|
||||||
|
assertEquals(hostId, config.savedProfileId)
|
||||||
|
assertEquals("MVS Production LPAR", config.savedProfileName)
|
||||||
|
assertEquals("TSO", config.hostType)
|
||||||
|
assertEquals(FTConfig.HostType.TSO, config.ftHostType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSavedHostToConnectionConfigCmsHostType() {
|
||||||
|
val host = SavedHost(
|
||||||
|
id = "cms-1",
|
||||||
|
name = "VM/CMS Guest",
|
||||||
|
host = "vm.local",
|
||||||
|
port = 23,
|
||||||
|
hostType = "CMS"
|
||||||
|
)
|
||||||
|
val config = host.toConnectionConfig()
|
||||||
|
assertEquals("CMS", config.hostType)
|
||||||
|
assertEquals(FTConfig.HostType.CMS, config.ftHostType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testResolveDefaultQuery() {
|
||||||
|
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, null))
|
||||||
|
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, ""))
|
||||||
|
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, " "))
|
||||||
|
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, "*"))
|
||||||
|
assertEquals("* * A", resolveDefaultQuery(FTConfig.HostType.CMS, "* * *"))
|
||||||
|
assertEquals("PROFILE EXEC A", resolveDefaultQuery(FTConfig.HostType.CMS, "PROFILE EXEC A"))
|
||||||
|
|
||||||
|
assertEquals("", resolveDefaultQuery(FTConfig.HostType.TSO, null))
|
||||||
|
assertEquals("", resolveDefaultQuery(FTConfig.HostType.TSO, ""))
|
||||||
|
assertEquals("SYS1.*", resolveDefaultQuery(FTConfig.HostType.TSO, "SYS1.*"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormatCMSFilter() {
|
||||||
|
assertEquals("* * A", formatCMSFilter(null))
|
||||||
|
assertEquals("* * A", formatCMSFilter(""))
|
||||||
|
assertEquals("* * A", formatCMSFilter(" "))
|
||||||
|
assertEquals("* * A", formatCMSFilter("*"))
|
||||||
|
assertEquals("* * A", formatCMSFilter("* * *"))
|
||||||
|
assertEquals("MYFILE * A", formatCMSFilter("MYFILE"))
|
||||||
|
assertEquals("MYFILE EXEC A", formatCMSFilter("MYFILE EXEC"))
|
||||||
|
assertEquals("MYFILE EXEC B", formatCMSFilter("MYFILE EXEC B"))
|
||||||
|
assertEquals("PROFILE EXEC A", formatCMSFilter("PROFILE.EXEC"))
|
||||||
|
assertEquals("PROFILE EXEC A", formatCMSFilter("'PROFILE.EXEC'"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMatchesWildcard() {
|
||||||
|
assertTrue(matchesWildcard("HELLO", "*"))
|
||||||
|
assertTrue(matchesWildcard("HELLO", "H*"))
|
||||||
|
assertTrue(matchesWildcard("HELLO", "H*O"))
|
||||||
|
assertTrue(matchesWildcard("HELLO", "H?LLO"))
|
||||||
|
assertTrue(matchesWildcard("HELLO", "?????"))
|
||||||
|
assertFalse(matchesWildcard("HELLO", "H?L"))
|
||||||
|
assertFalse(matchesWildcard("HELLO", "WORLD"))
|
||||||
|
|
||||||
|
assertTrue(matchesWildcard("DATA.SET.NAME", "*.NAME"))
|
||||||
|
assertTrue(matchesWildcard("DATA.SET.NAME", "DATA.*"))
|
||||||
|
assertFalse(matchesWildcard("DATA.SET.NAME", "SYS1.*"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMatchesCmsFilterAndFilemode() {
|
||||||
|
val entry = CMSDirectoryEntry("PROFILE", "EXEC", "A1").apply {
|
||||||
|
recfm = "F"
|
||||||
|
lrecl = 80
|
||||||
|
blksize = 80
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(matchesFilter(entry, null))
|
||||||
|
assertTrue(matchesFilter(entry, ""))
|
||||||
|
assertTrue(matchesFilter(entry, "* * *"))
|
||||||
|
assertTrue(matchesFilter(entry, "* * A"))
|
||||||
|
assertTrue(matchesFilter(entry, "PROFILE * A"))
|
||||||
|
assertTrue(matchesFilter(entry, "PROFILE EXEC *"))
|
||||||
|
assertTrue(matchesFilter(entry, "PROFILE EXEC A1"))
|
||||||
|
assertTrue(matchesFilter(entry, "PROFILE.EXEC"))
|
||||||
|
assertTrue(matchesFilter(entry, "PROFILE"))
|
||||||
|
assertTrue(matchesFilter(entry, "EXEC"))
|
||||||
|
|
||||||
|
assertFalse(matchesFilter(entry, "* * B"))
|
||||||
|
assertFalse(matchesFilter(entry, "TEST * *"))
|
||||||
|
assertFalse(matchesFilter(entry, "*.COBOL"))
|
||||||
|
assertFalse(matchesFilter(entry, "PROFILE EXEC A2"))
|
||||||
|
|
||||||
|
assertTrue(matchesCmsFilemode("A1", "A"))
|
||||||
|
assertTrue(matchesCmsFilemode("A1", "A1"))
|
||||||
|
assertTrue(matchesCmsFilemode("A1", "*"))
|
||||||
|
assertFalse(matchesCmsFilemode("A1", "B"))
|
||||||
|
assertFalse(matchesCmsFilemode("A1", "A2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMatchesTsoFilter() {
|
||||||
|
val entry = TSODirectoryEntry("USER.SOURCE.COBOL").apply {
|
||||||
|
volume = "VOL001"
|
||||||
|
dsorg = "PO"
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(matchesFilter(entry, null))
|
||||||
|
assertTrue(matchesFilter(entry, ""))
|
||||||
|
assertTrue(matchesFilter(entry, "*"))
|
||||||
|
assertTrue(matchesFilter(entry, "*SOURCE*"))
|
||||||
|
assertTrue(matchesFilter(entry, "USER.*.COBOL"))
|
||||||
|
assertTrue(matchesFilter(entry, "COBOL")) // substring search
|
||||||
|
assertTrue(matchesFilter(entry, "USER.SOURCE"))
|
||||||
|
|
||||||
|
assertFalse(matchesFilter(entry, "SYS1.*"))
|
||||||
|
assertFalse(matchesFilter(entry, "*.PLI"))
|
||||||
|
assertFalse(matchesFilter(entry, "FORTRAN"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAppSettingsIniHostTypeParsing() {
|
||||||
|
val testIni = """
|
||||||
|
[Connection]
|
||||||
|
hostType = CMS
|
||||||
|
|
||||||
|
[General]
|
||||||
|
defaultHostType = CICS
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
var parsedHostType: String? = null
|
||||||
|
AppSettings.parseIni(StringReader(testIni)) { _, key, value ->
|
||||||
|
if (key.equals("hostType", ignoreCase = true) ||
|
||||||
|
key.equals("hosttype", ignoreCase = true) ||
|
||||||
|
key.equals("host_type", ignoreCase = true) ||
|
||||||
|
key.equals("defaulthosttype", ignoreCase = true)
|
||||||
|
) {
|
||||||
|
parsedHostType = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotNull(parsedHostType)
|
||||||
|
// First or second value parsed
|
||||||
|
assertTrue(parsedHostType.equals("CMS", ignoreCase = true) || parsedHostType.equals("CICS", ignoreCase = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLastTransferStateUnitsAndDirBlocks() {
|
||||||
|
val state = LastTransferState()
|
||||||
|
assertEquals(FTConfig.HostType.TSO, state.hostType)
|
||||||
|
assertEquals(FTConfig.AllocationUnit.TRACKS, state.units)
|
||||||
|
assertEquals("", state.dirBlocks)
|
||||||
|
|
||||||
|
state.units = FTConfig.AllocationUnit.TRACKS
|
||||||
|
state.dirBlocks = "20"
|
||||||
|
state.hostType = FTConfig.HostType.CMS
|
||||||
|
|
||||||
|
assertEquals(FTConfig.AllocationUnit.TRACKS, state.units)
|
||||||
|
assertEquals("20", state.dirBlocks)
|
||||||
|
assertEquals(FTConfig.HostType.CMS, state.hostType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConnectChoiceEnum() {
|
||||||
|
val choices = ConnectChoice.values()
|
||||||
|
assertEquals(3, choices.size)
|
||||||
|
assertTrue(choices.contains(ConnectChoice.NEW_WINDOW))
|
||||||
|
assertTrue(choices.contains(ConnectChoice.ABORT_CURRENT))
|
||||||
|
assertTrue(choices.contains(ConnectChoice.CANCEL))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHostStorageExportAndLoadIniRoundtrip() {
|
||||||
|
val host1 = SavedHost(
|
||||||
|
id = "test-h1",
|
||||||
|
name = "Test Mainframe MVS",
|
||||||
|
host = "mvs.corp.local",
|
||||||
|
port = 992,
|
||||||
|
model = 4,
|
||||||
|
dynamicRows = 62,
|
||||||
|
dynamicCols = 160,
|
||||||
|
luName = "LU01",
|
||||||
|
autoConnect = true,
|
||||||
|
hostType = "TSO",
|
||||||
|
useTls = true,
|
||||||
|
tlsVerifyCert = false,
|
||||||
|
tn3270e = true,
|
||||||
|
graphicsMode = "VECTOR_GRAPHICS",
|
||||||
|
codePage = "1047",
|
||||||
|
proxyType = "SOCKS5",
|
||||||
|
proxyHost = "proxy.corp",
|
||||||
|
proxyPort = 1080,
|
||||||
|
proxyUsername = "admin",
|
||||||
|
proxyPassword = "secret"
|
||||||
|
)
|
||||||
|
val host2 = SavedHost(
|
||||||
|
id = "test-h2",
|
||||||
|
name = "VM CMS Guest",
|
||||||
|
host = "vm.corp.local",
|
||||||
|
port = 23,
|
||||||
|
hostType = "CMS"
|
||||||
|
)
|
||||||
|
|
||||||
|
val sw = java.io.StringWriter()
|
||||||
|
HostStorage.exportToIni(listOf(host1, host2), sw)
|
||||||
|
val exportedIni = sw.toString()
|
||||||
|
|
||||||
|
assertTrue(exportedIni.contains("[Test Mainframe MVS]"))
|
||||||
|
assertTrue(exportedIni.contains("host = mvs.corp.local"))
|
||||||
|
assertTrue(exportedIni.contains("port = 992"))
|
||||||
|
assertTrue(exportedIni.contains("autoConnect = true"))
|
||||||
|
assertTrue(exportedIni.contains("hostType = TSO"))
|
||||||
|
assertTrue(exportedIni.contains("useTls = true"))
|
||||||
|
assertTrue(exportedIni.contains("[VM CMS Guest]"))
|
||||||
|
assertTrue(exportedIni.contains("hostType = CMS"))
|
||||||
|
|
||||||
|
val loaded = HostStorage.loadFromIni(StringReader(exportedIni))
|
||||||
|
assertEquals(2, loaded.size)
|
||||||
|
|
||||||
|
val loadedH1 = loaded.find { it.name == "Test Mainframe MVS" }
|
||||||
|
assertNotNull(loadedH1)
|
||||||
|
assertEquals("test-h1", loadedH1!!.id)
|
||||||
|
assertEquals("mvs.corp.local", loadedH1.host)
|
||||||
|
assertEquals(992, loadedH1.port)
|
||||||
|
assertEquals(4, loadedH1.model)
|
||||||
|
assertEquals(62, loadedH1.dynamicRows)
|
||||||
|
assertEquals(160, loadedH1.dynamicCols)
|
||||||
|
assertEquals("LU01", loadedH1.luName)
|
||||||
|
assertTrue(loadedH1.autoConnect)
|
||||||
|
assertEquals("TSO", loadedH1.hostType)
|
||||||
|
assertTrue(loadedH1.useTls)
|
||||||
|
assertFalse(loadedH1.tlsVerifyCert)
|
||||||
|
assertTrue(loadedH1.tn3270e)
|
||||||
|
assertEquals("VECTOR_GRAPHICS", loadedH1.graphicsMode)
|
||||||
|
assertEquals("1047", loadedH1.codePage)
|
||||||
|
assertEquals("SOCKS5", loadedH1.proxyType)
|
||||||
|
assertEquals("proxy.corp", loadedH1.proxyHost)
|
||||||
|
assertEquals(1080, loadedH1.proxyPort)
|
||||||
|
assertEquals("admin", loadedH1.proxyUsername)
|
||||||
|
assertEquals("secret", loadedH1.proxyPassword)
|
||||||
|
|
||||||
|
val loadedH2 = loaded.find { it.name == "VM CMS Guest" }
|
||||||
|
assertNotNull(loadedH2)
|
||||||
|
assertEquals("test-h2", loadedH2!!.id)
|
||||||
|
assertEquals("vm.corp.local", loadedH2.host)
|
||||||
|
assertEquals(23, loadedH2.port)
|
||||||
|
assertEquals("CMS", loadedH2.hostType)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.a3270.storage.HostStorage
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.charset.CodePageRegistry
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
class DBCSAndParityCompatibilityTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNewJapaneseCodePagesAvailableAndResolvable() {
|
||||||
|
val availableKeys = HostStorage.AVAILABLE_CODE_PAGES.map { it.first }
|
||||||
|
assertTrue("290 should be available in HostStorage", availableKeys.contains("290"))
|
||||||
|
assertTrue("1027 should be available in HostStorage", availableKeys.contains("1027"))
|
||||||
|
assertTrue("1390 should be available in HostStorage", availableKeys.contains("1390"))
|
||||||
|
assertTrue("1399 should be available in HostStorage", availableKeys.contains("1399"))
|
||||||
|
|
||||||
|
val cp290 = CodePageRegistry.getCodePage("290")
|
||||||
|
assertNotNull("Cp290 must resolve", cp290)
|
||||||
|
assertFalse("Cp290 is SBCS", cp290.isDBCS)
|
||||||
|
|
||||||
|
val cp1027 = CodePageRegistry.getCodePage("1027")
|
||||||
|
assertNotNull("Cp1027 must resolve", cp1027)
|
||||||
|
assertFalse("Cp1027 is SBCS", cp1027.isDBCS)
|
||||||
|
|
||||||
|
val cp1390 = CodePageRegistry.getCodePage("1390")
|
||||||
|
assertNotNull("Cp1390 must resolve", cp1390)
|
||||||
|
assertTrue("Cp1390 is DBCS", cp1390.isDBCS)
|
||||||
|
|
||||||
|
val cp1399 = CodePageRegistry.getCodePage("1399")
|
||||||
|
assertNotNull("Cp1399 must resolve", cp1399)
|
||||||
|
assertTrue("Cp1399 is DBCS", cp1399.isDBCS)
|
||||||
|
|
||||||
|
val trans1390 = EbcdicTranslator("1390")
|
||||||
|
assertTrue("Translator for 1390 must report isDBCS", trans1390.isDBCS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testScreenExporterDBCSNoDuplication() {
|
||||||
|
val trans = EbcdicTranslator("930")
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, trans)
|
||||||
|
screen.erase(false)
|
||||||
|
|
||||||
|
// Put a 2-cell DBCS character at cells 0 and 1: '東' (\u6771)
|
||||||
|
val cell0 = screen.getCell(0)
|
||||||
|
cell0.db = ExtendedAttribute.DB_LEFT
|
||||||
|
cell0.ucs4 = '\u6771'
|
||||||
|
|
||||||
|
val cell1 = screen.getCell(1)
|
||||||
|
cell1.db = ExtendedAttribute.DB_RIGHT
|
||||||
|
cell1.ucs4 = '\u6771'
|
||||||
|
|
||||||
|
// Put SBCS 'X' at cell 2
|
||||||
|
val cell2 = screen.getCell(2)
|
||||||
|
cell2.db = ExtendedAttribute.DB_NONE
|
||||||
|
cell2.ucs4 = 'X'
|
||||||
|
|
||||||
|
// Invoke private generatePlainText via reflection
|
||||||
|
val exporterClass = Class.forName("haus.nightmare.a3270.ui.ScreenExporterDialogKt")
|
||||||
|
val method: Method = exporterClass.getDeclaredMethod(
|
||||||
|
"generatePlainText",
|
||||||
|
ScreenBuffer::class.java,
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Boolean::class.javaPrimitiveType
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
|
||||||
|
val plainText = method.invoke(null, screen, 1, 80, true) as String
|
||||||
|
assertTrue("Output should start with single DBCS char + X: expected '東X', got: '$plainText'", plainText.startsWith("東X"))
|
||||||
|
assertFalse("DBCS char must not be doubled into '東東X'", plainText.startsWith("東東"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTerminalGridMetricsCenteringClampedAtZero() {
|
||||||
|
val terminalViewClass = Class.forName("haus.nightmare.a3270.ui.TerminalViewKt")
|
||||||
|
val method = terminalViewClass.getDeclaredMethod(
|
||||||
|
"calculateGridMetrics",
|
||||||
|
Float::class.javaPrimitiveType,
|
||||||
|
Float::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
|
||||||
|
// View is constrained / non-negative offsets
|
||||||
|
val constrainedMetrics = method.invoke(null, 400f, 300f, 80, 24)
|
||||||
|
val offXField = constrainedMetrics.javaClass.getDeclaredField("offsetX")
|
||||||
|
offXField.isAccessible = true
|
||||||
|
val offX = offXField.getFloat(constrainedMetrics)
|
||||||
|
|
||||||
|
val offYField = constrainedMetrics.javaClass.getDeclaredField("offsetY")
|
||||||
|
offYField.isAccessible = true
|
||||||
|
val offY = offYField.getFloat(constrainedMetrics)
|
||||||
|
|
||||||
|
assertTrue("Constrained offX must be non-negative", offX >= 0f)
|
||||||
|
assertTrue("Constrained offY must be non-negative", offY >= 0f)
|
||||||
|
|
||||||
|
// Wide view (pillarbox with positive offX)
|
||||||
|
val wideMetrics = method.invoke(null, 3000f, 1000f, 80, 24)
|
||||||
|
val wideOffX = offXField.getFloat(wideMetrics)
|
||||||
|
val wideOffY = offYField.getFloat(wideMetrics)
|
||||||
|
assertTrue("Wide view offX should be positive", wideOffX > 0f)
|
||||||
|
assertEquals(0f, wideOffY, 0.001f)
|
||||||
|
|
||||||
|
// Tall view (letterbox with positive offY)
|
||||||
|
val tallMetrics = method.invoke(null, 1000f, 3000f, 80, 24)
|
||||||
|
val tallOffX = offXField.getFloat(tallMetrics)
|
||||||
|
val tallOffY = offYField.getFloat(tallMetrics)
|
||||||
|
assertEquals(0f, tallOffX, 0.001f)
|
||||||
|
assertTrue("Tall view offY should be positive", tallOffY > 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExtendedAttributeTransparencyConstants() {
|
||||||
|
assertEquals(0x00.toByte(), ExtendedAttribute.TR_DEFAULT)
|
||||||
|
assertEquals(0xF0.toByte(), ExtendedAttribute.TR_OR)
|
||||||
|
assertEquals(0xFF.toByte(), ExtendedAttribute.TR_OPAQUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
package haus.nightmare.a3270
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
import haus.nightmare.a3270.storage.SavedHost
|
import haus.nightmare.a3270.storage.SavedHost
|
||||||
|
import haus.nightmare.a3270.ui.getFgColorForAttribute
|
||||||
import haus.nightmare.lib3270j.ConnectionConfig
|
import haus.nightmare.lib3270j.ConnectionConfig
|
||||||
import haus.nightmare.lib3270j.TerminalModel
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.datastream.DataStreamProcessor
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class DynamicAndParityFeaturesTest {
|
class DynamicAndParityFeaturesTest {
|
||||||
@@ -101,4 +107,73 @@ class DynamicAndParityFeaturesTest {
|
|||||||
assertTrue((num and FA_NUMERIC) != 0)
|
assertTrue((num and FA_NUMERIC) != 0)
|
||||||
assertTrue((hi and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL)
|
assertTrue((hi and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCharacterAttributesPersistAcrossFields() {
|
||||||
|
val model = TerminalModel.IBM_3279_2
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(model, translator)
|
||||||
|
val processor = DataStreamProcessor(screen, translator)
|
||||||
|
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
stream.write(CMD_EW)
|
||||||
|
stream.write(0xC3) // WCC
|
||||||
|
|
||||||
|
// SF(prot, skip)
|
||||||
|
stream.write(ORDER_SF)
|
||||||
|
stream.write(FA_PRINTABLE or FA_PROTECT or FA_NUMERIC)
|
||||||
|
|
||||||
|
// SA(yellow): XA_FOREGROUND, COLOR_YELLOW (0xF6)
|
||||||
|
stream.write(ORDER_SA)
|
||||||
|
stream.write(XA_FOREGROUND)
|
||||||
|
stream.write(0xF6)
|
||||||
|
|
||||||
|
// Data: " 4 "
|
||||||
|
stream.write(translator.stringToEbcdic(" 4 "))
|
||||||
|
|
||||||
|
// Second SF(prot, skip)
|
||||||
|
stream.write(ORDER_SF)
|
||||||
|
stream.write(FA_PRINTABLE or FA_PROTECT or FA_NUMERIC)
|
||||||
|
|
||||||
|
// Data: "DISPLAY"
|
||||||
|
stream.write(translator.stringToEbcdic("DISPLAY"))
|
||||||
|
|
||||||
|
val record = stream.toByteArray()
|
||||||
|
processor.processRecord(record, 0, record.size, true)
|
||||||
|
|
||||||
|
// " 4 " cells (indices 1..5) should have yellow foreground (0xF6)
|
||||||
|
for (i in 1..5) {
|
||||||
|
assertEquals(0xF6.toByte(), screen.getCell(i).fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Across second SF, "DISPLAY" cells (indices 7..13) must retain yellow foreground (0xF6) (commit c28c097e)
|
||||||
|
for (i in 7..13) {
|
||||||
|
assertEquals(0xF6.toByte(), screen.getCell(i).fg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTerminalViewColorResolutionForProtectedField() {
|
||||||
|
val ea = ExtendedAttribute()
|
||||||
|
// Normal protected field attribute -> Turquoise / Cyan (0x00FFFF)
|
||||||
|
val protNormalColor = getFgColorForAttribute(ea, null, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||||
|
assertEquals(androidx.compose.ui.graphics.Color(0xFF00FFFF), protNormalColor)
|
||||||
|
|
||||||
|
// Intensified protected field attribute -> White (0xFFC0C0C0 per HOST_COLOR_WHITE / HoD palette)
|
||||||
|
val protHighColor = getFgColorForAttribute(ea, null, (FA_PRINTABLE or FA_PROTECT or FA_INT_HIGH_SEL).toByte())
|
||||||
|
assertEquals(androidx.compose.ui.graphics.Color(0xFFC0C0C0), protHighColor)
|
||||||
|
|
||||||
|
// Normal unprotected -> Green (0x00FF00)
|
||||||
|
val unprotNormalColor = getFgColorForAttribute(ea, null, FA_PRINTABLE.toByte())
|
||||||
|
assertEquals(androidx.compose.ui.graphics.Color(0xFF00FF00), unprotNormalColor)
|
||||||
|
|
||||||
|
// Intensified unprotected -> Red (0xFF0000)
|
||||||
|
val unprotHighColor = getFgColorForAttribute(ea, null, (FA_PRINTABLE or FA_INT_HIGH_SEL).toByte())
|
||||||
|
assertEquals(androidx.compose.ui.graphics.Color(0xFFFF0000), unprotHighColor)
|
||||||
|
|
||||||
|
// Character mode override (SA yellow = 0xF6)
|
||||||
|
ea.fg = 0xF6.toByte()
|
||||||
|
val yellowColor = getFgColorForAttribute(ea, null, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||||
|
assertEquals(androidx.compose.ui.graphics.Color(0xFFFFFF00), yellowColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLConstants
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLOIA
|
||||||
|
import haus.nightmare.lib3270j.ecl.ECLPS
|
||||||
|
import haus.nightmare.lib3270j.input.InputProcessor
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
class EntryAssistAndModesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEntryAssistDocModeAndWordWrap() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
val inputProcessor = InputProcessor(screen, translator, null)
|
||||||
|
val oia = ECLOIA(screen, inputProcessor, null)
|
||||||
|
inputProcessor.setOIA(oia)
|
||||||
|
|
||||||
|
screen.isEntryAssistDOCmode = true
|
||||||
|
screen.isEntryAssistWordWrap = true
|
||||||
|
screen.setLeftMargin(0)
|
||||||
|
screen.setRightMargin(20)
|
||||||
|
|
||||||
|
assertTrue(screen.isEntryAssistDOCmode)
|
||||||
|
assertTrue(screen.isEntryAssistWordWrap)
|
||||||
|
assertEquals(0, screen.entryAssistStartColumn)
|
||||||
|
assertEquals(20, screen.entryAssistEndColumn)
|
||||||
|
|
||||||
|
// Type at col 18 with word crossing col 20
|
||||||
|
screen.setCursorPosition(0, 18)
|
||||||
|
inputProcessor.typeCharacter('T')
|
||||||
|
inputProcessor.typeCharacter('E')
|
||||||
|
inputProcessor.typeCharacter('S')
|
||||||
|
inputProcessor.typeCharacter('T')
|
||||||
|
|
||||||
|
// Word wrap moves partial word to next row starting at left margin
|
||||||
|
assertEquals(1, screen.cursorRow)
|
||||||
|
assertTrue(screen.cursorCol >= 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDocModeTabStops() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
screen.isEntryAssistDOCmode = true
|
||||||
|
screen.setLeftMargin(0)
|
||||||
|
screen.setRightMargin(79)
|
||||||
|
screen.setWordTabPositions(intArrayOf(10, 20, 30))
|
||||||
|
|
||||||
|
screen.setCursorPosition(0, 0)
|
||||||
|
screen.processWordTab(true)
|
||||||
|
assertEquals(10, screen.cursorCol)
|
||||||
|
|
||||||
|
screen.processWordTab(true)
|
||||||
|
assertEquals(20, screen.cursorCol)
|
||||||
|
|
||||||
|
screen.processWordTab(false)
|
||||||
|
assertEquals(10, screen.cursorCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAplModeAndGraphicEscape() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
val inputProcessor = InputProcessor(screen, translator, null)
|
||||||
|
val oia = ECLOIA(screen, inputProcessor, null)
|
||||||
|
inputProcessor.setOIA(oia)
|
||||||
|
|
||||||
|
assertFalse(inputProcessor.isAplKeyboardMode)
|
||||||
|
inputProcessor.isAplKeyboardMode = true
|
||||||
|
assertTrue(inputProcessor.isAplKeyboardMode)
|
||||||
|
assertTrue(oia.isApl)
|
||||||
|
|
||||||
|
// Type 'a' in APL mode -> stores CS_GE Graphic Escape charset
|
||||||
|
screen.setCursorPosition(0, 0)
|
||||||
|
inputProcessor.typeCharacter('a')
|
||||||
|
|
||||||
|
val cell = screen.getCell(0)
|
||||||
|
assertEquals(CS_GE.toInt(), cell.cs.toInt())
|
||||||
|
|
||||||
|
// Toggle APL mode off
|
||||||
|
inputProcessor.toggleAplKeyboardMode()
|
||||||
|
assertFalse(inputProcessor.isAplKeyboardMode)
|
||||||
|
assertFalse(oia.isApl)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAudibleBellCallback() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
val inputProcessor = InputProcessor(screen, translator, null)
|
||||||
|
|
||||||
|
val bellCount = AtomicInteger(0)
|
||||||
|
inputProcessor.setBellListener { bellCount.incrementAndGet() }
|
||||||
|
inputProcessor.isBellEnabled = true
|
||||||
|
inputProcessor.bellColumn = 74
|
||||||
|
|
||||||
|
screen.setCursorPosition(0, 70)
|
||||||
|
inputProcessor.typeCharacter('A')
|
||||||
|
inputProcessor.typeCharacter('B')
|
||||||
|
inputProcessor.typeCharacter('C')
|
||||||
|
assertEquals(0, bellCount.get())
|
||||||
|
|
||||||
|
inputProcessor.typeCharacter('D') // Col 74 -> Bell fires
|
||||||
|
assertEquals(1, bellCount.get())
|
||||||
|
|
||||||
|
inputProcessor.typeCharacter('E') // Same line -> No re-fire
|
||||||
|
assertEquals(1, bellCount.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInsertModeResetOnAid() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
val inputProcessor = InputProcessor(screen, translator, null)
|
||||||
|
|
||||||
|
inputProcessor.isInsertOffOnAid = true
|
||||||
|
inputProcessor.isInsertMode = true
|
||||||
|
assertTrue(inputProcessor.isInsertMode)
|
||||||
|
|
||||||
|
inputProcessor.sendAid(AID_ENTER)
|
||||||
|
assertFalse(inputProcessor.isInsertMode)
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
inputProcessor.isInsertOffOnAid = false
|
||||||
|
inputProcessor.isInsertMode = true
|
||||||
|
inputProcessor.sendAid(AID_ENTER)
|
||||||
|
assertTrue(inputProcessor.isInsertMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNumericFieldLock() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
val inputProcessor = InputProcessor(screen, translator, null)
|
||||||
|
val oia = ECLOIA(screen, inputProcessor, null)
|
||||||
|
inputProcessor.setOIA(oia)
|
||||||
|
|
||||||
|
screen.setFieldAttribute(0, FA_NUMERIC.toByte())
|
||||||
|
screen.setFieldAttribute(6, FA_PROTECT.toByte())
|
||||||
|
screen.cursorAddress = 1
|
||||||
|
|
||||||
|
inputProcessor.isNumericFieldLock = true
|
||||||
|
inputProcessor.typeCharacter('Z') // Non-numeric
|
||||||
|
assertTrue(inputProcessor.isKeyboardLocked)
|
||||||
|
assertEquals(ECLConstants.INHIBIT_NUMERIC_ONLY, oia.inputInhibited)
|
||||||
|
|
||||||
|
inputProcessor.reset()
|
||||||
|
assertFalse(inputProcessor.isKeyboardLocked)
|
||||||
|
|
||||||
|
// With numeric lock disabled
|
||||||
|
inputProcessor.isNumericFieldLock = false
|
||||||
|
screen.cursorAddress = 1
|
||||||
|
inputProcessor.typeCharacter('Z')
|
||||||
|
assertFalse(inputProcessor.isKeyboardLocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBase4ColorParity() {
|
||||||
|
// In 3279 Base 4-Color Mode:
|
||||||
|
// Intensified Protected -> White (0xFFFFFF)
|
||||||
|
// Normal Protected -> Turquoise / Cyan (0x00FFFF)
|
||||||
|
// Intensified Unprotected -> Red (0xFF0000)
|
||||||
|
// Normal Unprotected -> Green (0x00FF00)
|
||||||
|
val faNormProt = FA_PROTECT
|
||||||
|
val faHighProt = FA_PROTECT or FA_INT_HIGH_SEL
|
||||||
|
val faNormUnprot = 0
|
||||||
|
val faHighUnprot = FA_INT_HIGH_SEL
|
||||||
|
|
||||||
|
fun getBase4Color(fa: Int): Int {
|
||||||
|
val isProt = (fa and FA_PROTECT) != 0
|
||||||
|
val isHigh = (fa and FA_INT_HIGH_SEL) != 0
|
||||||
|
return if (isProt) {
|
||||||
|
if (isHigh) 0xFFFFFF else 0x00FFFF
|
||||||
|
} else {
|
||||||
|
if (isHigh) 0xFF0000 else 0x00FF00
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0xFFFFFF, getBase4Color(faHighProt))
|
||||||
|
assertEquals(0x00FFFF, getBase4Color(faNormProt))
|
||||||
|
assertEquals(0xFF0000, getBase4Color(faHighUnprot))
|
||||||
|
assertEquals(0x00FF00, getBase4Color(faNormUnprot))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIniSectionSyntaxMatchingJ3270() {
|
||||||
|
val sampleIni = """
|
||||||
|
[behavior]
|
||||||
|
blockSelectMode = false
|
||||||
|
blockPaste = true
|
||||||
|
|
||||||
|
[clipboard]
|
||||||
|
blockPasteMode = true
|
||||||
|
|
||||||
|
[entryassist]
|
||||||
|
docMode = true
|
||||||
|
wordWrap = true
|
||||||
|
startCol = 5
|
||||||
|
endCol = 75
|
||||||
|
bell = true
|
||||||
|
bellCol = 70
|
||||||
|
tabStops = 5,10,15,20
|
||||||
|
|
||||||
|
[modes]
|
||||||
|
resetInsertOnAid = true
|
||||||
|
fourColorOverride = true
|
||||||
|
numericLock = false
|
||||||
|
autoSkip = true
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val lines = sampleIni.lines()
|
||||||
|
var currentSection = ""
|
||||||
|
val parsed = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
|
val trimmed = line.trim()
|
||||||
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||||
|
currentSection = trimmed.substring(1, trimmed.length - 1).lowercase()
|
||||||
|
} else if (trimmed.contains("=")) {
|
||||||
|
val parts = trimmed.split("=", limit = 2)
|
||||||
|
val key = parts[0].trim().lowercase()
|
||||||
|
val value = parts[1].trim()
|
||||||
|
parsed["$currentSection.$key"] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("false", parsed["behavior.blockselectmode"])
|
||||||
|
assertEquals("true", parsed["behavior.blockpaste"])
|
||||||
|
assertEquals("true", parsed["clipboard.blockpastemode"])
|
||||||
|
|
||||||
|
assertEquals("true", parsed["entryassist.docmode"])
|
||||||
|
assertEquals("true", parsed["entryassist.wordwrap"])
|
||||||
|
assertEquals("5", parsed["entryassist.startcol"])
|
||||||
|
assertEquals("75", parsed["entryassist.endcol"])
|
||||||
|
assertEquals("true", parsed["entryassist.bell"])
|
||||||
|
assertEquals("70", parsed["entryassist.bellcol"])
|
||||||
|
assertEquals("5,10,15,20", parsed["entryassist.tabstops"])
|
||||||
|
|
||||||
|
assertEquals("true", parsed["modes.resetinsertonaid"])
|
||||||
|
assertEquals("true", parsed["modes.fourcoloroverride"])
|
||||||
|
assertEquals("false", parsed["modes.numericlock"])
|
||||||
|
assertEquals("true", parsed["modes.autoskip"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBlockPasteMultiLineAlignment() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3278_2, translator)
|
||||||
|
val input = InputProcessor(screen, translator, null)
|
||||||
|
val ps = ECLPS(screen, input, translator)
|
||||||
|
|
||||||
|
// Verify default blockPaste is true
|
||||||
|
assertTrue(input.isBlockPaste)
|
||||||
|
assertTrue(ps.isBlockPaste)
|
||||||
|
|
||||||
|
// 1. Unformatted screen: verify start column 15 is preserved across newlines
|
||||||
|
screen.erase(false)
|
||||||
|
val startPos = screen.rowColToAddress(0, 15)
|
||||||
|
screen.cursorAddress = startPos
|
||||||
|
|
||||||
|
val multilineText = "FIRST\nSECOND\nTHIRD"
|
||||||
|
val pasted = input.pasteText(multilineText, true, false, true)
|
||||||
|
assertEquals(16, pasted)
|
||||||
|
|
||||||
|
assertEquals("FIRST", ps.getString(startPos, 5))
|
||||||
|
val row1Col15 = screen.rowColToAddress(1, 15)
|
||||||
|
assertEquals("SECOND", ps.getString(row1Col15, 6))
|
||||||
|
val row2Col15 = screen.rowColToAddress(2, 15)
|
||||||
|
assertEquals("THIRD", ps.getString(row2Col15, 5))
|
||||||
|
|
||||||
|
// 2. Formatted screen with blockPaste = true
|
||||||
|
screen.erase(false)
|
||||||
|
screen.setFieldAttribute(0, FA_PRINTABLE.toByte())
|
||||||
|
screen.setFieldAttribute(80, FA_PRINTABLE.toByte())
|
||||||
|
screen.setFieldAttribute(160, FA_PRINTABLE.toByte())
|
||||||
|
|
||||||
|
screen.cursorAddress = screen.rowColToAddress(0, 25)
|
||||||
|
val pastedFormatted = input.pasteText("ALPHA\r\nBETA\r\nGAMMA", true, false, true)
|
||||||
|
assertEquals(14, pastedFormatted)
|
||||||
|
|
||||||
|
assertEquals("ALPHA", ps.getString(screen.rowColToAddress(0, 25), 5))
|
||||||
|
assertEquals("BETA", ps.getString(screen.rowColToAddress(1, 25), 4))
|
||||||
|
assertEquals("GAMMA", ps.getString(screen.rowColToAddress(2, 25), 5))
|
||||||
|
|
||||||
|
// 3. Formatted screen with blockPaste = false: reverts to first unprotected field on next row (pos 81)
|
||||||
|
screen.erase(false)
|
||||||
|
screen.setFieldAttribute(0, FA_PRINTABLE.toByte())
|
||||||
|
screen.setFieldAttribute(80, FA_PRINTABLE.toByte())
|
||||||
|
screen.cursorAddress = screen.rowColToAddress(0, 25)
|
||||||
|
input.pasteText("LINE1\nLINE2", true, false, false)
|
||||||
|
|
||||||
|
assertEquals("LINE1", ps.getString(screen.rowColToAddress(0, 25), 5))
|
||||||
|
assertEquals("LINE2", ps.getString(81, 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCmsFilenameFormatting() {
|
||||||
|
// Upload (local -> host)
|
||||||
|
assertEquals("PROFILE EXEC A", haus.nightmare.a3270.ui.formatHostFilename("profile.exec", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
|
||||||
|
assertEquals("TEST TXT A", haus.nightmare.a3270.ui.formatHostFilename("test.txt", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
|
||||||
|
assertEquals("README FILE A", haus.nightmare.a3270.ui.formatHostFilename("README", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
|
||||||
|
|
||||||
|
// Download (host -> local)
|
||||||
|
assertEquals("profile.exec", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("PROFILE EXEC A", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
|
||||||
|
assertEquals("test.data", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("TEST DATA A1", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS))
|
||||||
|
|
||||||
|
// TSO
|
||||||
|
assertEquals("'MYDATA'", haus.nightmare.a3270.ui.formatHostFilename("mydata.txt", haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO))
|
||||||
|
assertEquals("dataset.txt", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("'USER.DATASET'", haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import haus.nightmare.lib3270j.datastream.DataStreamProcessor
|
|||||||
import haus.nightmare.lib3270j.graphics.GocaDecoder
|
import haus.nightmare.lib3270j.graphics.GocaDecoder
|
||||||
import haus.nightmare.lib3270j.graphics.GraphicsPlane
|
import haus.nightmare.lib3270j.graphics.GraphicsPlane
|
||||||
import haus.nightmare.lib3270j.input.InputProcessor
|
import haus.nightmare.lib3270j.input.InputProcessor
|
||||||
|
import haus.nightmare.lib3270j.graphics.GocaConstants
|
||||||
|
import haus.nightmare.a3270.ui.calculateGridMetrics
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
import haus.nightmare.lib3270j.protocol.TelnetConstants.*
|
import haus.nightmare.lib3270j.protocol.TelnetConstants.*
|
||||||
import haus.nightmare.lib3270j.protocol.TN3270EConstants.*
|
import haus.nightmare.lib3270j.protocol.TN3270EConstants.*
|
||||||
@@ -175,4 +177,120 @@ class GraphicsAndSysReqTest {
|
|||||||
inputProcessor.sendAid(AID_SYSREQ)
|
inputProcessor.sendAid(AID_SYSREQ)
|
||||||
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.connectionState)
|
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.connectionState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testImonGddmImageRenderingAndCompatibility() {
|
||||||
|
val hasSetCharDimensions = try {
|
||||||
|
GraphicsPlane::class.java.getMethod("setCharDimensions", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
|
||||||
|
true
|
||||||
|
} catch (_: NoSuchMethodException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSetCharDimensions) {
|
||||||
|
// Commit c28c097e compatibility: standard 720x384 graphics plane verification
|
||||||
|
val plane = GraphicsPlane(720, 384)
|
||||||
|
plane.setScreenDimensions(80, 24)
|
||||||
|
val decoder = GocaDecoder(plane)
|
||||||
|
assertEquals(720, plane.totalWidth)
|
||||||
|
assertEquals(384, plane.totalHeight)
|
||||||
|
|
||||||
|
val out = ByteArrayOutputStream()
|
||||||
|
out.write(GocaConstants.G_GSCP)
|
||||||
|
out.write(0x04)
|
||||||
|
out.write(0x00); out.write(0x10) // X = 16
|
||||||
|
out.write(0x00); out.write(0x10) // Y = 16
|
||||||
|
out.write(GocaConstants.G_GSCOL)
|
||||||
|
out.write(0x01)
|
||||||
|
decoder.decodeStream(out.toByteArray(), 0, out.size())
|
||||||
|
val metrics = calculateGridMetrics(1080f, 1920f, 80, 24)
|
||||||
|
assertTrue(metrics.gridWidth > 0f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit bdfe6eec: full IMON GDDM image rendering verification
|
||||||
|
val plane = GraphicsPlane(1040, 1118)
|
||||||
|
val setDims = GraphicsPlane::class.java.getMethod("setCharDimensions", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
|
||||||
|
setDims.invoke(plane, 13, 26)
|
||||||
|
plane.setScreenDimensions(80, 43)
|
||||||
|
val decoder = GocaDecoder(plane)
|
||||||
|
|
||||||
|
assertEquals(1040, plane.totalWidth)
|
||||||
|
assertEquals(1118, plane.totalHeight)
|
||||||
|
assertEquals(520, plane.xMax)
|
||||||
|
assertEquals(558, plane.yMax)
|
||||||
|
|
||||||
|
val out = ByteArrayOutputStream()
|
||||||
|
// GSCP: Set Current Position (377, 494)
|
||||||
|
out.write(GocaConstants.G_GSCP)
|
||||||
|
out.write(0x04)
|
||||||
|
out.write(0x01); out.write(0x79) // X = 377
|
||||||
|
out.write(0x01); out.write(0xEE) // Y = 494
|
||||||
|
|
||||||
|
// GSCOL: Color 1 = Blue
|
||||||
|
out.write(GocaConstants.G_GSCOL)
|
||||||
|
out.write(0x01)
|
||||||
|
|
||||||
|
// G_GBIMGC (0x91): Begin Image Current Position: len=6, flags=0, w=80, h=80
|
||||||
|
out.write(GocaConstants.G_GBIMGC)
|
||||||
|
out.write(0x06)
|
||||||
|
out.write(0x00); out.write(0x00)
|
||||||
|
out.write(0x00); out.write(0x50) // w = 80
|
||||||
|
out.write(0x00); out.write(0x50) // h = 80
|
||||||
|
|
||||||
|
// G_GIMD (0x92): 10 bytes scanline
|
||||||
|
out.write(GocaConstants.G_GIMD)
|
||||||
|
out.write(0x0A)
|
||||||
|
out.write(byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()))
|
||||||
|
|
||||||
|
// G_GEIMG (0x93): End Image with 2-byte operand
|
||||||
|
out.write(GocaConstants.G_GEIMG)
|
||||||
|
out.write(0x02)
|
||||||
|
out.write(0x00); out.write(0x00)
|
||||||
|
|
||||||
|
// Second image tile: Color 2 = Red
|
||||||
|
out.write(GocaConstants.G_GSCOL)
|
||||||
|
out.write(0x02)
|
||||||
|
|
||||||
|
out.write(GocaConstants.G_GBIMGC)
|
||||||
|
out.write(0x06)
|
||||||
|
out.write(0x00); out.write(0x00)
|
||||||
|
out.write(0x00); out.write(0x50)
|
||||||
|
out.write(0x00); out.write(0x50)
|
||||||
|
|
||||||
|
out.write(GocaConstants.G_GIMD)
|
||||||
|
out.write(0x0A)
|
||||||
|
out.write(byteArrayOf(0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte()))
|
||||||
|
|
||||||
|
out.write(GocaConstants.G_GEIMG)
|
||||||
|
out.write(0x02)
|
||||||
|
out.write(0x00); out.write(0x00)
|
||||||
|
|
||||||
|
val stream = out.toByteArray()
|
||||||
|
decoder.decodeStream(stream, 0, stream.size)
|
||||||
|
|
||||||
|
assertTrue(plane.hasContent())
|
||||||
|
val mappedX = plane.mapX(377)
|
||||||
|
val mappedY = plane.mapY(494)
|
||||||
|
assertEquals(897, mappedX)
|
||||||
|
assertEquals(64, mappedY)
|
||||||
|
|
||||||
|
val rgb = plane.rgbBuffer
|
||||||
|
assertNotNull(rgb)
|
||||||
|
val pixelAtImg = rgb[mappedY * 1040 + mappedX]
|
||||||
|
assertNotEquals(0, pixelAtImg)
|
||||||
|
|
||||||
|
// Verify grid metrics and UI scaling calculation against IMON canvas
|
||||||
|
val metrics = calculateGridMetrics(1080f, 1920f, 80, 43)
|
||||||
|
assertTrue(metrics.gridWidth > 0f)
|
||||||
|
assertTrue(metrics.gridHeight > 0f)
|
||||||
|
|
||||||
|
// Touch scaling test: touch in the middle of grid maps accurately to plane coordinates
|
||||||
|
val touchScreenX = metrics.offsetX + metrics.gridWidth / 2f
|
||||||
|
val touchScreenY = metrics.offsetY + metrics.gridHeight / 2f
|
||||||
|
val planeX = (((touchScreenX - metrics.offsetX) * plane.canvasWidth) / metrics.gridWidth).toInt()
|
||||||
|
val planeY = (((touchScreenY - metrics.offsetY) * plane.canvasHeight) / metrics.gridHeight).toInt()
|
||||||
|
assertEquals(plane.canvasWidth / 2, planeX)
|
||||||
|
assertEquals(plane.canvasHeight / 2, planeY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package haus.nightmare.a3270
|
||||||
|
|
||||||
|
import haus.nightmare.a3270.storage.AppSettings
|
||||||
|
import haus.nightmare.a3270.ui.HotspotEngine
|
||||||
|
import haus.nightmare.a3270.ui.HotspotEngine.HotspotType
|
||||||
|
import haus.nightmare.lib3270j.TerminalModel
|
||||||
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||||
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||||
|
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class HotspotAndHistoryFeaturesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHotspotUrlDetection() {
|
||||||
|
val engine = HotspotEngine()
|
||||||
|
val text = "Visit https://ibm.com/support or http://example.org:8080/test for help"
|
||||||
|
val hotspots = engine.scanText(text, rows = 1, cols = text.length)
|
||||||
|
|
||||||
|
val urls = hotspots.filter { it.type == HotspotType.URL }
|
||||||
|
assertEquals(2, urls.size)
|
||||||
|
assertEquals("https://ibm.com/support", urls[0].action)
|
||||||
|
assertEquals("http://example.org:8080/test", urls[1].action)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHotspotPfAndPaKeyDetection() {
|
||||||
|
val engine = HotspotEngine()
|
||||||
|
val text = "Press PF3 to Exit, PF12 to Cancel, F1 for Help, and PA1 for Attention"
|
||||||
|
val hotspots = engine.scanText(text, rows = 1, cols = text.length)
|
||||||
|
|
||||||
|
val pfHotspots = hotspots.filter { it.type == HotspotType.PF_KEY }
|
||||||
|
assertTrue(pfHotspots.any { it.action == "[pf3]" && it.text.equals("PF3", ignoreCase = true) })
|
||||||
|
assertTrue(pfHotspots.any { it.action == "[pf12]" && it.text.equals("PF12", ignoreCase = true) })
|
||||||
|
assertTrue(pfHotspots.any { it.action == "[pf1]" && it.text.equals("F1", ignoreCase = true) })
|
||||||
|
assertTrue(pfHotspots.any { it.action == "[pa1]" && it.text.equals("PA1", ignoreCase = true) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHotspotMenuChoiceAndEnterDetection() {
|
||||||
|
val engine = HotspotEngine()
|
||||||
|
val text = "1. Logon\n2. Logoff\n===> 3\n[Enter] to submit"
|
||||||
|
val hotspots = engine.scanText(text, rows = 4, cols = 30)
|
||||||
|
|
||||||
|
val menuChoices = hotspots.filter { it.type == HotspotType.MENU_CHOICE }
|
||||||
|
assertTrue(menuChoices.any { it.action == "1[enter]" })
|
||||||
|
assertTrue(menuChoices.any { it.action == "2[enter]" })
|
||||||
|
assertTrue(menuChoices.any { it.action == "3[enter]" })
|
||||||
|
|
||||||
|
val enterHotspots = hotspots.filter { it.type == HotspotType.ENTER }
|
||||||
|
assertTrue(enterHotspots.any { it.action == "[enter]" })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHotspotHitTesting() {
|
||||||
|
val engine = HotspotEngine()
|
||||||
|
val text = "Row 0: PF7 Backward PF8 Forward "
|
||||||
|
val hotspots = engine.scanText(text, rows = 1, cols = text.length)
|
||||||
|
|
||||||
|
val pf7 = hotspots.find { it.action == "[pf7]" }
|
||||||
|
assertNotNull(pf7)
|
||||||
|
val hit = engine.findHotspotAt(hotspots, 0, pf7!!.startCol + 1)
|
||||||
|
assertNotNull(hit)
|
||||||
|
assertEquals("[pf7]", hit?.action)
|
||||||
|
|
||||||
|
// Pointing outside any hotspot returns null
|
||||||
|
val miss = engine.findHotspotAt(hotspots, 0, 0)
|
||||||
|
assertNull(miss)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHotspotCategoryDisabling() {
|
||||||
|
val engine = HotspotEngine()
|
||||||
|
val text = "Visit https://example.com or press PF3 or choose 1. Option"
|
||||||
|
|
||||||
|
// Master toggle disabled
|
||||||
|
val disabledScan = engine.scanText(text, rows = 1, cols = text.length, enabled = false)
|
||||||
|
assertTrue(disabledScan.isEmpty())
|
||||||
|
|
||||||
|
// Disable URLs only
|
||||||
|
engine.urlEnabled = false
|
||||||
|
engine.pfKeysEnabled = true
|
||||||
|
engine.menuChoicesEnabled = true
|
||||||
|
val noUrls = engine.scanText(text, rows = 1, cols = text.length, enabled = true)
|
||||||
|
assertTrue(noUrls.none { it.type == HotspotType.URL })
|
||||||
|
assertTrue(noUrls.any { it.type == HotspotType.PF_KEY })
|
||||||
|
|
||||||
|
// Disable PF keys only
|
||||||
|
engine.urlEnabled = true
|
||||||
|
engine.pfKeysEnabled = false
|
||||||
|
val noPfs = engine.scanText(text, rows = 1, cols = text.length, enabled = true)
|
||||||
|
assertTrue(noPfs.none { it.type == HotspotType.PF_KEY })
|
||||||
|
assertTrue(noPfs.any { it.type == HotspotType.URL })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testScreenHistoryBufferCapturesSnapshots() {
|
||||||
|
val translator = EbcdicTranslator()
|
||||||
|
val screen = ScreenBuffer(TerminalModel.IBM_3279_2, translator)
|
||||||
|
val history = screen.historyBuffer
|
||||||
|
|
||||||
|
assertNotNull(history)
|
||||||
|
assertEquals(0, history.size())
|
||||||
|
|
||||||
|
// Write first screen
|
||||||
|
screen.setCursorPosition(0, 0)
|
||||||
|
screen.setChar(0, 0, 'A')
|
||||||
|
screen.setChar(0, 1, 'B')
|
||||||
|
screen.setChar(0, 2, 'C')
|
||||||
|
history.snapshot(screen)
|
||||||
|
|
||||||
|
assertEquals(1, history.size())
|
||||||
|
val snap0 = history.get(0)
|
||||||
|
assertNotNull(snap0)
|
||||||
|
assertEquals('A', snap0?.getChar(0, 0))
|
||||||
|
assertEquals('B', snap0?.getChar(0, 1))
|
||||||
|
assertEquals('C', snap0?.getChar(0, 2))
|
||||||
|
|
||||||
|
// Consecutive identical snapshot is suppressed
|
||||||
|
history.snapshot(screen)
|
||||||
|
assertEquals(1, history.size())
|
||||||
|
|
||||||
|
// Modify screen and take second snapshot
|
||||||
|
screen.setChar(0, 0, 'X')
|
||||||
|
history.snapshot(screen)
|
||||||
|
assertEquals(2, history.size())
|
||||||
|
assertEquals('X', history.get(1)?.getChar(0, 0))
|
||||||
|
|
||||||
|
// Verify table formatting
|
||||||
|
val formattedTable = snap0?.formattedTable
|
||||||
|
assertNotNull(formattedTable)
|
||||||
|
assertTrue(formattedTable!!.startsWith("ABC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFieldOutliningConstantsAndAttribute() {
|
||||||
|
assertEquals(0x01.toByte(), ExtendedAttribute.OL_UNDERLINE)
|
||||||
|
assertEquals(0x02.toByte(), ExtendedAttribute.OL_RIGHT)
|
||||||
|
assertEquals(0x04.toByte(), ExtendedAttribute.OL_OVERLINE)
|
||||||
|
assertEquals(0x08.toByte(), ExtendedAttribute.OL_LEFT)
|
||||||
|
assertEquals(0x0F.toByte(), ExtendedAttribute.OL_BOX)
|
||||||
|
|
||||||
|
val ea = ExtendedAttribute()
|
||||||
|
ea.ol = ExtendedAttribute.OL_BOX
|
||||||
|
assertEquals(ExtendedAttribute.OL_BOX, ea.ol)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAppSettingsIniRoundtripWithHotspotsAndSelectionPreferences() {
|
||||||
|
// Check that default for hotspots is false (disabled for touch screens)
|
||||||
|
val testIni = """
|
||||||
|
[behavior]
|
||||||
|
hotspots_enabled=false
|
||||||
|
trim_rect_sizing_handles=true
|
||||||
|
trim_rect_remain_after_edit=true
|
||||||
|
paste_word_break=false
|
||||||
|
|
||||||
|
[hotspots]
|
||||||
|
url=true
|
||||||
|
pf_keys=true
|
||||||
|
menu_choices=false
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
var hsEnabled: Boolean? = null
|
||||||
|
var trimHandles: Boolean? = null
|
||||||
|
var trimRemain: Boolean? = null
|
||||||
|
var wordBreak: Boolean? = null
|
||||||
|
var hsUrl: Boolean? = null
|
||||||
|
var hsPf: Boolean? = null
|
||||||
|
var hsChoices: Boolean? = null
|
||||||
|
|
||||||
|
AppSettings.parseIni(java.io.StringReader(testIni)) { _, k, v ->
|
||||||
|
when (k) {
|
||||||
|
"hotspots_enabled" -> hsEnabled = v.toBoolean()
|
||||||
|
"trim_rect_sizing_handles" -> trimHandles = v.toBoolean()
|
||||||
|
"trim_rect_remain_after_edit" -> trimRemain = v.toBoolean()
|
||||||
|
"paste_word_break" -> wordBreak = v.toBoolean()
|
||||||
|
"hotspots_url_enabled", "url" -> hsUrl = v.toBoolean()
|
||||||
|
"hotspots_pf_enabled", "pf_keys" -> hsPf = v.toBoolean()
|
||||||
|
"hotspots_menu_choices_enabled", "menu_choices" -> hsChoices = v.toBoolean()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(false, hsEnabled)
|
||||||
|
assertEquals(true, trimHandles)
|
||||||
|
assertEquals(true, trimRemain)
|
||||||
|
assertEquals(false, wordBreak)
|
||||||
|
assertEquals(true, hsUrl)
|
||||||
|
assertEquals(true, hsPf)
|
||||||
|
assertEquals(false, hsChoices)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user