Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b81a244bb9
|
|||
|
351379b5b3
|
|||
|
eba1406cbb
|
|||
|
88e3ffcbe9
|
|||
|
f389e626ea
|
|||
|
0eaab32e2f
|
|||
|
da289730db
|
|||
|
f77f51474c
|
|||
|
e82d01003b
|
@@ -41,6 +41,12 @@ jobs:
|
||||
run: |
|
||||
chmod +x ./gradlew ./build.sh
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
./gradlew testDebugUnitTest --no-daemon
|
||||
env:
|
||||
J3270_DIR: ${{ github.workspace }}/j3270
|
||||
|
||||
- name: Build Android APK
|
||||
run: |
|
||||
./gradlew assembleDebug --no-daemon
|
||||
|
||||
@@ -9,3 +9,5 @@ captures/
|
||||
.cxx
|
||||
*.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
|
||||
|
||||
- **Modern Jetpack Compose UI**: Responsive, touch-optimized 3270 terminal screen rendering.
|
||||
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption via `lib3270j`.
|
||||
- **On-Screen Function Key Bar**: Quick-access touch bar for PF1–PF24, PA1–PA3, Enter, Clear, Reset, and Attn keys.
|
||||
- **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.
|
||||
- **OIA Status Line**: Real-time Operator Information Area displaying connection state, keyboard lock, insert mode, cursor row/col, and LU name.
|
||||
- **Host Profile Storage**: Save and manage multiple mainframe connection profiles with custom ports, models, and SSL/TLS options.
|
||||
- **Background Session Persistence**: Android foreground service keeps your terminal session alive when multitasking.
|
||||
- **Terminal Models**:
|
||||
- Model 2 (24x80)
|
||||
- Model 3 (32x80)
|
||||
- Model 4 (43x80)
|
||||
- Model 5 (27x132)
|
||||
### 🖥️ Terminal Emulation & Protocol
|
||||
- **RFC 2355 TN3270 & TN3270E Engine**: Robust state machine, Option Negotiation, Device-Type Query, and LU name specification powered by `lib3270j`.
|
||||
- **Standard & Dynamic Terminal Models**:
|
||||
- Model 2 (24 × 80)
|
||||
- Model 3 (32 × 80)
|
||||
- Model 4 (43 × 80)
|
||||
- Model 5 (27 × 132)
|
||||
- **Dynamic Screen Dimensions**: Configurable custom geometry (rows & columns up to 255 × 255).
|
||||
- **EBCDIC Code Pages**: Full character translation across international code pages (037, 1047, 500, etc.).
|
||||
|
||||
### 📊 Graphics & Peripherals
|
||||
- **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
|
||||
- **Java Development Kit (JDK)**: JDK 17 or JDK 21
|
||||
- **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
|
||||
For the easiest setup, clone both `j3270` and `a3270` into the same parent folder:
|
||||
|
||||
@@ -45,14 +45,19 @@ if [ ! -d "$J3270_PATH/lib3270j" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building debug APK..."
|
||||
./gradlew assembleDebug
|
||||
if [ $# -gt 0 ]; then
|
||||
echo "Running: ./gradlew $@"
|
||||
./gradlew "$@"
|
||||
else
|
||||
echo "Building debug APK..."
|
||||
./gradlew assembleDebug --no-daemon
|
||||
|
||||
APK_PATH="$SCRIPT_DIR/build/outputs/apk/debug/a3270-debug.apk"
|
||||
if [ -f "$APK_PATH" ]; then
|
||||
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"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -115,6 +115,26 @@ esac
|
||||
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.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.content.Context
|
||||
import android.view.KeyCharacterMap
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||
import haus.nightmare.a3270.service.TerminalService
|
||||
import haus.nightmare.a3270.storage.AppSettings
|
||||
import haus.nightmare.a3270.storage.HostStorage
|
||||
import haus.nightmare.a3270.ui.ConnectDialog
|
||||
import haus.nightmare.a3270.ui.FileTransferDialog
|
||||
@@ -51,19 +52,15 @@ class MainActivity : ComponentActivity() {
|
||||
rootLogger.removeHandler(h)
|
||||
}
|
||||
rootLogger.addHandler(object : java.util.logging.Handler() {
|
||||
override fun publish(record: java.util.logging.LogRecord?) {
|
||||
if (record == null) return
|
||||
override fun publish(record: java.util.logging.LogRecord) {
|
||||
val tag = "lib3270j-" + (record.loggerName?.substringAfterLast('.') ?: "core")
|
||||
val msg = record.message ?: ""
|
||||
val tag = "a3270-" + (record.loggerName?.substringAfterLast('.') ?: "lib3270j")
|
||||
when {
|
||||
record.level.intValue() >= java.util.logging.Level.SEVERE.intValue() ->
|
||||
android.util.Log.e(tag, msg, record.thrown)
|
||||
record.level.intValue() >= java.util.logging.Level.WARNING.intValue() ->
|
||||
android.util.Log.w(tag, msg, record.thrown)
|
||||
record.level.intValue() >= java.util.logging.Level.INFO.intValue() ->
|
||||
android.util.Log.i(tag, msg, record.thrown)
|
||||
else ->
|
||||
android.util.Log.d(tag, msg, record.thrown)
|
||||
when (record.level.intValue()) {
|
||||
java.util.logging.Level.SEVERE.intValue() -> android.util.Log.e(tag, msg, record.thrown)
|
||||
java.util.logging.Level.WARNING.intValue() -> android.util.Log.w(tag, msg, record.thrown)
|
||||
java.util.logging.Level.INFO.intValue() -> android.util.Log.i(tag, msg, record.thrown)
|
||||
java.util.logging.Level.CONFIG.intValue() -> android.util.Log.d(tag, msg, record.thrown)
|
||||
else -> android.util.Log.v(tag, msg, record.thrown)
|
||||
}
|
||||
}
|
||||
override fun flush() {}
|
||||
@@ -74,6 +71,8 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private val viewModel: TerminalViewModel by viewModels()
|
||||
private val isShiftPressedState = mutableStateOf(false)
|
||||
private val isFindDialogOpenState = mutableStateOf(false)
|
||||
private val isFtDialogOpenState = mutableStateOf(false)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -88,17 +87,29 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
setContent {
|
||||
MaterialTheme(
|
||||
colorScheme = darkColorScheme(
|
||||
val uiTheme by viewModel.uiTheme.collectAsState()
|
||||
val colorScheme = if (uiTheme.equals("LIGHT", ignoreCase = true)) {
|
||||
lightColorScheme(
|
||||
background = Color(0xFFF2F2F4),
|
||||
surface = Color.White,
|
||||
primary = Color(0xFF1971C2)
|
||||
)
|
||||
} else {
|
||||
darkColorScheme(
|
||||
background = Color.Black,
|
||||
surface = Color(0xFF1E1E1E),
|
||||
primary = Color(0xFF339AF0)
|
||||
)
|
||||
) {
|
||||
}
|
||||
MaterialTheme(colorScheme = colorScheme) {
|
||||
MainScreen(
|
||||
viewModel = viewModel,
|
||||
isShiftPressed = isShiftPressedState.value,
|
||||
onClearShift = { isShiftPressedState.value = false }
|
||||
onClearShift = { isShiftPressedState.value = false },
|
||||
isFindDialogOpen = isFindDialogOpenState.value,
|
||||
onSetFindDialogOpen = { isFindDialogOpenState.value = it },
|
||||
isFtDialogOpen = isFtDialogOpenState.value,
|
||||
onSetFtDialogOpen = { isFtDialogOpenState.value = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -180,6 +191,29 @@ class MainActivity : ComponentActivity() {
|
||||
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)
|
||||
// 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) {
|
||||
val fNum = keyCode - KeyEvent.KEYCODE_F1 + 1
|
||||
val pfNum = if (shift || alt) fNum + 12 else fNum
|
||||
@@ -199,6 +233,14 @@ class MainActivity : ComponentActivity() {
|
||||
// 2. Ctrl Shortcuts
|
||||
if (ctrl) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_F -> {
|
||||
isFindDialogOpenState.value = true
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_D -> {
|
||||
viewModel.disconnect()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_V -> {
|
||||
pasteFromClipboard()
|
||||
return true
|
||||
@@ -246,9 +288,13 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Alt Shortcuts (PA keys, Clear, Reset)
|
||||
// 3. Alt Shortcuts (PA keys, Clear, Reset, Enter, Erase, Attn, SysReq, CursorSelect, Ruler, FT)
|
||||
if (alt) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
|
||||
viewModel.sendAid(AID_ENTER)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_1 -> {
|
||||
viewModel.sendAid(AID_PA1)
|
||||
return true
|
||||
@@ -261,7 +307,7 @@ class MainActivity : ComponentActivity() {
|
||||
viewModel.sendAid(AID_PA3)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_C -> {
|
||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_K -> {
|
||||
viewModel.sendAid(AID_CLEAR)
|
||||
return true
|
||||
}
|
||||
@@ -269,12 +315,34 @@ class MainActivity : ComponentActivity() {
|
||||
viewModel.resetKeyboard()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_D -> {
|
||||
viewModel.deleteWord()
|
||||
KeyEvent.KEYCODE_E -> {
|
||||
viewModel.eraseInput()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_E -> {
|
||||
viewModel.fieldEnd()
|
||||
KeyEvent.KEYCODE_A -> {
|
||||
viewModel.attn()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_S -> {
|
||||
viewModel.sysReq()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_Q -> {
|
||||
viewModel.cursorSelect()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_X -> {
|
||||
viewModel.toggleCrosshairRuler()
|
||||
val isRuler = viewModel.isCrosshairRuler.value
|
||||
Toast.makeText(this, "Crosshair Ruler: " + (if (isRuler) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_T -> {
|
||||
isFtDialogOpenState.value = true
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_D -> {
|
||||
viewModel.deleteWord()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_L -> {
|
||||
@@ -294,6 +362,10 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
// 5. Standard Navigation & Special Keys
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_INSERT -> {
|
||||
viewModel.toggleInsert()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
viewModel.cursorLeft()
|
||||
return true
|
||||
@@ -424,7 +496,11 @@ class MainActivity : ComponentActivity() {
|
||||
fun MainScreen(
|
||||
viewModel: TerminalViewModel,
|
||||
isShiftPressed: Boolean = false,
|
||||
onClearShift: () -> Unit = {}
|
||||
onClearShift: () -> Unit = {},
|
||||
isFindDialogOpen: Boolean = false,
|
||||
onSetFindDialogOpen: (Boolean) -> Unit = {},
|
||||
isFtDialogOpen: Boolean = false,
|
||||
onSetFtDialogOpen: (Boolean) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val connectionState by viewModel.connectionState.collectAsState()
|
||||
@@ -449,6 +525,23 @@ fun MainScreen(
|
||||
val ftState by viewModel.ftState.collectAsState()
|
||||
val isLightPenMode by viewModel.isLightPenMode.collectAsState()
|
||||
|
||||
val isCrosshairRuler by viewModel.isCrosshairRuler.collectAsState()
|
||||
val cursorStyle by viewModel.cursorStyle.collectAsState()
|
||||
val blockSelectMode by viewModel.blockSelectMode.collectAsState()
|
||||
val uiTheme by viewModel.uiTheme.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 isNumericField = remember(screenBuffer, cursorAddr, screenVersion) {
|
||||
if (screenBuffer != null && screenBuffer?.isFormatted == true) {
|
||||
val fa = screenBuffer?.getFieldAttributeAt(cursorAddr) ?: 0.toByte()
|
||||
(fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0
|
||||
} else false
|
||||
}
|
||||
|
||||
var showConnectDialog by remember { mutableStateOf(false) }
|
||||
var showFtDialog by remember { mutableStateOf(false) }
|
||||
var showSettingsDialog by remember { mutableStateOf(false) }
|
||||
@@ -460,6 +553,22 @@ fun MainScreen(
|
||||
|
||||
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
||||
|
||||
LaunchedEffect(isFindDialogOpen) {
|
||||
if (isFindDialogOpen) {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showFindDialog = true
|
||||
onSetFindDialogOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isFtDialogOpen) {
|
||||
if (isFtDialogOpen) {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showFtDialog = true
|
||||
onSetFtDialogOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
@@ -476,12 +585,17 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val startupBehavior = AppSettings.getStartupBehavior(context)
|
||||
when (startupBehavior) {
|
||||
AppSettings.StartupBehavior.AUTO_CONNECT -> {
|
||||
val autoHost = HostStorage.getAutoConnectHost(context)
|
||||
if (autoHost != null && !connectionState.isConnected()) {
|
||||
viewModel.connect(
|
||||
autoHost.host,
|
||||
autoHost.port,
|
||||
autoHost.model,
|
||||
autoHost.dynamicRows,
|
||||
autoHost.dynamicCols,
|
||||
autoHost.luName,
|
||||
autoHost.hostType,
|
||||
autoHost.useTls,
|
||||
@@ -497,6 +611,16 @@ fun MainScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
AppSettings.StartupBehavior.SHOW_CONNECT -> {
|
||||
if (!connectionState.isConnected()) {
|
||||
showConnectDialog = true
|
||||
}
|
||||
}
|
||||
AppSettings.StartupBehavior.DO_NOTHING -> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(connectionState) {
|
||||
if (connectionState.isConnected()) {
|
||||
@@ -532,6 +656,14 @@ fun MainScreen(
|
||||
blinkCursor = cursorBlink,
|
||||
searchHighlightAddr = searchHighlightAddr,
|
||||
searchHighlightLen = searchHighlightLen,
|
||||
crosshairRuler = isCrosshairRuler,
|
||||
cursorStyle = cursorStyle,
|
||||
isInsertMode = isInsertMode,
|
||||
blockSelectMode = blockSelectMode,
|
||||
fourColorOverride = fourColorOverride,
|
||||
onPasteLineWrap = { text, wrapCol, wordWrap ->
|
||||
viewModel.pasteLineWrap(text, wrapCol, wordWrap)
|
||||
},
|
||||
onTapAddress = { addr ->
|
||||
viewModel.setCursor(addr)
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
@@ -584,18 +716,56 @@ fun MainScreen(
|
||||
connectionState = connectionState,
|
||||
isShiftPressed = isShiftPressed,
|
||||
isLightPenMode = isLightPenMode,
|
||||
isDocMode = isDocMode,
|
||||
isWordWrap = isWordWrap,
|
||||
isAplMode = isAplMode,
|
||||
isInsertMode = isInsertMode,
|
||||
fourColorOverride = fourColorOverride,
|
||||
hapticFeedbackEnabled = hapticFeedback,
|
||||
onClearShift = onClearShift,
|
||||
onConnectClick = { showConnectDialog = true },
|
||||
onConnectClick = {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showConnectDialog = true
|
||||
},
|
||||
onDisconnectClick = { viewModel.disconnect() },
|
||||
onFtClick = { showFtDialog = true },
|
||||
onFieldInspectorClick = { showFieldInspectorDialog = true },
|
||||
onFindClick = { showFindDialog = true },
|
||||
onScreenExportClick = { showScreenExportDialog = true },
|
||||
onScriptClick = { showScriptDialog = true },
|
||||
onPrinterSessionClick = { showPrinterSessionDialog = true },
|
||||
onFtClick = {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showFtDialog = true
|
||||
},
|
||||
onFieldInspectorClick = {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showFieldInspectorDialog = true
|
||||
},
|
||||
onFindClick = {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showFindDialog = true
|
||||
},
|
||||
onScreenExportClick = {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showScreenExportDialog = true
|
||||
},
|
||||
onScriptClick = {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showScriptDialog = true
|
||||
},
|
||||
onPrinterSessionClick = {
|
||||
terminalInputViewRef?.hideSoftKeyboard()
|
||||
showPrinterSessionDialog = true
|
||||
},
|
||||
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 ->
|
||||
viewModel.sendAid(aid)
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
@@ -662,21 +832,32 @@ fun MainScreen(
|
||||
isTls = isTlsActive,
|
||||
isTlsVerified = isTlsVerified,
|
||||
graphicsMode = defaultGraphicsMode,
|
||||
codePage = codePage,
|
||||
isLightPenMode = isLightPenMode,
|
||||
isInsertMode = viewModel.getClient()?.inputProcessor?.isInsertMode ?: false,
|
||||
isInsertMode = isInsertMode,
|
||||
isDocMode = isDocMode,
|
||||
isWordWrap = isWordWrap,
|
||||
isAplMode = isAplMode,
|
||||
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
||||
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: ""
|
||||
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
|
||||
isNumericField = isNumericField,
|
||||
uiTheme = uiTheme,
|
||||
onToggleDocMode = { viewModel.toggleDocMode() },
|
||||
onToggleWordWrap = { viewModel.toggleWordWrap() },
|
||||
onToggleAplMode = { viewModel.toggleAplMode() },
|
||||
onToggleInsert = { viewModel.toggleInsert() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showConnectDialog) {
|
||||
ConnectDialog(
|
||||
onDismiss = { showConnectDialog = false },
|
||||
onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass ->
|
||||
onDismiss = {
|
||||
showConnectDialog = false
|
||||
viewModel.connect(host, port, model, 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 ->
|
||||
showConnectDialog = false
|
||||
viewModel.connect(host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
}
|
||||
)
|
||||
@@ -687,7 +868,10 @@ fun MainScreen(
|
||||
initialHostType = viewModel.activeHostType,
|
||||
ftProgressState = ftState,
|
||||
client = viewModel.getClient(),
|
||||
onDismiss = { showFtDialog = false },
|
||||
onDismiss = {
|
||||
showFtDialog = false
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
},
|
||||
onStartTransfer = { config ->
|
||||
viewModel.startFileTransfer(config) { error ->
|
||||
if (error != null) {
|
||||
@@ -709,10 +893,51 @@ fun MainScreen(
|
||||
initialVerifyCerts = verifyCerts,
|
||||
initialDefaultGraphicsMode = defaultGraphicsMode,
|
||||
initialDefaultCodePage = codePage,
|
||||
onDismiss = { showSettingsDialog = false },
|
||||
onSave = { mask, blink, haptic, verify, gfx, defaultCp ->
|
||||
initialUiTheme = uiTheme,
|
||||
initialCrosshairRuler = isCrosshairRuler,
|
||||
initialCursorStyle = cursorStyle,
|
||||
initialBlockSelectMode = blockSelectMode,
|
||||
initialStartupBehavior = AppSettings.getStartupBehavior(context).name,
|
||||
initialDynamicRows = AppSettings.getDynamicRows(context),
|
||||
initialDynamicCols = AppSettings.getDynamicCols(context),
|
||||
initialEntryAssistDocMode = isDocMode,
|
||||
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),
|
||||
onDismiss = {
|
||||
showSettingsDialog = false
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
},
|
||||
onSave = { mask, blink, haptic, verify, gfx, defaultCp, theme, crosshair, style, blockSel, startup, dynRows, dynCols,
|
||||
doc, wrap, startCol, endCol, bell, bellCol, tabs, resetIns, fourCol, numLk, autoSk ->
|
||||
showSettingsDialog = false
|
||||
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
|
||||
viewModel.setUiTheme(theme)
|
||||
viewModel.setCrosshairRuler(crosshair)
|
||||
viewModel.setCursorStyle(style)
|
||||
viewModel.setBlockSelectMode(blockSel)
|
||||
AppSettings.setStartupBehavior(context, AppSettings.StartupBehavior.valueOf(startup))
|
||||
AppSettings.setDynamicRows(context, dynRows)
|
||||
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)
|
||||
viewModel.reloadSettings()
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
}
|
||||
)
|
||||
@@ -722,7 +947,10 @@ fun MainScreen(
|
||||
FieldInspectorDialog(
|
||||
client = viewModel.getClient(),
|
||||
screenVersion = screenVersion,
|
||||
onDismiss = { showFieldInspectorDialog = false },
|
||||
onDismiss = {
|
||||
showFieldInspectorDialog = false
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
},
|
||||
onJumpToField = { addr ->
|
||||
viewModel.setCursor(addr)
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
@@ -733,7 +961,10 @@ fun MainScreen(
|
||||
if (showFindDialog) {
|
||||
FindDialog(
|
||||
cols = cols,
|
||||
onDismiss = { showFindDialog = false },
|
||||
onDismiss = {
|
||||
showFindDialog = false
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
},
|
||||
onFind = { query: String, matchCase: Boolean, forward: Boolean ->
|
||||
viewModel.find(query, matchCase, forward)
|
||||
},
|
||||
@@ -751,13 +982,19 @@ fun MainScreen(
|
||||
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
||||
graphicsPlane = viewModel.getClient()?.graphicsPlane,
|
||||
maskHidden = maskHiddenInput,
|
||||
onDismiss = { showScreenExportDialog = false }
|
||||
onDismiss = {
|
||||
showScreenExportDialog = false
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showScriptDialog) {
|
||||
ScriptDialog(
|
||||
onDismiss = { showScriptDialog = false },
|
||||
onDismiss = {
|
||||
showScriptDialog = false
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
},
|
||||
onExecuteScript = { script: String, onStatus: (String) -> Unit, onComplete: (String?) -> Unit ->
|
||||
viewModel.executeScript(script, onStatus, onComplete)
|
||||
}
|
||||
@@ -770,7 +1007,10 @@ fun MainScreen(
|
||||
initialHost = activeHost,
|
||||
initialCodePage = codePage,
|
||||
initialUseTls = isTlsActive,
|
||||
onDismiss = { showPrinterSessionDialog = false }
|
||||
onDismiss = {
|
||||
showPrinterSessionDialog = false
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ sealed interface TerminalInputAction {
|
||||
data object Dup : TerminalInputAction
|
||||
data object FieldMark : 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 LightPenSelect(val baddr: Int) : TerminalInputAction
|
||||
}
|
||||
@@ -146,6 +152,33 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _isLightPenMode = MutableStateFlow(false)
|
||||
val isLightPenMode: StateFlow<Boolean> = _isLightPenMode.asStateFlow()
|
||||
|
||||
private val _isCrosshairRuler = MutableStateFlow(AppSettings.isCrosshairRulerEnabled(application))
|
||||
val isCrosshairRuler: StateFlow<Boolean> = _isCrosshairRuler.asStateFlow()
|
||||
|
||||
private val _cursorStyle = MutableStateFlow(AppSettings.getCursorStyle(application))
|
||||
val cursorStyle: StateFlow<String> = _cursorStyle.asStateFlow()
|
||||
|
||||
private val _blockSelectMode = MutableStateFlow(AppSettings.isBlockSelectModeEnabled(application))
|
||||
val blockSelectMode: StateFlow<Boolean> = _blockSelectMode.asStateFlow()
|
||||
|
||||
private val _uiTheme = MutableStateFlow(AppSettings.getUiTheme(application))
|
||||
val uiTheme: StateFlow<String> = _uiTheme.asStateFlow()
|
||||
|
||||
private val _isInsertMode = MutableStateFlow(false)
|
||||
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()
|
||||
|
||||
fun toggleLightPen() {
|
||||
_isLightPenMode.value = !_isLightPenMode.value
|
||||
}
|
||||
@@ -154,6 +187,123 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
_isLightPenMode.value = enabled
|
||||
}
|
||||
|
||||
fun setCrosshairRuler(enabled: Boolean) {
|
||||
AppSettings.setCrosshairRulerEnabled(getApplication(), enabled)
|
||||
_isCrosshairRuler.value = enabled
|
||||
}
|
||||
|
||||
fun toggleCrosshairRuler() {
|
||||
setCrosshairRuler(!_isCrosshairRuler.value)
|
||||
}
|
||||
|
||||
fun setCursorStyle(style: String) {
|
||||
AppSettings.setCursorStyle(getApplication(), style)
|
||||
_cursorStyle.value = style
|
||||
}
|
||||
|
||||
fun setBlockSelectMode(enabled: Boolean) {
|
||||
AppSettings.setBlockSelectModeEnabled(getApplication(), enabled)
|
||||
_blockSelectMode.value = enabled
|
||||
}
|
||||
|
||||
fun setUiTheme(theme: String) {
|
||||
AppSettings.setUiTheme(getApplication(), 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)
|
||||
}
|
||||
_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() {
|
||||
val app = getApplication<Application>()
|
||||
_maskHiddenInput.value = AppSettings.isMaskHiddenInputEnabled(app)
|
||||
_cursorBlink.value = AppSettings.isCursorBlinkEnabled(app)
|
||||
_hapticFeedback.value = AppSettings.isHapticFeedbackEnabled(app)
|
||||
_verifyCerts.value = AppSettings.isVerifyCertsEnabled(app)
|
||||
_defaultGraphicsMode.value = AppSettings.getDefaultGraphicsMode(app)
|
||||
_codePage.value = AppSettings.getDefaultCodePage(app)
|
||||
_isCrosshairRuler.value = AppSettings.isCrosshairRulerEnabled(app)
|
||||
_cursorStyle.value = AppSettings.getCursorStyle(app)
|
||||
_blockSelectMode.value = AppSettings.isBlockSelectModeEnabled(app)
|
||||
_uiTheme.value = AppSettings.getUiTheme(app)
|
||||
_fourColorOverride.value = AppSettings.getFourColorOverride(app)
|
||||
applyModeSettings()
|
||||
}
|
||||
|
||||
fun pasteLineWrap(text: String, endCol: Int, wordWrap: Boolean) {
|
||||
val cur = client
|
||||
if (cur != null && cur.connectionState.isFullSession) {
|
||||
val curPos = cur.screenBuffer.cursorAddress
|
||||
cur.ps?.pasteLineWrap(text, curPos, endCol, wordWrap)
|
||||
_screenVersion.value = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
fun setSearchHighlight(addr: Int, len: Int) {
|
||||
_searchHighlightAddr.value = addr
|
||||
_searchHighlightLen.value = len
|
||||
@@ -352,6 +502,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
c.sendNVTChar('\u001B')
|
||||
} else {
|
||||
ip.reset()
|
||||
_isInsertMode.value = ip.isInsertMode
|
||||
}
|
||||
}
|
||||
is TerminalInputAction.WordLeft -> {
|
||||
@@ -374,8 +525,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
}
|
||||
is TerminalInputAction.DeleteWord -> {
|
||||
if (!isNvt) {
|
||||
ip.isKeyboardLocked = false
|
||||
if (isNvt) {
|
||||
c.sendNVTString("\u0017")
|
||||
} else {
|
||||
ip.processDeleteWord()
|
||||
}
|
||||
}
|
||||
@@ -385,13 +537,15 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
}
|
||||
is TerminalInputAction.Attn -> {
|
||||
if (!isNvt) {
|
||||
ip.attn()
|
||||
if (isNvt) {
|
||||
c.sendNVTChar('\u0003')
|
||||
} else {
|
||||
c.attn()
|
||||
}
|
||||
}
|
||||
is TerminalInputAction.SysReq -> {
|
||||
if (!isNvt) {
|
||||
ip.sysReq()
|
||||
c.sysReq()
|
||||
}
|
||||
}
|
||||
is TerminalInputAction.Dup -> {
|
||||
@@ -406,7 +560,48 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
is TerminalInputAction.ToggleInsert -> {
|
||||
if (!isNvt) {
|
||||
ip.processToggleInsert()
|
||||
c.toggleInsert()
|
||||
_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 -> {
|
||||
@@ -543,6 +738,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
host: String,
|
||||
port: Int = 23,
|
||||
modelNum: Int = 2,
|
||||
dynamicRows: Int = 62,
|
||||
dynamicCols: Int = 160,
|
||||
luName: String = "",
|
||||
hostType: String = "TSO",
|
||||
useTls: Boolean = false,
|
||||
@@ -577,6 +774,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
|
||||
val model = when (modelNum) {
|
||||
0 -> TerminalModel.IBM_DYNAMIC
|
||||
3 -> TerminalModel.IBM_3279_3
|
||||
4 -> TerminalModel.IBM_3279_4
|
||||
5 -> TerminalModel.IBM_3279_5
|
||||
@@ -591,18 +789,26 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
val globalVerify = AppSettings.isVerifyCertsEnabled(getApplication())
|
||||
val effectiveVerify = tlsVerifyCert && globalVerify
|
||||
val gMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(graphicsModeStr)
|
||||
val effectiveLu = if (luName.isNotBlank()) luName.trim() else (parsedConfig.luName ?: "").trim()
|
||||
|
||||
_isTlsActive.value = effectiveTls
|
||||
_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 {
|
||||
if (luName.isNotBlank()) {
|
||||
setLuName(luName)
|
||||
if (model == TerminalModel.IBM_DYNAMIC) {
|
||||
setDynamicDimensions(dynamicRows, dynamicCols)
|
||||
}
|
||||
if (effectiveLu.isNotBlank()) {
|
||||
setLuName(effectiveLu)
|
||||
}
|
||||
isUseTls = effectiveTls
|
||||
isTlsVerifyCert = effectiveVerify
|
||||
isTn3270eEnabled = effectiveTn3270e
|
||||
isAutoSysUnlock = true
|
||||
graphicsMode = gMode
|
||||
codePage = codePageStr
|
||||
|
||||
@@ -696,6 +902,20 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
_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 {
|
||||
override fun onScreenUpdated() {
|
||||
val buf = newClient.screenBuffer
|
||||
@@ -706,6 +926,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
_cursorAddress.value = buf.cursorAddress
|
||||
}
|
||||
_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()
|
||||
|
||||
// Drive CUT mode file transfers if active
|
||||
@@ -731,6 +955,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
|
||||
override fun onSoundAlarm() {
|
||||
log.fine("Sound Alarm")
|
||||
playBellSound()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -738,6 +963,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
_cols.value = model.defaultCols
|
||||
_screenBuffer.value = newClient.screenBuffer
|
||||
|
||||
applyModeSettings()
|
||||
|
||||
newClient.connect()
|
||||
} catch (e: Throwable) {
|
||||
log.severe("Failed to connect: ${e.message}")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,13 @@ package haus.nightmare.a3270.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import java.io.BufferedReader
|
||||
import java.io.PrintWriter
|
||||
import java.io.Reader
|
||||
import java.io.Writer
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object AppSettings {
|
||||
private const val PREFS_NAME = "a3270_settings"
|
||||
@@ -13,6 +20,34 @@ object AppSettings {
|
||||
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
|
||||
private const val KEY_DEFAULT_CODE_PAGE = "default_code_page"
|
||||
|
||||
// New settings aligned with j3270
|
||||
private const val KEY_UI_THEME = "ui_theme"
|
||||
private const val KEY_CROSSHAIR_RULER = "crosshair_ruler"
|
||||
private const val KEY_CURSOR_STYLE = "cursor_style"
|
||||
private const val KEY_BLOCK_SELECT_MODE = "block_select_mode"
|
||||
private const val KEY_STARTUP_BEHAVIOR = "startup_behavior"
|
||||
private const val KEY_DYNAMIC_ROWS = "dynamic_rows"
|
||||
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"
|
||||
|
||||
enum class StartupBehavior {
|
||||
SHOW_CONNECT,
|
||||
DO_NOTHING,
|
||||
AUTO_CONNECT
|
||||
}
|
||||
|
||||
private fun getPrefs(context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
@@ -64,4 +99,302 @@ object AppSettings {
|
||||
fun setDefaultCodePage(context: Context, codePage: String) {
|
||||
getPrefs(context).edit().putString(KEY_DEFAULT_CODE_PAGE, codePage).apply()
|
||||
}
|
||||
|
||||
// ========== UI Theme ==========
|
||||
fun getUiTheme(context: Context): String {
|
||||
return getPrefs(context).getString(KEY_UI_THEME, "DARK") ?: "DARK"
|
||||
}
|
||||
|
||||
fun setUiTheme(context: Context, theme: String) {
|
||||
getPrefs(context).edit().putString(KEY_UI_THEME, if (theme.equals("LIGHT", ignoreCase = true)) "LIGHT" else "DARK").apply()
|
||||
}
|
||||
|
||||
// ========== Crosshair Ruler ==========
|
||||
fun isCrosshairRulerEnabled(context: Context): Boolean {
|
||||
return getPrefs(context).getBoolean(KEY_CROSSHAIR_RULER, false)
|
||||
}
|
||||
|
||||
fun setCrosshairRulerEnabled(context: Context, enabled: Boolean) {
|
||||
getPrefs(context).edit().putBoolean(KEY_CROSSHAIR_RULER, enabled).apply()
|
||||
}
|
||||
|
||||
// ========== Cursor Style ==========
|
||||
fun getCursorStyle(context: Context): String {
|
||||
return getPrefs(context).getString(KEY_CURSOR_STYLE, "BLOCK") ?: "BLOCK"
|
||||
}
|
||||
|
||||
fun setCursorStyle(context: Context, style: String) {
|
||||
val s = if (style.equals("UNDERLINE", ignoreCase = true)) "UNDERLINE" else "BLOCK"
|
||||
getPrefs(context).edit().putString(KEY_CURSOR_STYLE, s).apply()
|
||||
}
|
||||
|
||||
// ========== Block Select Mode ==========
|
||||
fun isBlockSelectModeEnabled(context: Context): Boolean {
|
||||
return getPrefs(context).getBoolean(KEY_BLOCK_SELECT_MODE, false)
|
||||
}
|
||||
|
||||
fun setBlockSelectModeEnabled(context: Context, enabled: Boolean) {
|
||||
getPrefs(context).edit().putBoolean(KEY_BLOCK_SELECT_MODE, enabled).apply()
|
||||
}
|
||||
|
||||
// ========== Startup Behavior ==========
|
||||
fun getStartupBehavior(context: Context): StartupBehavior {
|
||||
val s = getPrefs(context).getString(KEY_STARTUP_BEHAVIOR, StartupBehavior.SHOW_CONNECT.name)
|
||||
return try {
|
||||
StartupBehavior.valueOf(s ?: StartupBehavior.SHOW_CONNECT.name)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
StartupBehavior.SHOW_CONNECT
|
||||
}
|
||||
}
|
||||
|
||||
fun setStartupBehavior(context: Context, behavior: StartupBehavior) {
|
||||
getPrefs(context).edit().putString(KEY_STARTUP_BEHAVIOR, behavior.name).apply()
|
||||
}
|
||||
|
||||
// ========== Dynamic Screen Dimensions ==========
|
||||
fun getDynamicRows(context: Context): Int {
|
||||
return getPrefs(context).getInt(KEY_DYNAMIC_ROWS, 62).coerceIn(24, 255)
|
||||
}
|
||||
|
||||
fun setDynamicRows(context: Context, rows: Int) {
|
||||
getPrefs(context).edit().putInt(KEY_DYNAMIC_ROWS, rows.coerceIn(24, 255)).apply()
|
||||
}
|
||||
|
||||
fun getDynamicCols(context: Context): Int {
|
||||
return getPrefs(context).getInt(KEY_DYNAMIC_COLS, 160).coerceIn(80, 255)
|
||||
}
|
||||
|
||||
fun setDynamicCols(context: Context, cols: Int) {
|
||||
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()
|
||||
}
|
||||
|
||||
// ========== INI Config Export / Import ==========
|
||||
|
||||
fun exportToIni(context: Context, writer: Writer) {
|
||||
val p = PrintWriter(writer)
|
||||
val timeStamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
||||
p.println("; a3270 / j3270 Configuration File")
|
||||
p.println("; Exported on $timeStamp")
|
||||
p.println()
|
||||
|
||||
p.println("[appearance]")
|
||||
p.println("javaUiTheme = ${getUiTheme(context)}")
|
||||
p.println("crosshairRuler = ${isCrosshairRulerEnabled(context)}")
|
||||
p.println("cursorStyle = ${getCursorStyle(context)}")
|
||||
p.println()
|
||||
|
||||
p.println("[behavior]")
|
||||
p.println("startupBehavior = ${getStartupBehavior(context).name}")
|
||||
val autoHost = HostStorage.getAutoConnectHost(context)
|
||||
if (autoHost != null) {
|
||||
p.println("autoConnectHost = ${autoHost.host}")
|
||||
p.println("autoConnectPort = ${autoHost.port}")
|
||||
p.println("autoConnectTls = ${autoHost.useTls}")
|
||||
p.println("autoConnectVerifyCert = ${autoHost.tlsVerifyCert}")
|
||||
p.println("autoConnectTn3270e = ${autoHost.tn3270e}")
|
||||
}
|
||||
p.println("codePage = ${getDefaultCodePage(context)}")
|
||||
p.println("dynamicRows = ${getDynamicRows(context)}")
|
||||
p.println("dynamicCols = ${getDynamicCols(context)}")
|
||||
p.println("blockSelectMode = ${isBlockSelectModeEnabled(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("[graphics]")
|
||||
p.println("graphicsMode = ${getDefaultGraphicsMode(context)}")
|
||||
p.println()
|
||||
|
||||
p.flush()
|
||||
}
|
||||
|
||||
fun loadFromIni(context: Context, reader: Reader) {
|
||||
val bufferedReader = BufferedReader(reader)
|
||||
var currentSection = ""
|
||||
|
||||
bufferedReader.forEachLine { rawLine ->
|
||||
val line = rawLine.trim()
|
||||
if (line.isNotEmpty() && !line.startsWith(";") && !line.startsWith("#")) {
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
currentSection = line.substring(1, line.length - 1).lowercase(Locale.US).trim()
|
||||
} else {
|
||||
val eq = line.indexOf('=')
|
||||
if (eq >= 0) {
|
||||
val key = line.substring(0, eq).trim()
|
||||
val value = line.substring(eq + 1).trim()
|
||||
applyConfigEntry(context, currentSection, key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyConfigEntry(context: Context, section: String, key: String, value: String) {
|
||||
when (section) {
|
||||
"appearance" -> {
|
||||
when (key.lowercase(Locale.US)) {
|
||||
"javauitheme", "theme", "uitheme" -> setUiTheme(context, value)
|
||||
"crosshairruler", "ruler" -> setCrosshairRulerEnabled(context, value.toBoolean())
|
||||
"cursorstyle" -> setCursorStyle(context, value)
|
||||
}
|
||||
}
|
||||
"behavior", "connection" -> {
|
||||
when (key.lowercase(Locale.US)) {
|
||||
"startupbehavior" -> {
|
||||
try {
|
||||
setStartupBehavior(context, StartupBehavior.valueOf(value.uppercase(Locale.US)))
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
"codepage", "charset" -> setDefaultCodePage(context, value)
|
||||
"dynamicrows", "dynamic_rows" -> value.toIntOrNull()?.let { setDynamicRows(context, it) }
|
||||
"dynamiccols", "dynamic_cols" -> value.toIntOrNull()?.let { setDynamicCols(context, it) }
|
||||
"blockselectmode" -> setBlockSelectModeEnabled(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" -> {
|
||||
if (key.equals("graphicsmode", ignoreCase = true) || key.equals("mode", ignoreCase = true)) {
|
||||
setDefaultGraphicsMode(context, value.uppercase(Locale.US))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ data class SavedHost(
|
||||
val host: String,
|
||||
val port: Int = 23,
|
||||
val model: Int = 2,
|
||||
val dynamicRows: Int = 62,
|
||||
val dynamicCols: Int = 160,
|
||||
val luName: String = "",
|
||||
val autoConnect: Boolean = false,
|
||||
val hostType: String = "TSO",
|
||||
@@ -33,6 +35,8 @@ data class SavedHost(
|
||||
put("host", host)
|
||||
put("port", port)
|
||||
put("model", model)
|
||||
put("dynamicRows", dynamicRows)
|
||||
put("dynamicCols", dynamicCols)
|
||||
put("luName", luName)
|
||||
put("autoConnect", autoConnect)
|
||||
put("hostType", hostType)
|
||||
@@ -57,6 +61,8 @@ data class SavedHost(
|
||||
host = json.optString("host", "127.0.0.1"),
|
||||
port = json.optInt("port", 23),
|
||||
model = json.optInt("model", 2),
|
||||
dynamicRows = json.optInt("dynamicRows", 62),
|
||||
dynamicCols = json.optInt("dynamicCols", 160),
|
||||
luName = json.optString("luName", ""),
|
||||
autoConnect = json.optBoolean("autoConnect", false),
|
||||
hostType = json.optString("hostType", "TSO"),
|
||||
|
||||
@@ -42,6 +42,8 @@ fun ConnectDialog(
|
||||
host: String,
|
||||
port: Int,
|
||||
model: Int,
|
||||
dynamicRows: Int,
|
||||
dynamicCols: Int,
|
||||
luName: String,
|
||||
hostType: String,
|
||||
useTls: Boolean,
|
||||
@@ -61,9 +63,11 @@ fun ConnectDialog(
|
||||
var selectedHostId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var profileName by remember { mutableStateOf("") }
|
||||
var host by remember { mutableStateOf("127.0.0.1") }
|
||||
var portStr by remember { mutableStateOf("23") }
|
||||
var host by remember { mutableStateOf("") }
|
||||
var portStr by remember { mutableStateOf("") }
|
||||
var modelNum by remember { mutableIntStateOf(2) }
|
||||
var dynamicRowsStr by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()) }
|
||||
var dynamicColsStr by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()) }
|
||||
var luName by remember { mutableStateOf("") }
|
||||
var autoConnect by remember { mutableStateOf(false) }
|
||||
var hostType by remember { mutableStateOf("TSO") }
|
||||
@@ -85,6 +89,8 @@ fun ConnectDialog(
|
||||
host = saved.host
|
||||
portStr = saved.port.toString()
|
||||
modelNum = saved.model
|
||||
dynamicRowsStr = saved.dynamicRows.toString()
|
||||
dynamicColsStr = saved.dynamicCols.toString()
|
||||
luName = saved.luName
|
||||
autoConnect = saved.autoConnect
|
||||
hostType = saved.hostType
|
||||
@@ -104,9 +110,11 @@ fun ConnectDialog(
|
||||
fun clearFields() {
|
||||
selectedHostId = null
|
||||
profileName = ""
|
||||
host = "127.0.0.1"
|
||||
portStr = "23"
|
||||
host = ""
|
||||
portStr = ""
|
||||
modelNum = 2
|
||||
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
|
||||
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
||||
luName = ""
|
||||
autoConnect = false
|
||||
hostType = "TSO"
|
||||
@@ -126,14 +134,21 @@ fun ConnectDialog(
|
||||
fun saveCurrentProfile(): SavedHost? {
|
||||
val port = portStr.trim().toIntOrNull() ?: if (useTls) 992 else 23
|
||||
val trimmedHost = host.trim()
|
||||
if (trimmedHost.isBlank()) return null
|
||||
if (trimmedHost.isBlank()) {
|
||||
android.widget.Toast.makeText(context, "Please enter a Host / IP Address", android.widget.Toast.LENGTH_SHORT).show()
|
||||
return null
|
||||
}
|
||||
val nameToSave = profileName.trim().ifBlank { "$trimmedHost:$port" }
|
||||
val dynRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
|
||||
val dynCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
|
||||
val hostToSave = SavedHost(
|
||||
id = selectedHostId ?: java.util.UUID.randomUUID().toString(),
|
||||
name = nameToSave,
|
||||
host = trimmedHost,
|
||||
port = port,
|
||||
model = modelNum,
|
||||
dynamicRows = dynRows,
|
||||
dynamicCols = dynCols,
|
||||
luName = luName.trim(),
|
||||
autoConnect = autoConnect,
|
||||
hostType = hostType,
|
||||
@@ -258,7 +273,7 @@ fun ConnectDialog(
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "${saved.host}:${saved.port} (M${saved.model} - ${saved.hostType})$tlsTag$eTag$gfxTag" +
|
||||
text = "${saved.host}:${saved.port} (${if (saved.model == 0) "Dyn ${saved.dynamicRows}x${saved.dynamicCols}" else "M" + saved.model} - ${saved.hostType})$tlsTag$eTag$gfxTag" +
|
||||
if (saved.autoConnect) " [Auto]" else "",
|
||||
fontSize = 10.sp,
|
||||
color = Color.LightGray
|
||||
@@ -273,6 +288,8 @@ fun ConnectDialog(
|
||||
hostToConn.host,
|
||||
hostToConn.port,
|
||||
hostToConn.model,
|
||||
hostToConn.dynamicRows,
|
||||
hostToConn.dynamicCols,
|
||||
hostToConn.luName,
|
||||
hostToConn.hostType,
|
||||
hostToConn.useTls,
|
||||
@@ -335,6 +352,7 @@ fun ConnectDialog(
|
||||
value = host,
|
||||
onValueChange = { host = it.filterNot { c -> c.isWhitespace() } },
|
||||
label = { Text("Host / IP Address") },
|
||||
placeholder = { Text("127.0.0.1", color = Color.Gray) },
|
||||
singleLine = true,
|
||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||
modifier = Modifier
|
||||
@@ -345,6 +363,7 @@ fun ConnectDialog(
|
||||
value = portStr,
|
||||
onValueChange = { portStr = it.filter { c -> c.isDigit() } },
|
||||
label = { Text("Port") },
|
||||
placeholder = { Text(if (useTls) "992" else "23", color = Color.Gray) },
|
||||
singleLine = true,
|
||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||
modifier = Modifier.weight(0.32f)
|
||||
@@ -363,7 +382,8 @@ fun ConnectDialog(
|
||||
2 to "M2",
|
||||
3 to "M3",
|
||||
4 to "M4",
|
||||
5 to "M5"
|
||||
5 to "M5",
|
||||
0 to "DYN"
|
||||
).forEach { (m, label) ->
|
||||
val isSelected = (modelNum == m)
|
||||
Surface(
|
||||
@@ -389,6 +409,32 @@ fun ConnectDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If Dynamic Model selected, allow specifying Rows and Columns
|
||||
if (modelNum == 0) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = dynamicRowsStr,
|
||||
onValueChange = { dynamicRowsStr = it },
|
||||
label = { Text("Dyn Rows (24-255)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = dynamicColsStr,
|
||||
onValueChange = { dynamicColsStr = it },
|
||||
label = { Text("Dyn Cols (80-255)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Host System Type (TSO, CMS, CICS)
|
||||
@@ -745,6 +791,8 @@ fun ConnectDialog(
|
||||
saved.host,
|
||||
saved.port,
|
||||
saved.model,
|
||||
saved.dynamicRows,
|
||||
saved.dynamicCols,
|
||||
saved.luName,
|
||||
saved.hostType,
|
||||
saved.useTls,
|
||||
|
||||
@@ -37,6 +37,56 @@ private val TerminalKeyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Ascii
|
||||
)
|
||||
|
||||
data class LastTransferState(
|
||||
val hostFile: String = "",
|
||||
val localFile: String = "",
|
||||
val isReceive: Boolean = true,
|
||||
val isAscii: Boolean = true,
|
||||
val hostType: FTConfig.HostType = FTConfig.HostType.TSO,
|
||||
val recfm: FTConfig.RecordFormat = FTConfig.RecordFormat.DEFAULT,
|
||||
val lreclStr: String = "",
|
||||
val blksizeStr: String = "",
|
||||
val spaceStr: String = "",
|
||||
val cmsOptions: String = ""
|
||||
)
|
||||
private var lastTransferState: 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
|
||||
fun FileTransferDialog(
|
||||
initialHostType: String = "TSO",
|
||||
@@ -54,15 +104,17 @@ fun FileTransferDialog(
|
||||
else -> FTConfig.HostType.TSO
|
||||
}
|
||||
|
||||
var hostFile by remember { mutableStateOf("") }
|
||||
var localFile by remember { mutableStateOf("") }
|
||||
var hostFile by remember { mutableStateOf(lastTransferState?.hostFile ?: "") }
|
||||
var localFile by remember { mutableStateOf(lastTransferState?.localFile ?: "") }
|
||||
var localDisplayName by remember { mutableStateOf("") }
|
||||
var isReceive by remember { mutableStateOf(true) }
|
||||
var isAscii by remember { mutableStateOf(true) }
|
||||
var hostType by remember { mutableStateOf(initialTypeEnum) }
|
||||
var recfm by remember { mutableStateOf(FTConfig.RecordFormat.DEFAULT) }
|
||||
var lreclStr by remember { mutableStateOf("") }
|
||||
var blksizeStr by remember { mutableStateOf("") }
|
||||
var isReceive by remember { mutableStateOf(lastTransferState?.isReceive ?: true) }
|
||||
var isAscii by remember { mutableStateOf(lastTransferState?.isAscii ?: true) }
|
||||
var hostType by remember { mutableStateOf(lastTransferState?.hostType ?: initialTypeEnum) }
|
||||
var recfm by remember { mutableStateOf(lastTransferState?.recfm ?: FTConfig.RecordFormat.DEFAULT) }
|
||||
var lreclStr by remember { mutableStateOf(lastTransferState?.lreclStr ?: "") }
|
||||
var blksizeStr by remember { mutableStateOf(lastTransferState?.blksizeStr ?: "") }
|
||||
var spaceStr by remember { mutableStateOf(lastTransferState?.spaceStr ?: "") }
|
||||
var cmsOptions by remember { mutableStateOf(lastTransferState?.cmsOptions ?: "") }
|
||||
var overwrite by remember { mutableStateOf(true) }
|
||||
var showBrowseDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -86,8 +138,7 @@ fun FileTransferDialog(
|
||||
localFile = tempFile.absolutePath
|
||||
localDisplayName = displayName
|
||||
if (hostFile.isBlank()) {
|
||||
val cleanBase = displayName.substringBeforeLast('.').replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
|
||||
hostFile = if (hostType == FTConfig.HostType.TSO) "'$cleanBase'" else "$cleanBase FILE A"
|
||||
hostFile = formatHostFilename(displayName, hostType)
|
||||
}
|
||||
Toast.makeText(context, "Selected: $displayName", Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
@@ -143,8 +194,7 @@ fun FileTransferDialog(
|
||||
onSelectDataset = { selected ->
|
||||
hostFile = selected
|
||||
if (localFile.isBlank()) {
|
||||
val base = selected.trim('\'').substringAfterLast('.').substringAfterLast(' ')
|
||||
val safeName = if (base.isNotBlank()) "$base.txt" else "download.txt"
|
||||
val safeName = formatLocalFilenameFromHost(selected, hostType)
|
||||
val tempDir = File(context.cacheDir, "ft_download").apply { if (!exists()) mkdirs() }
|
||||
val tempFile = File(tempDir, safeName)
|
||||
localFile = tempFile.absolutePath
|
||||
@@ -361,8 +411,7 @@ fun FileTransferDialog(
|
||||
onClick = {
|
||||
if (isReceive) {
|
||||
val defName = if (hostFile.isNotBlank()) {
|
||||
val base = hostFile.trim('\'').substringAfterLast('.').substringAfterLast(' ')
|
||||
if (base.isNotBlank()) "$base.txt" else "download.txt"
|
||||
formatLocalFilenameFromHost(hostFile, hostType)
|
||||
} else "download.txt"
|
||||
createDocumentLauncher.launch(defName)
|
||||
} else {
|
||||
@@ -467,10 +516,35 @@ fun FileTransferDialog(
|
||||
onValueChange = { blksizeStr = it.filter { c -> c.isDigit() } },
|
||||
label = { Text("BLKSIZE", fontSize = 11.sp) },
|
||||
singleLine = true,
|
||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done),
|
||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
OutlinedTextField(
|
||||
value = spaceStr,
|
||||
onValueChange = { spaceStr = it },
|
||||
label = { Text("SPACE (Allocation)", fontSize = 11.sp) },
|
||||
placeholder = { Text("e.g. (1,1) cyl or 5,2") },
|
||||
singleLine = true,
|
||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
// 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 +571,24 @@ fun FileTransferDialog(
|
||||
setRecfm(recfm)
|
||||
lreclStr.trim().toIntOrNull()?.let { setLrecl(it) }
|
||||
blksizeStr.trim().toIntOrNull()?.let { setBlksize(it) }
|
||||
if (spaceStr.isNotBlank()) setSpace(spaceStr.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,
|
||||
spaceStr = spaceStr,
|
||||
cmsOptions = cmsOptions
|
||||
)
|
||||
onStartTransfer(config)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,6 +15,8 @@ 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.activity.compose.BackHandler
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import haus.nightmare.lib3270j.ConnectionState
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||
|
||||
@@ -23,6 +25,11 @@ fun TwoRowKeyBar(
|
||||
connectionState: ConnectionState,
|
||||
isShiftPressed: Boolean = false,
|
||||
isLightPenMode: Boolean = false,
|
||||
isInsertMode: Boolean = false,
|
||||
isDocMode: Boolean = false,
|
||||
isWordWrap: Boolean = false,
|
||||
isAplMode: Boolean = false,
|
||||
fourColorOverride: Boolean = false,
|
||||
hapticFeedbackEnabled: Boolean = true,
|
||||
onClearShift: () -> Unit = {},
|
||||
onConnectClick: () -> Unit,
|
||||
@@ -34,6 +41,11 @@ fun TwoRowKeyBar(
|
||||
onScriptClick: () -> Unit = {},
|
||||
onPrinterSessionClick: () -> Unit = {},
|
||||
onToggleLightPen: () -> Unit = {},
|
||||
onToggleInsert: () -> Unit = {},
|
||||
onToggleDocMode: () -> Unit = {},
|
||||
onToggleWordWrap: () -> Unit = {},
|
||||
onToggleAplMode: () -> Unit = {},
|
||||
onToggleFourColorOverride: () -> Unit = {},
|
||||
onSettingsClick: () -> Unit = {},
|
||||
onSendAid: (Int) -> Unit,
|
||||
onReset: () -> Unit,
|
||||
@@ -51,6 +63,11 @@ fun TwoRowKeyBar(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var menuExpanded by remember { mutableStateOf(false) }
|
||||
var lastDismissTime by remember { mutableLongStateOf(0L) }
|
||||
|
||||
BackHandler(enabled = menuExpanded) {
|
||||
menuExpanded = false
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
@@ -73,11 +90,22 @@ fun TwoRowKeyBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
hapticFeedbackEnabled = hapticFeedbackEnabled,
|
||||
innerPaddingHorizontal = 0.dp,
|
||||
onClick = { menuExpanded = true }
|
||||
onClick = {
|
||||
if (System.currentTimeMillis() - lastDismissTime > 200L) {
|
||||
menuExpanded = !menuExpanded
|
||||
}
|
||||
}
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = menuExpanded,
|
||||
onDismissRequest = { menuExpanded = false },
|
||||
onDismissRequest = {
|
||||
menuExpanded = false
|
||||
lastDismissTime = System.currentTimeMillis()
|
||||
},
|
||||
properties = PopupProperties(
|
||||
focusable = false,
|
||||
dismissOnClickOutside = true
|
||||
),
|
||||
modifier = Modifier
|
||||
.background(Color(0xFF2C2D30))
|
||||
.heightIn(max = 420.dp)
|
||||
@@ -178,6 +206,42 @@ fun TwoRowKeyBar(
|
||||
}
|
||||
)
|
||||
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(
|
||||
text = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package haus.nightmare.a3270.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -27,16 +28,32 @@ fun OiaStatusBar(
|
||||
isTls: Boolean = false,
|
||||
isTlsVerified: Boolean = true,
|
||||
graphicsMode: String = "NONE",
|
||||
codePage: String = "037",
|
||||
isLightPenMode: 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,
|
||||
luName: String = "",
|
||||
isNumericField: Boolean = false,
|
||||
uiTheme: String = "DARK",
|
||||
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 col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
||||
val posStr = String.format("%02d/%02d", row, col)
|
||||
val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr)
|
||||
|
||||
val hostText = if (currentHost.isNotBlank()) currentHost else oiaText
|
||||
|
||||
@@ -63,20 +80,25 @@ fun OiaStatusBar(
|
||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> "X OP"
|
||||
else -> "X LOCKED"
|
||||
}
|
||||
txt to Color(0xFFFF6B6B)
|
||||
val clr = when (inhibitReason) {
|
||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> commCheckClr
|
||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> attentionClr
|
||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NUMERIC_ONLY -> attentionClr
|
||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OVERFLOW -> attentionClr
|
||||
else -> inputInhibitedClr
|
||||
}
|
||||
txt to clr
|
||||
} else if (isKeyboardLocked) {
|
||||
"X SYSTEM" to Color(0xFFFF6B6B)
|
||||
} else if (isInsertMode) {
|
||||
"INSERT" to Color(0xFF339AF0)
|
||||
"X SYSTEM" to inputInhibitedClr
|
||||
} else {
|
||||
"READY" to Color(0xFF51CF66)
|
||||
"READY" to sysAvailClr
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(26.dp)
|
||||
.background(Color(0xFF161719))
|
||||
.background(barBg)
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
@@ -89,12 +111,12 @@ fun OiaStatusBar(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(if (connectionState.isConnected()) Color(0xFF51CF66) else Color(0xFF868E96))
|
||||
.background(if (connectionState.isConnected()) Color(0xFF7890F0) else Color(0xFF868E96))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = hostText,
|
||||
color = Color.White,
|
||||
color = defaultTextClr,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -141,16 +163,16 @@ fun OiaStatusBar(
|
||||
}
|
||||
}
|
||||
|
||||
// CodePage Badge
|
||||
if (connectionState.isConnected() && codePage.isNotBlank()) {
|
||||
// Field Status: NUM vs ALPHA
|
||||
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Surface(
|
||||
color = Color(0xFFFA5252).copy(alpha = 0.2f),
|
||||
color = Color(0xFF339AF0).copy(alpha = 0.2f),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "CP$codePage",
|
||||
color = Color(0xFFFF8787),
|
||||
text = if (isNumericField) "NUM" else "ALPHA",
|
||||
color = if (isNumericField) Color(0xFFFFD43B) else Color(0xFF74C0FC),
|
||||
fontSize = 9.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -214,6 +236,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)
|
||||
Surface(
|
||||
color = lockStatusColor.copy(alpha = 0.2f),
|
||||
@@ -230,11 +324,11 @@ fun OiaStatusBar(
|
||||
)
|
||||
}
|
||||
|
||||
// Cursor Position
|
||||
// Cursor Position & Buffer Address
|
||||
Text(
|
||||
text = "R$posStr",
|
||||
color = Color(0xFF22B8CF),
|
||||
fontSize = 11.sp,
|
||||
text = posStr,
|
||||
color = if (isLight) Color(0xFF0C8599) else Color(0xFF22B8CF),
|
||||
fontSize = 10.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
@@ -41,8 +41,8 @@ import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun PrinterSessionDialog(
|
||||
initialHost: String = "127.0.0.1",
|
||||
initialPort: Int = 23,
|
||||
initialHost: String = "",
|
||||
initialPort: Int = 0,
|
||||
initialCodePage: String = "037",
|
||||
initialUseTls: Boolean = false,
|
||||
onDismiss: () -> Unit
|
||||
@@ -50,8 +50,8 @@ fun PrinterSessionDialog(
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
var host by remember { mutableStateOf(initialHost.ifBlank { "127.0.0.1" }) }
|
||||
var portStr by remember { mutableStateOf(if (initialPort > 0) initialPort.toString() else "23") }
|
||||
var host by remember { mutableStateOf(initialHost) }
|
||||
var portStr by remember { mutableStateOf(if (initialPort > 0) initialPort.toString() else "") }
|
||||
var printerLu by remember { mutableStateOf("") }
|
||||
var assocDisplayLu by remember { mutableStateOf("") }
|
||||
var codePage by remember { mutableStateOf(initialCodePage) }
|
||||
@@ -69,8 +69,9 @@ fun PrinterSessionDialog(
|
||||
val codePagesList = haus.nightmare.a3270.storage.HostStorage.AVAILABLE_CODE_PAGES
|
||||
|
||||
fun startSession() {
|
||||
val port = portStr.trim().toIntOrNull() ?: 23
|
||||
val config = PrinterConfig(host.trim(), port).apply {
|
||||
val targetHost = host.trim().ifBlank { "127.0.0.1" }
|
||||
val port = portStr.trim().toIntOrNull() ?: if (useTls) 992 else 23
|
||||
val config = PrinterConfig(targetHost, port).apply {
|
||||
this.isUseTls = useTls
|
||||
this.codePage = codePage
|
||||
if (printerLu.isNotBlank()) this.printerLuName = printerLu.trim()
|
||||
@@ -273,6 +274,7 @@ fun PrinterSessionDialog(
|
||||
value = host,
|
||||
onValueChange = { host = it },
|
||||
label = { Text("Host") },
|
||||
placeholder = { Text("127.0.0.1", color = Color.Gray) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(2.5f)
|
||||
)
|
||||
@@ -280,6 +282,7 @@ fun PrinterSessionDialog(
|
||||
value = portStr,
|
||||
onValueChange = { portStr = it },
|
||||
label = { Text("Port") },
|
||||
placeholder = { Text(if (useTls) "992" else "23", color = Color.Gray) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(1f)
|
||||
|
||||
@@ -316,20 +316,24 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
|
||||
sb.append("<!DOCTYPE html>\n<html>\n<head>\n")
|
||||
sb.append("<meta charset=\"UTF-8\">\n")
|
||||
sb.append("<style>\n")
|
||||
sb.append("body { background-color: #0A0A0A; color: #00FF66; font-family: monospace; font-size: 14px; margin: 20px; }\n")
|
||||
sb.append("body { background-color: #0A0A0A; color: #00FF00; font-family: monospace; font-size: 14px; margin: 20px; }\n")
|
||||
sb.append("pre { line-height: 1.2; font-family: monospace; }\n")
|
||||
sb.append(".c-green { color: #00FF66; }\n")
|
||||
sb.append(".c-green { color: #00FF00; }\n")
|
||||
sb.append(".c-white { color: #FFFFFF; }\n")
|
||||
sb.append(".c-blue { color: #3399FF; }\n")
|
||||
sb.append(".c-red { color: #FF3333; }\n")
|
||||
sb.append(".c-yellow { color: #FFFF33; }\n")
|
||||
sb.append(".c-turquoise { color: #00E5FF; }\n")
|
||||
sb.append(".c-pink { color: #FF66CC; }\n")
|
||||
sb.append(".c-blue { color: #7890F0; }\n")
|
||||
sb.append(".c-red { color: #FF0000; }\n")
|
||||
sb.append(".c-yellow { color: #FFFF00; }\n")
|
||||
sb.append(".c-turquoise { color: #00FFFF; }\n")
|
||||
sb.append(".c-pink { color: #FF00FF; }\n")
|
||||
sb.append(".c-orange { color: #FFA200; }\n")
|
||||
sb.append(".c-mustard { color: #A0A000; }\n")
|
||||
sb.append(".c-grey { color: #C0C0C0; }\n")
|
||||
sb.append("</style>\n</head>\n<body>\n<pre>\n")
|
||||
|
||||
var currentHidden = false
|
||||
var currentProt = false
|
||||
var currentHi = false
|
||||
var currentFieldEa: haus.nightmare.lib3270j.screen.ExtendedAttribute? = null
|
||||
|
||||
for (r in 0 until rows) {
|
||||
for (c in 0 until cols) {
|
||||
@@ -340,17 +344,41 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
|
||||
currentProt = (faVal and FA_PROTECT) != 0
|
||||
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||
currentFieldEa = cell
|
||||
sb.append(' ')
|
||||
} else {
|
||||
if (currentHidden && maskHidden) {
|
||||
sb.append(' ')
|
||||
} else {
|
||||
val ch = cell.ucs4
|
||||
val colorClass = when {
|
||||
currentHi -> "c-white"
|
||||
currentProt -> "c-blue"
|
||||
val fgByte = if (cell.fg != 0.toByte()) (cell.fg.toInt() and 0xFF)
|
||||
else (currentFieldEa?.fg?.toInt()?.and(0xFF) ?: 0)
|
||||
val colorClass = if (fgByte in 0xf0..0xff) {
|
||||
when (fgByte - 0xf0) {
|
||||
1 -> "c-blue"
|
||||
2 -> "c-red"
|
||||
3 -> "c-pink"
|
||||
4 -> "c-green"
|
||||
5 -> "c-turquoise"
|
||||
6 -> "c-yellow"
|
||||
7 -> "c-white"
|
||||
9 -> "c-blue"
|
||||
10 -> "c-orange"
|
||||
11 -> "c-pink"
|
||||
12 -> "c-green"
|
||||
13 -> "c-turquoise"
|
||||
14 -> "c-mustard"
|
||||
15 -> "c-grey"
|
||||
else -> "c-green"
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
currentProt && currentHi -> "c-white"
|
||||
currentProt && !currentHi -> "c-turquoise"
|
||||
!currentProt && currentHi -> "c-red"
|
||||
else -> "c-green"
|
||||
}
|
||||
}
|
||||
val escChar = when (ch) {
|
||||
'<' -> "<"
|
||||
'>' -> ">"
|
||||
@@ -394,6 +422,18 @@ private fun generateBitmap(
|
||||
|
||||
if (screenBuffer == null) return bitmap
|
||||
|
||||
// Layer 1: Vector graphics plane (under text)
|
||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||
val rgb = graphicsPlane.rgbBuffer
|
||||
val gWidth = graphicsPlane.canvasWidth
|
||||
val gHeight = graphicsPlane.canvasHeight
|
||||
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
||||
val gfxBmp = Bitmap.createBitmap(rgb, gWidth, gHeight, Bitmap.Config.ARGB_8888)
|
||||
val scaledGfx = Bitmap.createScaledBitmap(gfxBmp, width, height, true)
|
||||
canvas.drawBitmap(scaledGfx, 0f, 0f, null)
|
||||
}
|
||||
}
|
||||
|
||||
val textPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
typeface = Typeface.MONOSPACE
|
||||
@@ -403,6 +443,7 @@ private fun generateBitmap(
|
||||
var currentHidden = false
|
||||
var currentProt = false
|
||||
var currentHi = false
|
||||
var currentFieldEa: haus.nightmare.lib3270j.screen.ExtendedAttribute? = null
|
||||
|
||||
for (r in 0 until rows) {
|
||||
for (c in 0 until cols) {
|
||||
@@ -416,22 +457,47 @@ private fun generateBitmap(
|
||||
currentProt = (faVal and FA_PROTECT) != 0
|
||||
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
|
||||
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
|
||||
currentFieldEa = cell
|
||||
} else {
|
||||
val csVal = cell.cs.toInt() and 0xFF
|
||||
val ecVal = cell.ec.toInt() and 0xFF
|
||||
val fgByte = if (cell.fg != 0.toByte()) (cell.fg.toInt() and 0xFF)
|
||||
else (currentFieldEa?.fg?.toInt()?.and(0xFF) ?: 0)
|
||||
val cellColor = if (fgByte in 0xf0..0xff) {
|
||||
when (fgByte - 0xf0) {
|
||||
1 -> android.graphics.Color.parseColor("#7890F0")
|
||||
2 -> android.graphics.Color.parseColor("#FF0000")
|
||||
3 -> android.graphics.Color.parseColor("#FF00FF")
|
||||
4 -> android.graphics.Color.parseColor("#00FF00")
|
||||
5 -> android.graphics.Color.parseColor("#00FFFF")
|
||||
6 -> android.graphics.Color.parseColor("#FFFF00")
|
||||
7 -> android.graphics.Color.WHITE
|
||||
9 -> android.graphics.Color.parseColor("#000080")
|
||||
10 -> android.graphics.Color.parseColor("#FFA200")
|
||||
11 -> android.graphics.Color.parseColor("#800080")
|
||||
12 -> android.graphics.Color.parseColor("#008000")
|
||||
13 -> android.graphics.Color.parseColor("#008080")
|
||||
14 -> android.graphics.Color.parseColor("#A0A000")
|
||||
15 -> android.graphics.Color.parseColor("#C0C0C0")
|
||||
else -> android.graphics.Color.parseColor("#00FF00")
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
currentProt && currentHi -> android.graphics.Color.WHITE
|
||||
currentProt && !currentHi -> android.graphics.Color.parseColor("#00FFFF")
|
||||
!currentProt && currentHi -> android.graphics.Color.parseColor("#FF0000")
|
||||
else -> android.graphics.Color.parseColor("#00FF00")
|
||||
}
|
||||
}
|
||||
|
||||
var drawnAsPs = false
|
||||
if (csVal >= 0x40 && programSymbolManager != null) {
|
||||
val slot = programSymbolManager.getSymbol(csVal, ecVal)
|
||||
if (slot != null) {
|
||||
val symWidth = slot.width
|
||||
val symHeight = slot.height
|
||||
val fgArgb = when {
|
||||
currentHi -> android.graphics.Color.WHITE
|
||||
currentProt -> android.graphics.Color.parseColor("#3399FF")
|
||||
else -> android.graphics.Color.parseColor("#00FF66")
|
||||
}
|
||||
val bgArgb = android.graphics.Color.parseColor("#0A0A0A")
|
||||
val rgbArray = slot.getRgbPixels(fgArgb, bgArgb)
|
||||
val bgArgb = android.graphics.Color.TRANSPARENT
|
||||
val rgbArray = slot.getRgbPixels(cellColor, bgArgb)
|
||||
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
|
||||
val symBmp = Bitmap.createBitmap(rgbArray, symWidth, symHeight, Bitmap.Config.ARGB_8888)
|
||||
canvas.drawBitmap(symBmp, null, android.graphics.RectF(left, top, left + charWidth, top + charHeight), null)
|
||||
@@ -443,11 +509,7 @@ private fun generateBitmap(
|
||||
if (!drawnAsPs && (!currentHidden || !maskHidden)) {
|
||||
val ch = cell.ucs4
|
||||
if (ch in ' '..'~' || ch > '\u007F') {
|
||||
textPaint.color = when {
|
||||
currentHi -> android.graphics.Color.WHITE
|
||||
currentProt -> android.graphics.Color.parseColor("#3399FF")
|
||||
else -> android.graphics.Color.parseColor("#00FF66")
|
||||
}
|
||||
textPaint.color = cellColor
|
||||
canvas.drawText(ch.toString(), left + 2f, top + 18f, textPaint)
|
||||
}
|
||||
}
|
||||
@@ -455,17 +517,5 @@ private fun generateBitmap(
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay vector graphics plane if present
|
||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||
val rgb = graphicsPlane.rgbBuffer
|
||||
val gWidth = graphicsPlane.canvasWidth
|
||||
val gHeight = graphicsPlane.canvasHeight
|
||||
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
||||
val gfxBmp = Bitmap.createBitmap(rgb, gWidth, gHeight, Bitmap.Config.ARGB_8888)
|
||||
val scaledGfx = Bitmap.createScaledBitmap(gfxBmp, width, height, true)
|
||||
canvas.drawBitmap(scaledGfx, 0f, 0f, null)
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
@@ -26,9 +26,55 @@ fun SettingsDialog(
|
||||
initialVerifyCerts: Boolean = true,
|
||||
initialDefaultGraphicsMode: String = "BOTH",
|
||||
initialDefaultCodePage: String = "037",
|
||||
initialUiTheme: String = "DARK",
|
||||
initialCrosshairRuler: Boolean = false,
|
||||
initialCursorStyle: String = "BLOCK",
|
||||
initialBlockSelectMode: Boolean = false,
|
||||
initialStartupBehavior: String = "SHOW_CONNECT",
|
||||
initialDynamicRows: Int = 62,
|
||||
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,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean, verifyCerts: Boolean, defaultGraphicsMode: String, defaultCodePage: String) -> Unit
|
||||
onSave: (
|
||||
maskHiddenInput: Boolean,
|
||||
cursorBlink: Boolean,
|
||||
hapticFeedback: Boolean,
|
||||
verifyCerts: Boolean,
|
||||
defaultGraphicsMode: String,
|
||||
defaultCodePage: String,
|
||||
uiTheme: String,
|
||||
crosshairRuler: Boolean,
|
||||
cursorStyle: String,
|
||||
blockSelectMode: Boolean,
|
||||
startupBehavior: String,
|
||||
dynamicRows: 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
|
||||
) -> Unit
|
||||
) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
|
||||
|
||||
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
|
||||
var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
|
||||
var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) }
|
||||
@@ -36,6 +82,28 @@ fun SettingsDialog(
|
||||
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
|
||||
var defaultCodePage by remember { mutableStateOf(initialDefaultCodePage) }
|
||||
|
||||
var uiTheme by remember { mutableStateOf(initialUiTheme) }
|
||||
var crosshairRuler by remember { mutableStateOf(initialCrosshairRuler) }
|
||||
var cursorStyle by remember { mutableStateOf(initialCursorStyle) }
|
||||
var blockSelectMode by remember { mutableStateOf(initialBlockSelectMode) }
|
||||
var startupBehavior by remember { mutableStateOf(initialStartupBehavior) }
|
||||
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.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 showImportDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
@@ -45,7 +113,7 @@ fun SettingsDialog(
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.95f)
|
||||
.heightIn(max = 680.dp)
|
||||
.heightIn(max = 700.dp)
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Column(
|
||||
@@ -63,65 +131,88 @@ fun SettingsDialog(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Setting 1: Mask Protected/Hidden Input with '*'
|
||||
// SECTION 1: APPEARANCE
|
||||
Text("Appearance", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// UI Theme
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("UI Theme", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
listOf("DARK" to "Dark", "LIGHT" to "Light").forEach { (themeKey, themeLabel) ->
|
||||
val isSelected = uiTheme.equals(themeKey, ignoreCase = true)
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
onClick = { uiTheme = themeKey },
|
||||
label = { Text(themeLabel, fontSize = 11.sp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Crosshair Ruler
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text(
|
||||
text = "Show '*' for Hidden Fields",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Display asterisks for typed characters in non-display / password fields rather than blank spaces.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
Text("Crosshair Ruler", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Display intersecting row/column ruler tracking cursor position.", fontSize = 12.sp, color = Color.LightGray)
|
||||
}
|
||||
Switch(
|
||||
checked = maskHiddenInput,
|
||||
onCheckedChange = { maskHiddenInput = it },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = Color(0xFF2B8A3E)
|
||||
)
|
||||
checked = crosshairRuler,
|
||||
onCheckedChange = { crosshairRuler = 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))
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Setting 2: Blinking Cursor
|
||||
// Cursor Style
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text(
|
||||
text = "Blinking Cursor",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Blink the terminal cursor to clearly indicate current screen position.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
Text("Cursor Style", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Shape of 3270 screen cursor.", fontSize = 12.sp, color = Color.LightGray)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
listOf("BLOCK" to "Block", "UNDERLINE" to "Underline").forEach { (styleKey, styleLabel) ->
|
||||
val isSelected = cursorStyle.equals(styleKey, ignoreCase = true)
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
onClick = { cursorStyle = styleKey },
|
||||
label = { Text(styleLabel, fontSize = 11.sp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Blinking Cursor
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text("Blinking Cursor", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Blink the terminal cursor indicator.", fontSize = 12.sp, color = Color.LightGray)
|
||||
}
|
||||
Switch(
|
||||
checked = cursorBlink,
|
||||
onCheckedChange = { cursorBlink = it },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = Color(0xFF2B8A3E)
|
||||
)
|
||||
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -129,86 +220,138 @@ fun SettingsDialog(
|
||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Setting 3: Button Haptic Feedback
|
||||
// SECTION 2: INTERACTION & BEHAVIOR
|
||||
Text("Interaction & Behavior", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Mask Hidden Inputs
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text(
|
||||
text = "Button Haptic Feedback",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
Text("Show '*' for Hidden Fields", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Display asterisks for typed characters in password fields.", fontSize = 12.sp, color = Color.LightGray)
|
||||
}
|
||||
Switch(
|
||||
checked = maskHiddenInput,
|
||||
onCheckedChange = { maskHiddenInput = it },
|
||||
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||
)
|
||||
Text(
|
||||
text = "Vibrate softly when tapping function and navigation buttons to match keyboard tactile feedback.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Block Selection Mode
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text("Block Selection Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Select rectangular blocks instead of linear character stream.", fontSize = 12.sp, color = Color.LightGray)
|
||||
}
|
||||
Switch(
|
||||
checked = blockSelectMode,
|
||||
onCheckedChange = { blockSelectMode = it },
|
||||
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Haptic Feedback
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text("Button Haptic Feedback", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Vibrate softly when tapping keybar buttons.", fontSize = 12.sp, color = Color.LightGray)
|
||||
}
|
||||
Switch(
|
||||
checked = hapticFeedback,
|
||||
onCheckedChange = { hapticFeedback = it },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = Color(0xFF2B8A3E)
|
||||
)
|
||||
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))
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Setting 4: Overall TLS Certificate Verification
|
||||
// Startup Behavior
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Startup Behavior", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Action to take when launching the application.", fontSize = 12.sp, color = Color.LightGray)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text(
|
||||
text = "Verify TLS Certificates",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Enforce SSL/TLS certificate validation. Turn off to allow self-signed or unverified certificates without security prompts.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
listOf(
|
||||
"SHOW_CONNECT" to "Show Connect",
|
||||
"DO_NOTHING" to "Do Nothing",
|
||||
"AUTO_CONNECT" to "Auto-Connect"
|
||||
).forEach { (sbKey, sbLabel) ->
|
||||
val isSelected = startupBehavior.equals(sbKey, ignoreCase = true)
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
onClick = { startupBehavior = sbKey },
|
||||
label = { Text(sbLabel, fontSize = 10.sp) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = verifyCerts,
|
||||
onCheckedChange = { verifyCerts = 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))
|
||||
|
||||
// Setting 5: Default Graphics Support Mode
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "Default Graphics Mode",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Configure default vector graphics (GOCA) and Programmed Symbols (APL) mode.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
// SECTION 3: PROTOCOL & DYNAMIC DEFAULTS
|
||||
Text("Protocol & Defaults", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Default Dynamic Dimensions
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Default Dynamic Screen Dimensions", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Rows (24-255) and columns (80-255) for IBM-DYNAMIC sessions.", fontSize = 12.sp, color = Color.LightGray)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = dynamicRowsStr,
|
||||
onValueChange = { dynamicRowsStr = it },
|
||||
label = { Text("Rows (24-255)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||
)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = dynamicColsStr,
|
||||
onValueChange = { dynamicColsStr = it },
|
||||
label = { Text("Cols (80-255)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Graphics Mode
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Default Graphics Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
@@ -220,53 +363,26 @@ fun SettingsDialog(
|
||||
"NONE" to "Off"
|
||||
).forEach { (modeKey, modeLabel) ->
|
||||
val isSelected = defaultGraphicsMode.equals(modeKey, ignoreCase = true)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
color = if (isSelected) Color(0xFF2B8A3E) else Color(0xFF2C2D30),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(34.dp)
|
||||
.clickable { defaultGraphicsMode = modeKey }
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = modeLabel,
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
onClick = { defaultGraphicsMode = modeKey },
|
||||
label = { Text(modeLabel, fontSize = 10.sp) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Setting 6: Default EBCDIC Code Page
|
||||
// Default CodePage
|
||||
var codePageMenuExpanded by remember { mutableStateOf(false) }
|
||||
val codePagesList = haus.nightmare.a3270.storage.HostStorage.AVAILABLE_CODE_PAGES
|
||||
val selectedCpLabel = codePagesList.firstOrNull { it.first == defaultCodePage }?.second ?: "Code Page $defaultCodePage"
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "Default EBCDIC Code Page",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Character encoding translation table for new sessions.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Default EBCDIC Code Page", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
@@ -278,27 +394,18 @@ fun SettingsDialog(
|
||||
.clickable { codePageMenuExpanded = true }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 10.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = selectedCpLabel,
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(text = selectedCpLabel, color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
Text("▼", color = Color.Gray, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = codePageMenuExpanded,
|
||||
onDismissRequest = { codePageMenuExpanded = false },
|
||||
modifier = Modifier
|
||||
.background(Color(0xFF2C2D30))
|
||||
.heightIn(max = 280.dp)
|
||||
modifier = Modifier.background(Color(0xFF2C2D30)).heightIn(max = 280.dp)
|
||||
) {
|
||||
codePagesList.forEach { (cpId, label) ->
|
||||
DropdownMenuItem(
|
||||
@@ -320,6 +427,257 @@ fun SettingsDialog(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Verify TLS Certs
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text("Verify TLS Certificates", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
Text("Validate TLS certificates against system trust store.", fontSize = 12.sp, color = Color.LightGray)
|
||||
}
|
||||
Switch(
|
||||
checked = verifyCerts,
|
||||
onCheckedChange = { verifyCerts = 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 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("Compatible with j3270 desktop INI configuration files.", fontSize = 12.sp, color = Color.LightGray)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
val sw = java.io.StringWriter()
|
||||
haus.nightmare.a3270.storage.AppSettings.exportToIni(context, sw)
|
||||
val iniText = sw.toString()
|
||||
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(iniText))
|
||||
android.widget.Toast.makeText(context, "Exported INI copied to clipboard", android.widget.Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Export to INI", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { showImportDialog = true },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Import from INI", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Action Buttons
|
||||
@@ -333,7 +691,37 @@ fun SettingsDialog(
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
onSave(maskHiddenInput, cursorBlink, hapticFeedback, verifyCerts, defaultGraphicsMode, defaultCodePage)
|
||||
val dRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
|
||||
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(
|
||||
maskHiddenInput,
|
||||
cursorBlink,
|
||||
hapticFeedback,
|
||||
verifyCerts,
|
||||
defaultGraphicsMode,
|
||||
defaultCodePage,
|
||||
uiTheme,
|
||||
crosshairRuler,
|
||||
cursorStyle,
|
||||
blockSelectMode,
|
||||
startupBehavior,
|
||||
dRows,
|
||||
dCols,
|
||||
entryAssistDocMode,
|
||||
entryAssistWordWrap,
|
||||
eaStartCol,
|
||||
eaEndCol,
|
||||
entryAssistBell,
|
||||
eaBellCol,
|
||||
entryAssistTabStops,
|
||||
insertOffOnAid,
|
||||
fourColorOverride,
|
||||
numericFieldLock,
|
||||
autoSkipEnabled
|
||||
)
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||
) {
|
||||
@@ -343,4 +731,65 @@ fun SettingsDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showImportDialog) {
|
||||
var importText by remember { mutableStateOf("") }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showImportDialog = false },
|
||||
title = { Text("Import INI Configuration") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Paste INI configuration content below:", fontSize = 12.sp)
|
||||
OutlinedTextField(
|
||||
value = importText,
|
||||
onValueChange = { importText = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(180.dp),
|
||||
maxLines = 10
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showImportDialog = false
|
||||
if (importText.isNotBlank()) {
|
||||
try {
|
||||
haus.nightmare.a3270.storage.AppSettings.loadFromIni(context, java.io.StringReader(importText))
|
||||
uiTheme = haus.nightmare.a3270.storage.AppSettings.getUiTheme(context)
|
||||
crosshairRuler = haus.nightmare.a3270.storage.AppSettings.isCrosshairRulerEnabled(context)
|
||||
cursorStyle = haus.nightmare.a3270.storage.AppSettings.getCursorStyle(context)
|
||||
blockSelectMode = haus.nightmare.a3270.storage.AppSettings.isBlockSelectModeEnabled(context)
|
||||
startupBehavior = haus.nightmare.a3270.storage.AppSettings.getStartupBehavior(context).name
|
||||
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
|
||||
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
||||
defaultCodePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(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)
|
||||
android.widget.Toast.makeText(context, "Settings imported from INI", 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 = { showImportDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,4 +111,10 @@ class TerminalInputView(context: Context) : View(context) {
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
fun hideSoftKeyboard() {
|
||||
clearFocus()
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import android.widget.Toast
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
@@ -24,32 +22,36 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||
|
||||
// Standard 3279 Host Colors (16-color palette)
|
||||
// Standard 3279 Host Colors (16-color palette) aligned 1:1 with IBM Host On-Demand ColorRemapModel3270
|
||||
private val HOST_COLORS = arrayOf(
|
||||
Color(0xFF000000), // 0: Neutral Black
|
||||
Color(0xFF5078FF), // 1: Blue
|
||||
Color(0xFFFF3232), // 2: Red
|
||||
Color(0xFFFF82B4), // 3: Pink
|
||||
Color(0xFF32CD32), // 4: Green
|
||||
Color(0xFF40E0D0), // 5: Turquoise
|
||||
Color(0xFFFFFF50), // 6: Yellow
|
||||
Color(0xFF7890F0), // 1: Blue (0x7890F0 CUSTOMBLUE)
|
||||
Color(0xFFFF0000), // 2: Red
|
||||
Color(0xFFFF00FF), // 3: Pink
|
||||
Color(0xFF00FF00), // 4: Green
|
||||
Color(0xFF00FFFF), // 5: Turquoise / Cyan
|
||||
Color(0xFFFFFF00), // 6: Yellow
|
||||
Color(0xFFFFFFFF), // 7: Neutral White
|
||||
Color(0xFF000000), // 8: Black
|
||||
Color(0xFF1E3CB4), // 9: Deep Blue
|
||||
Color(0xFFFFA500), // 10: Orange
|
||||
Color(0xFFB482FF), // 11: Purple
|
||||
Color(0xFF90EE90), // 12: Pale Green
|
||||
Color(0xFFAFEEEE), // 13: Pale Turquoise
|
||||
Color(0xFFAAAAAA), // 14: Grey
|
||||
Color(0xFFFFFFFF) // 15: White
|
||||
Color(0xFF000080), // 9: Deep Blue
|
||||
Color(0xFFFFA200), // 10: Orange (0xFFFFA200)
|
||||
Color(0xFF800080), // 11: Purple
|
||||
Color(0xFF008000), // 12: Pale Green
|
||||
Color(0xFF008080), // 13: Pale Turquoise
|
||||
Color(0xFFA0A000), // 14: Mustard (0xFFA0A000)
|
||||
Color(0xFFC0C0C0) // 15: Grey (0xFFC0C0C0)
|
||||
)
|
||||
|
||||
private val COLOR_BLACK = Color(0xFF0A0A0A)
|
||||
private val SELECTION_COLOR = Color(75, 110, 175, 102) // 40% alpha blend per HoD
|
||||
private val SEARCH_HIGHLIGHT_COLOR = Color(255, 215, 0, 120) // Gold per HoD
|
||||
private val CROSSHAIR_RULER_COLOR = Color(0, 255, 0, 102) // 40% alpha green per HoD
|
||||
|
||||
@Composable
|
||||
fun TerminalView(
|
||||
@@ -64,12 +66,18 @@ fun TerminalView(
|
||||
isLightPenMode: Boolean = false,
|
||||
maskHiddenFields: Boolean = true,
|
||||
blinkCursor: Boolean = true,
|
||||
crosshairRuler: Boolean = false,
|
||||
cursorStyle: String = "BLOCK",
|
||||
isInsertMode: Boolean = false,
|
||||
blockSelectMode: Boolean = false,
|
||||
fourColorOverride: Boolean = false,
|
||||
searchHighlightAddr: Int = -1,
|
||||
searchHighlightLen: Int = 0,
|
||||
onTapAddress: (Int) -> Unit,
|
||||
onLightPenSelect: ((Int) -> Unit)? = null,
|
||||
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
|
||||
onPasteText: (String) -> Unit = {},
|
||||
onPasteLineWrap: ((String, Int, Boolean) -> Unit)? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -83,6 +91,7 @@ fun TerminalView(
|
||||
var selectionEnd by remember { mutableStateOf<Offset?>(null) }
|
||||
var showContextMenu by remember { mutableStateOf(false) }
|
||||
var contextMenuOffset by remember { mutableStateOf(Offset.Zero) }
|
||||
var showPasteWrapDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// Blinking cursor state (~530ms interval matching j3270)
|
||||
var cursorVisible by remember { mutableStateOf(true) }
|
||||
@@ -98,6 +107,15 @@ fun TerminalView(
|
||||
}
|
||||
}
|
||||
|
||||
// Text blinking timer (500ms cycle matching j3270)
|
||||
var textBlinkVisible by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(500L)
|
||||
textBlinkVisible = !textBlinkVisible
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
@@ -235,6 +253,45 @@ fun TerminalView(
|
||||
selMaxCol = maxOf(startCol, endCol)
|
||||
}
|
||||
|
||||
// 1. Draw Vector Graphics Plane under text if present
|
||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||
val gridW = metrics.gridWidth.toInt()
|
||||
val gridH = metrics.gridHeight.toInt()
|
||||
val gWidth = graphicsPlane.canvasWidth
|
||||
val gHeight = graphicsPlane.canvasHeight
|
||||
val rgb = graphicsPlane.rgbBuffer
|
||||
if (gridW > 0 && gridH > 0 && rgb != null && gWidth > 0 && gHeight > 0) {
|
||||
val bmp = android.graphics.Bitmap.createBitmap(rgb, gWidth, gHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||
bmp,
|
||||
null,
|
||||
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Draw Crosshair Ruler if enabled
|
||||
if (crosshairRuler && cols > 0 && rows > 0) {
|
||||
val curCol = (activeCursorAddr % cols).coerceIn(0, cols - 1)
|
||||
val curRow = ((activeCursorAddr / cols) % rows).coerceIn(0, rows - 1)
|
||||
val gridW = cols * cellWidth
|
||||
val gridH = rows * cellHeight
|
||||
val cx = offsetX + curCol * cellWidth
|
||||
val cy = offsetY + curRow * cellHeight
|
||||
|
||||
drawRect(
|
||||
color = CROSSHAIR_RULER_COLOR,
|
||||
topLeft = Offset(offsetX, cy),
|
||||
size = Size(gridW, cellHeight)
|
||||
)
|
||||
drawRect(
|
||||
color = CROSSHAIR_RULER_COLOR,
|
||||
topLeft = Offset(cx, offsetY),
|
||||
size = Size(cellWidth, gridH)
|
||||
)
|
||||
}
|
||||
|
||||
for (r in 0 until rows) {
|
||||
for (c in 0 until cols) {
|
||||
val addr = r * cols + c
|
||||
@@ -249,6 +306,7 @@ fun TerminalView(
|
||||
var isBold = false
|
||||
var isUnderline = false
|
||||
var isReverse = false
|
||||
var isBlink = false
|
||||
var csVal = 0
|
||||
var ecVal = 0
|
||||
|
||||
@@ -262,7 +320,7 @@ fun TerminalView(
|
||||
}
|
||||
|
||||
// Compute 3270 color and field intensity
|
||||
fgColor = getFgColorForAttribute(ea, currentFieldEa, currentFA)
|
||||
fgColor = getFgColorForAttribute(ea, currentFieldEa, currentFA, fourColorOverride)
|
||||
bgColor = getBgColorForAttribute(ea, currentFieldEa)
|
||||
|
||||
csVal = if (ea.cs != 0.toByte()) (ea.cs.toInt() and 0xFF)
|
||||
@@ -296,6 +354,7 @@ fun TerminalView(
|
||||
if ((grVal and GR_INTENSIFY) != 0) isBold = true
|
||||
if ((grVal and GR_UNDERLINE) != 0) isUnderline = true
|
||||
if ((grVal and GR_REVERSE) != 0) isReverse = true
|
||||
if ((grVal and GR_BLINK) != 0) isBlink = true
|
||||
}
|
||||
} else {
|
||||
fgColor = HOST_COLORS[HOST_COLOR_GREEN]
|
||||
@@ -307,7 +366,7 @@ fun TerminalView(
|
||||
bgColor = tmp
|
||||
}
|
||||
|
||||
if (bgColor != COLOR_BLACK) {
|
||||
if (bgColor != COLOR_BLACK && bgColor != Color.Transparent) {
|
||||
drawRect(
|
||||
color = bgColor,
|
||||
topLeft = Offset(left, top),
|
||||
@@ -315,33 +374,52 @@ fun TerminalView(
|
||||
)
|
||||
}
|
||||
|
||||
// Draw selection box highlight if selected
|
||||
val isSelected = r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
|
||||
// Selection highlight (Block select vs Linear stream select)
|
||||
val isSelected = if (blockSelectMode) {
|
||||
r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
|
||||
} else {
|
||||
if (selMinRow == -1 || selMaxRow == -1) false
|
||||
else if (selMinRow == selMaxRow) r == selMinRow && c in selMinCol..selMaxCol
|
||||
else if (r == selMinRow) c >= selMinCol
|
||||
else if (r == selMaxRow) c <= selMaxCol
|
||||
else r in (selMinRow + 1) until selMaxRow
|
||||
}
|
||||
|
||||
val isSearchHighlighted = searchHighlightAddr >= 0 && searchHighlightLen > 0 &&
|
||||
addr >= searchHighlightAddr && addr < (searchHighlightAddr + searchHighlightLen)
|
||||
|
||||
if (isSearchHighlighted) {
|
||||
drawRect(
|
||||
color = Color(0x99FFC107), // Amber / Gold highlight for search matches
|
||||
color = SEARCH_HIGHLIGHT_COLOR,
|
||||
topLeft = Offset(left, top),
|
||||
size = Size(cellWidth, cellHeight)
|
||||
)
|
||||
} else if (isSelected) {
|
||||
drawRect(
|
||||
color = Color(0x773399FF),
|
||||
color = SELECTION_COLOR,
|
||||
topLeft = Offset(left, top),
|
||||
size = Size(cellWidth, cellHeight)
|
||||
)
|
||||
}
|
||||
|
||||
// Cursor indicator (respects blinking toggle & timer)
|
||||
// Cursor indicator (respects blinking toggle & timer, cursor style, insert mode)
|
||||
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
|
||||
val isUnderlineCursor = cursorStyle.equals("UNDERLINE", ignoreCase = true) || isInsertMode
|
||||
if (isUnderlineCursor) {
|
||||
val ulH = maxOf(4f, cellHeight / 4f)
|
||||
drawRect(
|
||||
color = HOST_COLORS[HOST_COLOR_TURQUOISE].copy(alpha = 0.5f),
|
||||
color = Color(255, 255, 255, 180),
|
||||
topLeft = Offset(left, top + cellHeight - ulH),
|
||||
size = Size(cellWidth, ulH)
|
||||
)
|
||||
} else {
|
||||
drawRect(
|
||||
color = Color(255, 255, 255, 180),
|
||||
topLeft = Offset(left, top),
|
||||
size = Size(cellWidth, cellHeight)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw Programmed Symbol (PS / APL) if defined
|
||||
var drawnAsPs = false
|
||||
@@ -364,7 +442,8 @@ fun TerminalView(
|
||||
}
|
||||
}
|
||||
|
||||
if (!drawnAsPs && charVal != ' ') {
|
||||
val suppressText = isBlink && !textBlinkVisible
|
||||
if (!drawnAsPs && charVal != ' ' && !suppressText) {
|
||||
paint.color = fgColor.toArgb()
|
||||
paint.isFakeBoldText = isBold
|
||||
val fontMetrics = paint.fontMetrics
|
||||
@@ -390,22 +469,44 @@ fun TerminalView(
|
||||
}
|
||||
}
|
||||
|
||||
// Draw Vector Graphics Plane overlay if present
|
||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||
val gridW = metrics.gridWidth.toInt()
|
||||
val gridH = metrics.gridHeight.toInt()
|
||||
if (gridW > 0 && gridH > 0) {
|
||||
graphicsPlane.resize(gridW, gridH)
|
||||
val rgb = graphicsPlane.rgbBuffer
|
||||
if (rgb != null) {
|
||||
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
|
||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||
bmp,
|
||||
null,
|
||||
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
|
||||
null
|
||||
// Draw Graphic Cursor if active in GOCA or GraphicsPlane
|
||||
val isGraphicCursorActive = (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) ||
|
||||
(graphicsPlane != null && graphicsPlane.isGraphicCursorAttached)
|
||||
if (isGraphicCursorActive && graphicsPlane != null) {
|
||||
val gocaX = if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) gocaDecoder.graphicCursorX else graphicsPlane.graphicCursorX
|
||||
val gocaY = if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) gocaDecoder.graphicCursorY else graphicsPlane.graphicCursorY
|
||||
val canvasPx = graphicsPlane.mapX(gocaX)
|
||||
val canvasPy = graphicsPlane.mapY(gocaY)
|
||||
val gridW = metrics.gridWidth
|
||||
val gridH = metrics.gridHeight
|
||||
val gWidth = graphicsPlane.canvasWidth.toFloat()
|
||||
val gHeight = graphicsPlane.canvasHeight.toFloat()
|
||||
val px = offsetX + if (gWidth > 0) (canvasPx.toFloat() * gridW / gWidth) else canvasPx.toFloat()
|
||||
val py = offsetY + if (gHeight > 0) (canvasPy.toFloat() * gridH / gHeight) else canvasPy.toFloat()
|
||||
|
||||
val shape = graphicsPlane.hodCursorShape
|
||||
if (shape == 2) {
|
||||
// Box cursor
|
||||
drawRect(
|
||||
color = Color.White,
|
||||
topLeft = Offset(px - 6f, py - 6f),
|
||||
size = Size(12f, 12f),
|
||||
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2f)
|
||||
)
|
||||
} else {
|
||||
// Crosshair cursor
|
||||
drawLine(
|
||||
color = Color.White,
|
||||
start = Offset(px - 10f, py),
|
||||
end = Offset(px + 10f, py),
|
||||
strokeWidth = 2f
|
||||
)
|
||||
drawLine(
|
||||
color = Color.White,
|
||||
start = Offset(px, py - 10f),
|
||||
end = Offset(px, py + 10f),
|
||||
strokeWidth = 2f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,7 +526,7 @@ fun TerminalView(
|
||||
text = { Text("Copy Selection") },
|
||||
onClick = {
|
||||
showContextMenu = false
|
||||
copySelection(selectionStart, selectionEnd, screenBuffer, rows, cols, clipboardManager, context)
|
||||
copySelection(selectionStart, selectionEnd, screenBuffer, rows, cols, clipboardManager, context, blockSelectMode)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
@@ -460,6 +561,65 @@ fun TerminalView(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Paste with Line Wrap...") },
|
||||
onClick = {
|
||||
showContextMenu = false
|
||||
showPasteWrapDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showPasteWrapDialog) {
|
||||
var marginText by remember { mutableStateOf(cols.toString()) }
|
||||
var wordWrap by remember { mutableStateOf(true) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPasteWrapDialog = false },
|
||||
title = { Text("Paste with Line Wrap") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Wrap text at column (1-$cols, 0 = no wrap):", fontSize = 13.sp)
|
||||
OutlinedTextField(
|
||||
value = marginText,
|
||||
onValueChange = { marginText = it },
|
||||
singleLine = true,
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||
)
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = wordWrap, onCheckedChange = { wordWrap = it })
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Word wrap lines")
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showPasteWrapDialog = false
|
||||
val clipText = clipboardManager.getText()?.text
|
||||
if (!clipText.isNullOrEmpty()) {
|
||||
val margin = marginText.trim().toIntOrNull() ?: cols
|
||||
if (onPasteLineWrap != null) {
|
||||
onPasteLineWrap(clipText, margin, wordWrap)
|
||||
} else {
|
||||
onPasteText(clipText)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Clipboard is empty", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}) {
|
||||
Text("Paste")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showPasteWrapDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -522,7 +682,8 @@ private fun copySelection(
|
||||
rows: Int,
|
||||
cols: Int,
|
||||
clipboardManager: androidx.compose.ui.platform.ClipboardManager,
|
||||
context: android.content.Context
|
||||
context: android.content.Context,
|
||||
blockSelectMode: Boolean = false
|
||||
) {
|
||||
if (start != null && end != null && screenBuffer != null && cols > 0 && rows > 0) {
|
||||
val displayMetrics = context.resources.displayMetrics
|
||||
@@ -544,7 +705,9 @@ private fun copySelection(
|
||||
val sb = StringBuilder()
|
||||
for (r in minRow..maxRow) {
|
||||
val line = StringBuilder()
|
||||
for (c in minCol..maxCol) {
|
||||
val colStart = if (blockSelectMode) minCol else (if (r == minRow) minCol else 0)
|
||||
val colEnd = if (blockSelectMode) maxCol else (if (r == maxRow) maxCol else cols - 1)
|
||||
for (c in colStart..colEnd) {
|
||||
val addr = r * cols + c
|
||||
if (addr < (rows * cols)) {
|
||||
val cell = screenBuffer.getCell(addr)
|
||||
@@ -560,12 +723,18 @@ private fun copySelection(
|
||||
val textToCopy = sb.toString().trimEnd()
|
||||
if (textToCopy.isNotBlank()) {
|
||||
clipboardManager.setText(AnnotatedString(textToCopy))
|
||||
Toast.makeText(context, "Copied selected block to clipboard", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, "Copied selection to clipboard", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFgColorForAttribute(ea: ExtendedAttribute, currentFieldEa: ExtendedAttribute?, currentFA: Byte): Color {
|
||||
internal fun getFgColorForAttribute(
|
||||
ea: ExtendedAttribute,
|
||||
currentFieldEa: ExtendedAttribute?,
|
||||
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
|
||||
@@ -573,9 +742,10 @@ private fun getFgColorForAttribute(ea: ExtendedAttribute, currentFieldEa: Extend
|
||||
if (fg in 0xf0..0xff) {
|
||||
return HOST_COLORS[fg - 0xf0]
|
||||
}
|
||||
}
|
||||
val fa = currentFA.toInt() and 0xFF
|
||||
return if (faIsProtected(fa)) {
|
||||
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_WHITE] else HOST_COLORS[HOST_COLOR_BLUE]
|
||||
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_WHITE] else HOST_COLORS[HOST_COLOR_TURQUOISE]
|
||||
} else {
|
||||
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_RED] else HOST_COLORS[HOST_COLOR_GREEN]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package haus.nightmare.a3270
|
||||
|
||||
import haus.nightmare.a3270.storage.SavedHost
|
||||
import haus.nightmare.a3270.ui.getFgColorForAttribute
|
||||
import haus.nightmare.lib3270j.ConnectionConfig
|
||||
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.screen.ExtendedAttribute
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.UUID
|
||||
|
||||
class DynamicAndParityFeaturesTest {
|
||||
|
||||
@Test
|
||||
fun testSavedHostDynamicModelSerialization() {
|
||||
val host = SavedHost(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = "Dynamic Mainframe Session",
|
||||
host = "mainframe.example.org",
|
||||
port = 23,
|
||||
model = 0,
|
||||
dynamicRows = 62,
|
||||
dynamicCols = 160,
|
||||
luName = "DYNLU01",
|
||||
autoConnect = false,
|
||||
hostType = "TSO",
|
||||
useTls = true,
|
||||
tlsVerifyCert = true,
|
||||
tn3270e = true,
|
||||
graphicsMode = "BOTH",
|
||||
codePage = "037"
|
||||
)
|
||||
|
||||
val json = host.toJson()
|
||||
assertEquals(0, json.getInt("model"))
|
||||
assertEquals(62, json.getInt("dynamicRows"))
|
||||
assertEquals(160, json.getInt("dynamicCols"))
|
||||
|
||||
val deserialized = SavedHost.fromJson(json)
|
||||
assertEquals(0, deserialized.model)
|
||||
assertEquals(62, deserialized.dynamicRows)
|
||||
assertEquals(160, deserialized.dynamicCols)
|
||||
assertEquals("Dynamic Mainframe Session", deserialized.name)
|
||||
assertEquals("mainframe.example.org", deserialized.host)
|
||||
assertEquals(23, deserialized.port)
|
||||
assertTrue(deserialized.useTls)
|
||||
assertEquals("BOTH", deserialized.graphicsMode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSavedHostBackwardCompatibilityDefaults() {
|
||||
// Legacy JSON without dynamicRows and dynamicCols
|
||||
val json = JSONObject().apply {
|
||||
put("id", "legacy-id")
|
||||
put("name", "Legacy Host")
|
||||
put("host", "legacy.host")
|
||||
put("port", 23)
|
||||
put("model", 2)
|
||||
}
|
||||
|
||||
val deserialized = SavedHost.fromJson(json)
|
||||
assertEquals(62, deserialized.dynamicRows)
|
||||
assertEquals(160, deserialized.dynamicCols)
|
||||
assertEquals(2, deserialized.model)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConnectionConfigDynamicModel() {
|
||||
val config = ConnectionConfig("10.0.0.1", 23)
|
||||
config.setDynamicDimensions(72, 140)
|
||||
|
||||
assertEquals(TerminalModel.IBM_DYNAMIC, config.model)
|
||||
assertTrue(config.isDynamicModel)
|
||||
assertEquals(72, config.dynamicRows)
|
||||
assertEquals(140, config.dynamicCols)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHodColorRemappingConstants() {
|
||||
// Base 4-Color Model parity verification:
|
||||
// Normal Protected must be Turquoise/Cyan (0x00FFFF), NOT Blue
|
||||
// Intensified Unprotected must be Red (0xFF0000)
|
||||
// Normal Unprotected is Green (0x00FF00)
|
||||
// Intensified Protected is White (0xFFFFFF)
|
||||
val turquoiseRgb = 0x00FFFF
|
||||
val redRgb = 0xFF0000
|
||||
val greenRgb = 0x00FF00
|
||||
val whiteRgb = 0xFFFFFF
|
||||
|
||||
assertEquals(0x00FFFF, turquoiseRgb)
|
||||
assertEquals(0xFF0000, redRgb)
|
||||
assertEquals(0x00FF00, greenRgb)
|
||||
assertEquals(0xFFFFFF, whiteRgb)
|
||||
|
||||
// Validate bit patterns match IBM 3270 Field Attributes
|
||||
val prot = FA_PROTECT
|
||||
val num = FA_NUMERIC
|
||||
val hi = FA_INT_HIGH_SEL
|
||||
|
||||
assertTrue((prot and FA_PROTECT) != 0)
|
||||
assertTrue((num and FA_NUMERIC) != 0)
|
||||
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,255 @@
|
||||
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.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 = """
|
||||
[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("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 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.GraphicsPlane
|
||||
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.TelnetConstants.*
|
||||
import haus.nightmare.lib3270j.protocol.TN3270EConstants.*
|
||||
@@ -175,4 +177,120 @@ class GraphicsAndSysReqTest {
|
||||
inputProcessor.sendAid(AID_SYSREQ)
|
||||
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,179 @@
|
||||
package haus.nightmare.a3270
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import haus.nightmare.lib3270j.TerminalModel
|
||||
import haus.nightmare.lib3270j.charset.CodePageRegistry
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants
|
||||
import haus.nightmare.lib3270j.input.InputProcessor
|
||||
import haus.nightmare.lib3270j.nvt.NvtProcessor
|
||||
import haus.nightmare.lib3270j.printer.PrinterDefinitionTable
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer
|
||||
|
||||
class NvtAndExtendedFeaturesTest {
|
||||
|
||||
private lateinit var screenBuffer: ScreenBuffer
|
||||
private lateinit var translator: EbcdicTranslator
|
||||
private lateinit var inputProcessor: InputProcessor
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val model = TerminalModel.IBM_3279_2
|
||||
translator = EbcdicTranslator()
|
||||
screenBuffer = ScreenBuffer(model, translator)
|
||||
inputProcessor = InputProcessor(screenBuffer, translator, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtendedCodePagesAvailability() {
|
||||
val testCps = listOf(
|
||||
"420", "424", "803", "838", "1160",
|
||||
"1025", "1123", "1154", "880", "905", "1026", "1155",
|
||||
"1140", "1141", "1142", "1143", "1144", "1145", "1146", "1147", "1148", "1149",
|
||||
"930", "939", "935", "937", "1388", "1371", "933"
|
||||
)
|
||||
|
||||
for (cp in testCps) {
|
||||
val trans = CodePageRegistry.getCodePage(cp)
|
||||
assertNotNull("Code page $cp should be registered in CodePageRegistry", trans)
|
||||
assertEquals(cp, trans.codePageId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtendedCodePageTranslation() {
|
||||
// Test Greek Cp875
|
||||
val greekTrans = CodePageRegistry.getCodePage("875")
|
||||
assertNotNull(greekTrans)
|
||||
val alphaEbcdic = greekTrans.unicodeToEbcdic('α')
|
||||
val alphaUnicode = greekTrans.ebcdicToUnicode(alphaEbcdic.toInt() and 0xFF)
|
||||
assertEquals('α', alphaUnicode)
|
||||
|
||||
// Test Cyrillic Cp1025
|
||||
val cyrTrans = CodePageRegistry.getCodePage("1025")
|
||||
assertNotNull(cyrTrans)
|
||||
val cyrA = 'А' // Cyrillic capital A
|
||||
val cyrEbcdic = cyrTrans.unicodeToEbcdic(cyrA)
|
||||
val cyrResult = cyrTrans.ebcdicToUnicode(cyrEbcdic.toInt() and 0xFF)
|
||||
assertEquals(cyrA, cyrResult)
|
||||
|
||||
// Test Euro variant Cp1140 has € symbol at 0x9F
|
||||
val euroTrans = CodePageRegistry.getCodePage("1140")
|
||||
assertNotNull(euroTrans)
|
||||
val euroUnicode = euroTrans.ebcdicToUnicode(0x9F)
|
||||
assertEquals('€', euroUnicode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPDTTables() {
|
||||
val pcl5 = PrinterDefinitionTable.createPcl5PDT()
|
||||
assertNotNull(pcl5)
|
||||
assertEquals("PCL_5", pcl5.name)
|
||||
assertNotNull(pcl5.getControlCode(PrinterDefinitionTable.CMD_START_JOB))
|
||||
|
||||
val escp = PrinterDefinitionTable.createEpsonEscPPDT()
|
||||
assertNotNull(escp)
|
||||
assertEquals("EPSON_ESC_P", escp.name)
|
||||
assertNotNull(escp.getControlCode(PrinterDefinitionTable.CMD_START_BOLD))
|
||||
|
||||
val ps = PrinterDefinitionTable.createPostScriptPDT()
|
||||
assertNotNull(ps)
|
||||
assertEquals("POSTSCRIPT", ps.name)
|
||||
val psStart = String(ps.getControlCode(PrinterDefinitionTable.CMD_START_JOB) ?: ByteArray(0), java.nio.charset.StandardCharsets.ISO_8859_1)
|
||||
assertTrue(psStart.contains("%!PS"))
|
||||
|
||||
val plain = PrinterDefinitionTable.createPlainTextPDT()
|
||||
assertNotNull(plain)
|
||||
assertEquals("PLAIN_TEXT", plain.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWordNavigationAndEditing() {
|
||||
val text = "IBM MAINFRAME SYSTEM"
|
||||
for (i in text.indices) {
|
||||
screenBuffer.getCell(i).ucs4 = text[i]
|
||||
screenBuffer.getCell(i).ec = translator.unicodeToEbcdic(text[i]).toByte()
|
||||
}
|
||||
|
||||
screenBuffer.cursorAddress = 0
|
||||
inputProcessor.processWordRight()
|
||||
assertEquals(5, screenBuffer.cursorAddress)
|
||||
|
||||
inputProcessor.processWordRight()
|
||||
assertEquals(16, screenBuffer.cursorAddress)
|
||||
|
||||
inputProcessor.processWordLeft()
|
||||
assertEquals(5, screenBuffer.cursorAddress)
|
||||
|
||||
inputProcessor.processWordLeft()
|
||||
assertEquals(0, screenBuffer.cursorAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFieldEndAndToggleInsert() {
|
||||
screenBuffer.setCellFA(0, FA_PRINTABLE.toByte())
|
||||
screenBuffer.setCellFA(20, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||
|
||||
val text = "HELLO"
|
||||
for (i in text.indices) {
|
||||
screenBuffer.getCell(1 + i).ucs4 = text[i]
|
||||
screenBuffer.getCell(1 + i).ec = translator.unicodeToEbcdic(text[i]).toByte()
|
||||
}
|
||||
|
||||
screenBuffer.cursorAddress = 1
|
||||
inputProcessor.processFieldEnd()
|
||||
assertEquals(6, screenBuffer.cursorAddress)
|
||||
|
||||
assertFalse(inputProcessor.isInsertMode)
|
||||
inputProcessor.processToggleInsert()
|
||||
assertTrue(inputProcessor.isInsertMode)
|
||||
inputProcessor.processToggleInsert()
|
||||
assertFalse(inputProcessor.isInsertMode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNvtFunctionKeySequences() {
|
||||
val nvt = NvtProcessor(screenBuffer, null)
|
||||
|
||||
val f1 = nvt.getFunctionKeySequence(1)
|
||||
val f5 = nvt.getFunctionKeySequence(5)
|
||||
val f10 = nvt.getFunctionKeySequence(10)
|
||||
|
||||
assertNotNull(f1)
|
||||
assertNotNull(f5)
|
||||
assertNotNull(f10)
|
||||
assertTrue(f1.startsWith("\u001B"))
|
||||
assertTrue(f5.startsWith("\u001B"))
|
||||
assertTrue(f10.startsWith("\u001B"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testECLFieldWrapping() {
|
||||
val totalCells = screenBuffer.rows * screenBuffer.cols
|
||||
val faPos = totalCells - 10
|
||||
screenBuffer.setCellFA(faPos, FA_PRINTABLE.toByte())
|
||||
screenBuffer.setCellFA(15, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||
|
||||
val fieldList = screenBuffer.buildFieldList()
|
||||
assertNotNull(fieldList)
|
||||
val wrappedField = fieldList.findField(totalCells - 5)
|
||||
assertNotNull(wrappedField)
|
||||
assertTrue("Field spanning buffer boundary should be marked wrapped", wrappedField.isWrapped)
|
||||
assertTrue(wrappedField.contains(totalCells - 2))
|
||||
assertTrue(wrappedField.contains(5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOiaInhibitConstants() {
|
||||
assertEquals(1, ECLConstants.INHIBIT_SYSTEM_LOCK)
|
||||
assertEquals(0, ECLConstants.INHIBIT_NOT_INHIBITED)
|
||||
assertEquals(5, ECLConstants.INHIBIT_COMM_CHECK)
|
||||
assertEquals(3, ECLConstants.INHIBIT_PROTECTED_FIELD)
|
||||
assertEquals(2, ECLConstants.INHIBIT_NUMERIC_ONLY)
|
||||
assertEquals(4, ECLConstants.INHIBIT_OVERFLOW)
|
||||
assertEquals(6, ECLConstants.INHIBIT_OPERATOR_DUE)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user