Compare commits
5 Commits
v1.0.2
...
88e3ffcbe9
| Author | SHA1 | Date | |
|---|---|---|---|
|
88e3ffcbe9
|
|||
|
f389e626ea
|
|||
|
0eaab32e2f
|
|||
|
da289730db
|
|||
|
f77f51474c
|
@@ -41,6 +41,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
chmod +x ./gradlew ./build.sh
|
chmod +x ./gradlew ./build.sh
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
run: |
|
||||||
|
./gradlew testDebugUnitTest --no-daemon
|
||||||
|
env:
|
||||||
|
J3270_DIR: ${{ github.workspace }}/j3270
|
||||||
|
|
||||||
- name: Build Android APK
|
- name: Build Android APK
|
||||||
run: |
|
run: |
|
||||||
./gradlew assembleDebug --no-daemon
|
./gradlew assembleDebug --no-daemon
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ captures/
|
|||||||
.cxx
|
.cxx
|
||||||
*.jar
|
*.jar
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
*.txt
|
||||||
|
|||||||
@@ -34,20 +34,70 @@ adb install -r a3270.apk
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📱 Compatibility & System Requirements
|
||||||
|
|
||||||
|
- **Minimum Android Version**: Android 7.0 (Nougat) — **API Level 24** (`minSdk 24`)
|
||||||
|
- **Target Android Version**: Android 14 — **API Level 34** (`targetSdk 34`)
|
||||||
|
- **Compile SDK**: **API Level 34** (`compileSdk 34`)
|
||||||
|
- **Architecture**: Universal — pure Kotlin/Java running on ART, fully compatible with all CPU ABIs (`arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64`)
|
||||||
|
- **Form Factors**: Adaptive UI scaling optimized for smartphones, foldables, tablets, and Chromebooks (Android runtime)
|
||||||
|
- **Input Devices**: Full touch-screen support, native virtual keyboards, and external physical/Bluetooth keyboards
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Modern Jetpack Compose UI**: Responsive, touch-optimized 3270 terminal screen rendering.
|
### 🖥️ Terminal Emulation & Protocol
|
||||||
- **TN3270 & TN3270E Protocol Support**: RFC 2355 compliant state machine, negotiation, Device-Type query, and SSL/TLS encryption via `lib3270j`.
|
- **RFC 2355 TN3270 & TN3270E Engine**: Robust state machine, Option Negotiation, Device-Type Query, and LU name specification powered by `lib3270j`.
|
||||||
- **On-Screen Function Key Bar**: Quick-access touch bar for PF1–PF24, PA1–PA3, Enter, Clear, Reset, and Attn keys.
|
- **Standard & Dynamic Terminal Models**:
|
||||||
- **IND$FILE File Transfer**: Full mobile UI for **CUT** and **DFT (DDM)** high-speed structured field file transfers with ASCII/binary translation, CRLF handling, and recfm/lrecl formatting for TSO, VM/CMS, and CICS.
|
- Model 2 (24 × 80)
|
||||||
- **OIA Status Line**: Real-time Operator Information Area displaying connection state, keyboard lock, insert mode, cursor row/col, and LU name.
|
- Model 3 (32 × 80)
|
||||||
- **Host Profile Storage**: Save and manage multiple mainframe connection profiles with custom ports, models, and SSL/TLS options.
|
- Model 4 (43 × 80)
|
||||||
- **Background Session Persistence**: Android foreground service keeps your terminal session alive when multitasking.
|
- Model 5 (27 × 132)
|
||||||
- **Terminal Models**:
|
- **Dynamic Screen Dimensions**: Configurable custom geometry (rows & columns up to 255 × 255).
|
||||||
- Model 2 (24x80)
|
- **EBCDIC Code Pages**: Full character translation across international code pages (037, 1047, 500, etc.).
|
||||||
- Model 3 (32x80)
|
|
||||||
- Model 4 (43x80)
|
### 📊 Graphics & Peripherals
|
||||||
- Model 5 (27x132)
|
- **GDDM & GOCA Graphics**: Native rendering of Programmed Symbols (PS) and GOCA vector graphics directly in the Compose canvas.
|
||||||
|
- **Light Pen Simulation**: Interactive touch support to simulate light pen selection and graphic cursor pick correlation.
|
||||||
|
- **IBM 3287 Printer Emulation**: Integrated printer session support for spool/report capture (via associated display LU or dedicated printer LU) with direct printing through Android PrintManager (PDF export / system print).
|
||||||
|
|
||||||
|
### 📁 IND$FILE File Transfer
|
||||||
|
- Full mobile UI supporting both **CUT** and **DFT (DDM)** high-speed structured field transfers.
|
||||||
|
- Compatible with **TSO**, **VM/CMS**, and **CICS** environments.
|
||||||
|
- ASCII/binary translation, CRLF conversions, and configurable record format (`recfm`), record length (`lrecl`), and allocation units.
|
||||||
|
|
||||||
|
### 🔍 Developer & Productivity Tools
|
||||||
|
- **Field Inspector**: Live interactive inspector displaying start/end addresses, length, protection, numeric status, MDT (Modified Data Tag), display intensity, and extended highlighting/color, with one-tap "Jump to Field".
|
||||||
|
- **Screen Search / Find**: On-screen text search with case sensitivity, forward/backward navigation, and visual match highlighting.
|
||||||
|
- **Screen Exporter**: Export the current screen buffer to **Plain Text**, formatted **HTML**, or **PNG image**, with one-tap clipboard copy and Android system share sheet integration.
|
||||||
|
- **Script & Macro Automation**: Built-in script runner supporting automated keystroke and wait sequences (e.g. `[enter]`, `[wait]`, `[tab]`) for login macros and repetitive tasks.
|
||||||
|
|
||||||
|
### ✍️ Entry Assist & Typing Modes
|
||||||
|
- **Document (DOC) Mode**: Configurable left/right margins, custom tab stops, and end-of-line bell warning.
|
||||||
|
- **Word Wrap**: Automatic word wrapping across lines during input.
|
||||||
|
- **APL Mode**: Support for APL extended character sets and symbols.
|
||||||
|
- **Field Handling**: Automatic field skip (Auto-Skip), Numeric field lock, and Insert Mode auto-reset on AID transmission.
|
||||||
|
|
||||||
|
### 🎨 Display, Themes & Ergonomics
|
||||||
|
- **Jetpack Compose UI**: Smooth, hardware-accelerated 3270 terminal screen rendering with touch-to-position cursor.
|
||||||
|
- **Color Themes**: Light and Dark application themes, 3270 extended color support, and 4-color legacy override mode.
|
||||||
|
- **Cursor Customization**: Block and Underline cursor styles with toggleable blinking.
|
||||||
|
- **Crosshair Ruler**: Toggleable horizontal and vertical ruler lines following the cursor for easy data tracking.
|
||||||
|
- **Block Selection**: Rectangular text selection and copy mode.
|
||||||
|
- **OIA Status Line**: Real-time Operator Information Area displaying connection status, keyboard lock, insert mode, cursor row/col, LU name, and inhibit reason codes.
|
||||||
|
|
||||||
|
### 🔒 Security, Networking & Storage
|
||||||
|
- **SSL / TLS Encryption**: Secure encrypted mainframe sessions with TLS certificate verification.
|
||||||
|
- **Interactive Untrusted Certificate Prompt**: Inspect SSL certificates, view full certificate chain details, and accept/reject unverified connections.
|
||||||
|
- **Proxy Support**: Connect via HTTP CONNECT, SOCKS4, SOCKS5, and Telnet proxies with optional user/password authentication.
|
||||||
|
- **Background Session Persistence**: Android foreground service (`specialUse`) keeps your terminal TCP connection alive with status notifications when multitasking or screen is off.
|
||||||
|
- **Host Profile Manager & INI Export**: Save multiple mainframe profiles, configure auto-connect on launch, and export/import configuration in INI format (interoperable with desktop `j3270`).
|
||||||
|
|
||||||
|
### ⌨️ Input & Keyboards
|
||||||
|
- **On-Screen Function Key Bar**: Two-row responsive touch bar providing PF1–PF24, PA1–PA3, Enter, Clear, Reset, Attn, SysReq, Erase EOF, Erase Input, Tab, BackTab, and D-pad cursor controls with haptic feedback.
|
||||||
|
- **Termux-Style Native Input**: Low-latency native input connection with permanent number row and lock protection against soft-keyboard resets.
|
||||||
|
- **Hardware Keyboard Support**: Complete physical keyboard integration with F1–F12 (mapped to PF1–PF12 / PF13–PF24 with Shift/Alt), Ctrl shortcuts, dead keys, and international character input.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,6 +106,7 @@ adb install -r a3270.apk
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
- **Java Development Kit (JDK)**: JDK 17 or JDK 21
|
- **Java Development Kit (JDK)**: JDK 17 or JDK 21
|
||||||
- **Android SDK**: API 34 platforms and build-tools (`ANDROID_HOME` or `ANDROID_SDK_ROOT` set)
|
- **Android SDK**: API 34 platforms and build-tools (`ANDROID_HOME` or `ANDROID_SDK_ROOT` set)
|
||||||
|
- **Minimum Target Device**: Android 7.0+ (API 24+)
|
||||||
|
|
||||||
### 1. Clone Repositories Side-by-Side
|
### 1. Clone Repositories Side-by-Side
|
||||||
For the easiest setup, clone both `j3270` and `a3270` into the same parent folder:
|
For the easiest setup, clone both `j3270` and `a3270` into the same parent folder:
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ if [ ! -d "$J3270_PATH/lib3270j" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Building debug APK..."
|
echo "Building debug APK..."
|
||||||
./gradlew assembleDebug
|
./gradlew assembleDebug --no-daemon
|
||||||
|
|
||||||
APK_PATH="$SCRIPT_DIR/build/outputs/apk/debug/a3270-debug.apk"
|
APK_PATH="$SCRIPT_DIR/build/outputs/apk/debug/a3270-debug.apk"
|
||||||
if [ -f "$APK_PATH" ]; then
|
if [ -f "$APK_PATH" ]; then
|
||||||
|
|||||||
@@ -191,6 +191,29 @@ class MainActivity : ComponentActivity() {
|
|||||||
val alt = event.isAltPressed || (event.metaState and KeyEvent.META_ALT_ON != 0)
|
val alt = event.isAltPressed || (event.metaState and KeyEvent.META_ALT_ON != 0)
|
||||||
|
|
||||||
// 1. Function Keys (F1..F12 -> PF1..PF12 or PF13..PF24 with Shift or Alt)
|
// 1. Function Keys (F1..F12 -> PF1..PF12 or PF13..PF24 with Shift or Alt)
|
||||||
|
// Mode toggles matching j3270:
|
||||||
|
// Alt+F1: DOC mode (Entry Assist)
|
||||||
|
// Alt+F2: Word Wrap
|
||||||
|
// Alt+F3: APL Mode
|
||||||
|
if (alt && keyCode == KeyEvent.KEYCODE_F1) {
|
||||||
|
viewModel.toggleDocMode()
|
||||||
|
val isDoc = viewModel.isDocMode.value
|
||||||
|
Toast.makeText(this, "DOC Mode: " + (if (isDoc) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (alt && keyCode == KeyEvent.KEYCODE_F2) {
|
||||||
|
viewModel.toggleWordWrap()
|
||||||
|
val isWrap = viewModel.isWordWrap.value
|
||||||
|
Toast.makeText(this, "Word Wrap: " + (if (isWrap) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (alt && keyCode == KeyEvent.KEYCODE_F3) {
|
||||||
|
viewModel.toggleAplMode()
|
||||||
|
val isApl = viewModel.isAplMode.value
|
||||||
|
Toast.makeText(this, "APL Mode: " + (if (isApl) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if (keyCode in KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12) {
|
if (keyCode in KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12) {
|
||||||
val fNum = keyCode - KeyEvent.KEYCODE_F1 + 1
|
val fNum = keyCode - KeyEvent.KEYCODE_F1 + 1
|
||||||
val pfNum = if (shift || alt) fNum + 12 else fNum
|
val pfNum = if (shift || alt) fNum + 12 else fNum
|
||||||
@@ -339,6 +362,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
// 5. Standard Navigation & Special Keys
|
// 5. Standard Navigation & Special Keys
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_INSERT -> {
|
||||||
|
viewModel.toggleInsert()
|
||||||
|
return true
|
||||||
|
}
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||||
viewModel.cursorLeft()
|
viewModel.cursorLeft()
|
||||||
return true
|
return true
|
||||||
@@ -503,6 +530,10 @@ fun MainScreen(
|
|||||||
val blockSelectMode by viewModel.blockSelectMode.collectAsState()
|
val blockSelectMode by viewModel.blockSelectMode.collectAsState()
|
||||||
val uiTheme by viewModel.uiTheme.collectAsState()
|
val uiTheme by viewModel.uiTheme.collectAsState()
|
||||||
val isInsertMode by viewModel.isInsertMode.collectAsState()
|
val isInsertMode by viewModel.isInsertMode.collectAsState()
|
||||||
|
val isDocMode by viewModel.isDocMode.collectAsState()
|
||||||
|
val isWordWrap by viewModel.isWordWrap.collectAsState()
|
||||||
|
val isAplMode by viewModel.isAplMode.collectAsState()
|
||||||
|
val fourColorOverride by viewModel.fourColorOverride.collectAsState()
|
||||||
|
|
||||||
val isNumericField = remember(screenBuffer, cursorAddr, screenVersion) {
|
val isNumericField = remember(screenBuffer, cursorAddr, screenVersion) {
|
||||||
if (screenBuffer != null && screenBuffer?.isFormatted == true) {
|
if (screenBuffer != null && screenBuffer?.isFormatted == true) {
|
||||||
@@ -510,15 +541,6 @@ fun MainScreen(
|
|||||||
(fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0
|
(fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
val modelName = remember(rows, cols) {
|
|
||||||
when {
|
|
||||||
rows == 24 && cols == 80 -> "3279-2"
|
|
||||||
rows == 32 && cols == 80 -> "3279-3"
|
|
||||||
rows == 43 && cols == 80 -> "3279-4"
|
|
||||||
rows == 27 && cols == 132 -> "3279-5"
|
|
||||||
else -> "DYNAMIC"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var showConnectDialog by remember { mutableStateOf(false) }
|
var showConnectDialog by remember { mutableStateOf(false) }
|
||||||
var showFtDialog by remember { mutableStateOf(false) }
|
var showFtDialog by remember { mutableStateOf(false) }
|
||||||
@@ -529,8 +551,11 @@ fun MainScreen(
|
|||||||
var showScriptDialog by remember { mutableStateOf(false) }
|
var showScriptDialog by remember { mutableStateOf(false) }
|
||||||
var showPrinterSessionDialog by remember { mutableStateOf(false) }
|
var showPrinterSessionDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(isFindDialogOpen) {
|
LaunchedEffect(isFindDialogOpen) {
|
||||||
if (isFindDialogOpen) {
|
if (isFindDialogOpen) {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
showFindDialog = true
|
showFindDialog = true
|
||||||
onSetFindDialogOpen(false)
|
onSetFindDialogOpen(false)
|
||||||
}
|
}
|
||||||
@@ -538,13 +563,12 @@ fun MainScreen(
|
|||||||
|
|
||||||
LaunchedEffect(isFtDialogOpen) {
|
LaunchedEffect(isFtDialogOpen) {
|
||||||
if (isFtDialogOpen) {
|
if (isFtDialogOpen) {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
showFtDialog = true
|
showFtDialog = true
|
||||||
onSetFtDialogOpen(false)
|
onSetFtDialogOpen(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
|
|
||||||
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
DisposableEffect(lifecycleOwner) {
|
DisposableEffect(lifecycleOwner) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
@@ -636,6 +660,7 @@ fun MainScreen(
|
|||||||
cursorStyle = cursorStyle,
|
cursorStyle = cursorStyle,
|
||||||
isInsertMode = isInsertMode,
|
isInsertMode = isInsertMode,
|
||||||
blockSelectMode = blockSelectMode,
|
blockSelectMode = blockSelectMode,
|
||||||
|
fourColorOverride = fourColorOverride,
|
||||||
onPasteLineWrap = { text, wrapCol, wordWrap ->
|
onPasteLineWrap = { text, wrapCol, wordWrap ->
|
||||||
viewModel.pasteLineWrap(text, wrapCol, wordWrap)
|
viewModel.pasteLineWrap(text, wrapCol, wordWrap)
|
||||||
},
|
},
|
||||||
@@ -691,18 +716,56 @@ fun MainScreen(
|
|||||||
connectionState = connectionState,
|
connectionState = connectionState,
|
||||||
isShiftPressed = isShiftPressed,
|
isShiftPressed = isShiftPressed,
|
||||||
isLightPenMode = isLightPenMode,
|
isLightPenMode = isLightPenMode,
|
||||||
|
isDocMode = isDocMode,
|
||||||
|
isWordWrap = isWordWrap,
|
||||||
|
isAplMode = isAplMode,
|
||||||
|
isInsertMode = isInsertMode,
|
||||||
|
fourColorOverride = fourColorOverride,
|
||||||
hapticFeedbackEnabled = hapticFeedback,
|
hapticFeedbackEnabled = hapticFeedback,
|
||||||
onClearShift = onClearShift,
|
onClearShift = onClearShift,
|
||||||
onConnectClick = { showConnectDialog = true },
|
onConnectClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showConnectDialog = true
|
||||||
|
},
|
||||||
onDisconnectClick = { viewModel.disconnect() },
|
onDisconnectClick = { viewModel.disconnect() },
|
||||||
onFtClick = { showFtDialog = true },
|
onFtClick = {
|
||||||
onFieldInspectorClick = { showFieldInspectorDialog = true },
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
onFindClick = { showFindDialog = true },
|
showFtDialog = true
|
||||||
onScreenExportClick = { showScreenExportDialog = true },
|
},
|
||||||
onScriptClick = { showScriptDialog = true },
|
onFieldInspectorClick = {
|
||||||
onPrinterSessionClick = { showPrinterSessionDialog = true },
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showFieldInspectorDialog = true
|
||||||
|
},
|
||||||
|
onFindClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showFindDialog = true
|
||||||
|
},
|
||||||
|
onScreenExportClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showScreenExportDialog = true
|
||||||
|
},
|
||||||
|
onScriptClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showScriptDialog = true
|
||||||
|
},
|
||||||
|
onPrinterSessionClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showPrinterSessionDialog = true
|
||||||
|
},
|
||||||
onToggleLightPen = { viewModel.toggleLightPen() },
|
onToggleLightPen = { viewModel.toggleLightPen() },
|
||||||
onSettingsClick = { showSettingsDialog = true },
|
onToggleDocMode = { viewModel.toggleDocMode() },
|
||||||
|
onToggleWordWrap = { viewModel.toggleWordWrap() },
|
||||||
|
onToggleAplMode = { viewModel.toggleAplMode() },
|
||||||
|
onToggleInsert = { viewModel.toggleInsert() },
|
||||||
|
onToggleFourColorOverride = {
|
||||||
|
val next = !fourColorOverride
|
||||||
|
viewModel.setFourColorOverride(next)
|
||||||
|
AppSettings.setFourColorOverride(context, next)
|
||||||
|
},
|
||||||
|
onSettingsClick = {
|
||||||
|
terminalInputViewRef?.hideSoftKeyboard()
|
||||||
|
showSettingsDialog = true
|
||||||
|
},
|
||||||
onSendAid = { aid ->
|
onSendAid = { aid ->
|
||||||
viewModel.sendAid(aid)
|
viewModel.sendAid(aid)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
@@ -769,20 +832,29 @@ fun MainScreen(
|
|||||||
isTls = isTlsActive,
|
isTls = isTlsActive,
|
||||||
isTlsVerified = isTlsVerified,
|
isTlsVerified = isTlsVerified,
|
||||||
graphicsMode = defaultGraphicsMode,
|
graphicsMode = defaultGraphicsMode,
|
||||||
codePage = codePage,
|
|
||||||
isLightPenMode = isLightPenMode,
|
isLightPenMode = isLightPenMode,
|
||||||
isInsertMode = isInsertMode,
|
isInsertMode = isInsertMode,
|
||||||
|
isDocMode = isDocMode,
|
||||||
|
isWordWrap = isWordWrap,
|
||||||
|
isAplMode = isAplMode,
|
||||||
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
|
||||||
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
|
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
|
||||||
isNumericField = isNumericField,
|
isNumericField = isNumericField,
|
||||||
modelName = modelName
|
uiTheme = uiTheme,
|
||||||
|
onToggleDocMode = { viewModel.toggleDocMode() },
|
||||||
|
onToggleWordWrap = { viewModel.toggleWordWrap() },
|
||||||
|
onToggleAplMode = { viewModel.toggleAplMode() },
|
||||||
|
onToggleInsert = { viewModel.toggleInsert() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showConnectDialog) {
|
if (showConnectDialog) {
|
||||||
ConnectDialog(
|
ConnectDialog(
|
||||||
onDismiss = { showConnectDialog = false },
|
onDismiss = {
|
||||||
|
showConnectDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onConnect = { host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass ->
|
onConnect = { host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass ->
|
||||||
showConnectDialog = false
|
showConnectDialog = false
|
||||||
viewModel.connect(host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
|
viewModel.connect(host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
|
||||||
@@ -796,7 +868,10 @@ fun MainScreen(
|
|||||||
initialHostType = viewModel.activeHostType,
|
initialHostType = viewModel.activeHostType,
|
||||||
ftProgressState = ftState,
|
ftProgressState = ftState,
|
||||||
client = viewModel.getClient(),
|
client = viewModel.getClient(),
|
||||||
onDismiss = { showFtDialog = false },
|
onDismiss = {
|
||||||
|
showFtDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onStartTransfer = { config ->
|
onStartTransfer = { config ->
|
||||||
viewModel.startFileTransfer(config) { error ->
|
viewModel.startFileTransfer(config) { error ->
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
@@ -825,8 +900,23 @@ fun MainScreen(
|
|||||||
initialStartupBehavior = AppSettings.getStartupBehavior(context).name,
|
initialStartupBehavior = AppSettings.getStartupBehavior(context).name,
|
||||||
initialDynamicRows = AppSettings.getDynamicRows(context),
|
initialDynamicRows = AppSettings.getDynamicRows(context),
|
||||||
initialDynamicCols = AppSettings.getDynamicCols(context),
|
initialDynamicCols = AppSettings.getDynamicCols(context),
|
||||||
onDismiss = { showSettingsDialog = false },
|
initialEntryAssistDocMode = isDocMode,
|
||||||
onSave = { mask, blink, haptic, verify, gfx, defaultCp, theme, crosshair, style, blockSel, startup, dynRows, dynCols ->
|
initialEntryAssistWordWrap = isWordWrap,
|
||||||
|
initialEntryAssistStartCol = AppSettings.getEntryAssistStartCol(context),
|
||||||
|
initialEntryAssistEndCol = AppSettings.getEntryAssistEndCol(context),
|
||||||
|
initialEntryAssistBell = AppSettings.getEntryAssistBell(context),
|
||||||
|
initialEntryAssistBellCol = AppSettings.getEntryAssistBellCol(context),
|
||||||
|
initialEntryAssistTabStops = AppSettings.getEntryAssistTabStops(context),
|
||||||
|
initialInsertOffOnAid = AppSettings.getInsertOffOnAid(context),
|
||||||
|
initialFourColorOverride = fourColorOverride,
|
||||||
|
initialNumericFieldLock = AppSettings.getNumericFieldLock(context),
|
||||||
|
initialAutoSkipEnabled = AppSettings.getAutoSkipEnabled(context),
|
||||||
|
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
|
showSettingsDialog = false
|
||||||
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
|
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
|
||||||
viewModel.setUiTheme(theme)
|
viewModel.setUiTheme(theme)
|
||||||
@@ -836,6 +926,18 @@ fun MainScreen(
|
|||||||
AppSettings.setStartupBehavior(context, AppSettings.StartupBehavior.valueOf(startup))
|
AppSettings.setStartupBehavior(context, AppSettings.StartupBehavior.valueOf(startup))
|
||||||
AppSettings.setDynamicRows(context, dynRows)
|
AppSettings.setDynamicRows(context, dynRows)
|
||||||
AppSettings.setDynamicCols(context, dynCols)
|
AppSettings.setDynamicCols(context, dynCols)
|
||||||
|
AppSettings.setEntryAssistDocMode(context, doc)
|
||||||
|
AppSettings.setEntryAssistWordWrap(context, wrap)
|
||||||
|
AppSettings.setEntryAssistStartCol(context, startCol)
|
||||||
|
AppSettings.setEntryAssistEndCol(context, endCol)
|
||||||
|
AppSettings.setEntryAssistBell(context, bell)
|
||||||
|
AppSettings.setEntryAssistBellCol(context, bellCol)
|
||||||
|
AppSettings.setEntryAssistTabStops(context, tabs)
|
||||||
|
AppSettings.setInsertOffOnAid(context, resetIns)
|
||||||
|
AppSettings.setFourColorOverride(context, fourCol)
|
||||||
|
AppSettings.setNumericFieldLock(context, numLk)
|
||||||
|
AppSettings.setAutoSkipEnabled(context, autoSk)
|
||||||
|
viewModel.reloadSettings()
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -845,7 +947,10 @@ fun MainScreen(
|
|||||||
FieldInspectorDialog(
|
FieldInspectorDialog(
|
||||||
client = viewModel.getClient(),
|
client = viewModel.getClient(),
|
||||||
screenVersion = screenVersion,
|
screenVersion = screenVersion,
|
||||||
onDismiss = { showFieldInspectorDialog = false },
|
onDismiss = {
|
||||||
|
showFieldInspectorDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onJumpToField = { addr ->
|
onJumpToField = { addr ->
|
||||||
viewModel.setCursor(addr)
|
viewModel.setCursor(addr)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
@@ -856,7 +961,10 @@ fun MainScreen(
|
|||||||
if (showFindDialog) {
|
if (showFindDialog) {
|
||||||
FindDialog(
|
FindDialog(
|
||||||
cols = cols,
|
cols = cols,
|
||||||
onDismiss = { showFindDialog = false },
|
onDismiss = {
|
||||||
|
showFindDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onFind = { query: String, matchCase: Boolean, forward: Boolean ->
|
onFind = { query: String, matchCase: Boolean, forward: Boolean ->
|
||||||
viewModel.find(query, matchCase, forward)
|
viewModel.find(query, matchCase, forward)
|
||||||
},
|
},
|
||||||
@@ -874,13 +982,19 @@ fun MainScreen(
|
|||||||
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
||||||
graphicsPlane = viewModel.getClient()?.graphicsPlane,
|
graphicsPlane = viewModel.getClient()?.graphicsPlane,
|
||||||
maskHidden = maskHiddenInput,
|
maskHidden = maskHiddenInput,
|
||||||
onDismiss = { showScreenExportDialog = false }
|
onDismiss = {
|
||||||
|
showScreenExportDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showScriptDialog) {
|
if (showScriptDialog) {
|
||||||
ScriptDialog(
|
ScriptDialog(
|
||||||
onDismiss = { showScriptDialog = false },
|
onDismiss = {
|
||||||
|
showScriptDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
},
|
||||||
onExecuteScript = { script: String, onStatus: (String) -> Unit, onComplete: (String?) -> Unit ->
|
onExecuteScript = { script: String, onStatus: (String) -> Unit, onComplete: (String?) -> Unit ->
|
||||||
viewModel.executeScript(script, onStatus, onComplete)
|
viewModel.executeScript(script, onStatus, onComplete)
|
||||||
}
|
}
|
||||||
@@ -893,7 +1007,10 @@ fun MainScreen(
|
|||||||
initialHost = activeHost,
|
initialHost = activeHost,
|
||||||
initialCodePage = codePage,
|
initialCodePage = codePage,
|
||||||
initialUseTls = isTlsActive,
|
initialUseTls = isTlsActive,
|
||||||
onDismiss = { showPrinterSessionDialog = false }
|
onDismiss = {
|
||||||
|
showPrinterSessionDialog = false
|
||||||
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ sealed interface TerminalInputAction {
|
|||||||
data object Dup : TerminalInputAction
|
data object Dup : TerminalInputAction
|
||||||
data object FieldMark : TerminalInputAction
|
data object FieldMark : TerminalInputAction
|
||||||
data object ToggleInsert : TerminalInputAction
|
data object ToggleInsert : TerminalInputAction
|
||||||
|
data object ToggleDocMode : TerminalInputAction
|
||||||
|
data object ToggleWordWrap : TerminalInputAction
|
||||||
|
data object ToggleApl : TerminalInputAction
|
||||||
|
data class SetDocMode(val enabled: Boolean) : TerminalInputAction
|
||||||
|
data class SetWordWrap(val enabled: Boolean) : TerminalInputAction
|
||||||
|
data class SetAplMode(val enabled: Boolean) : TerminalInputAction
|
||||||
data class SetCursor(val baddr: Int) : TerminalInputAction
|
data class SetCursor(val baddr: Int) : TerminalInputAction
|
||||||
data class LightPenSelect(val baddr: Int) : TerminalInputAction
|
data class LightPenSelect(val baddr: Int) : TerminalInputAction
|
||||||
}
|
}
|
||||||
@@ -161,6 +167,18 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _isInsertMode = MutableStateFlow(false)
|
private val _isInsertMode = MutableStateFlow(false)
|
||||||
val isInsertMode: StateFlow<Boolean> = _isInsertMode.asStateFlow()
|
val isInsertMode: StateFlow<Boolean> = _isInsertMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _isDocMode = MutableStateFlow(AppSettings.getEntryAssistDocMode(application))
|
||||||
|
val isDocMode: StateFlow<Boolean> = _isDocMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _isWordWrap = MutableStateFlow(AppSettings.getEntryAssistWordWrap(application))
|
||||||
|
val isWordWrap: StateFlow<Boolean> = _isWordWrap.asStateFlow()
|
||||||
|
|
||||||
|
private val _isAplMode = MutableStateFlow(false)
|
||||||
|
val isAplMode: StateFlow<Boolean> = _isAplMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _fourColorOverride = MutableStateFlow(AppSettings.getFourColorOverride(application))
|
||||||
|
val fourColorOverride: StateFlow<Boolean> = _fourColorOverride.asStateFlow()
|
||||||
|
|
||||||
fun toggleLightPen() {
|
fun toggleLightPen() {
|
||||||
_isLightPenMode.value = !_isLightPenMode.value
|
_isLightPenMode.value = !_isLightPenMode.value
|
||||||
}
|
}
|
||||||
@@ -193,6 +211,74 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_uiTheme.value = theme
|
_uiTheme.value = theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggleDocMode() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.ToggleDocMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDocMode(enabled: Boolean) {
|
||||||
|
inputChannel.trySend(TerminalInputAction.SetDocMode(enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleWordWrap() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.ToggleWordWrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWordWrap(enabled: Boolean) {
|
||||||
|
inputChannel.trySend(TerminalInputAction.SetWordWrap(enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAplMode() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.ToggleApl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAplMode(enabled: Boolean) {
|
||||||
|
inputChannel.trySend(TerminalInputAction.SetAplMode(enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFourColorOverride(override: Boolean) {
|
||||||
|
AppSettings.setFourColorOverride(getApplication(), override)
|
||||||
|
_fourColorOverride.value = override
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleFourColorOverride() {
|
||||||
|
setFourColorOverride(!_fourColorOverride.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playBellSound() {
|
||||||
|
try {
|
||||||
|
val tg = android.media.ToneGenerator(android.media.AudioManager.STREAM_NOTIFICATION, 80)
|
||||||
|
tg.startTone(android.media.ToneGenerator.TONE_PROP_BEEP, 150)
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyModeSettings() {
|
||||||
|
val app = getApplication<Application>()
|
||||||
|
val c = client ?: return
|
||||||
|
val sb = c.screenBuffer
|
||||||
|
if (sb != null) {
|
||||||
|
sb.isEntryAssistDOCmode = AppSettings.getEntryAssistDocMode(app)
|
||||||
|
sb.isEntryAssistWordWrap = AppSettings.getEntryAssistWordWrap(app)
|
||||||
|
sb.setLeftMargin((AppSettings.getEntryAssistStartCol(app) - 1).coerceAtLeast(0))
|
||||||
|
sb.setRightMargin((AppSettings.getEntryAssistEndCol(app) - 1).coerceAtLeast(0))
|
||||||
|
sb.setWordTabPositions(AppSettings.getEntryAssistTabStopsArray(app))
|
||||||
|
}
|
||||||
|
val ip = c.inputProcessor
|
||||||
|
if (ip != null) {
|
||||||
|
ip.isBellEnabled = AppSettings.getEntryAssistBell(app)
|
||||||
|
ip.bellColumn = (AppSettings.getEntryAssistBellCol(app) - 1).coerceAtLeast(0)
|
||||||
|
ip.isInsertOffOnAid = AppSettings.getInsertOffOnAid(app)
|
||||||
|
ip.isNumericFieldLock = AppSettings.getNumericFieldLock(app)
|
||||||
|
ip.isAutoSkipEnabled = AppSettings.getAutoSkipEnabled(app)
|
||||||
|
}
|
||||||
|
_fourColorOverride.value = AppSettings.getFourColorOverride(app)
|
||||||
|
_isDocMode.value = c.isDocMode
|
||||||
|
_isWordWrap.value = c.isWordWrap
|
||||||
|
_isAplMode.value = c.isAplMode
|
||||||
|
_isInsertMode.value = c.isInsertMode
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadSettings() {
|
fun reloadSettings() {
|
||||||
val app = getApplication<Application>()
|
val app = getApplication<Application>()
|
||||||
_maskHiddenInput.value = AppSettings.isMaskHiddenInputEnabled(app)
|
_maskHiddenInput.value = AppSettings.isMaskHiddenInputEnabled(app)
|
||||||
@@ -205,6 +291,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_cursorStyle.value = AppSettings.getCursorStyle(app)
|
_cursorStyle.value = AppSettings.getCursorStyle(app)
|
||||||
_blockSelectMode.value = AppSettings.isBlockSelectModeEnabled(app)
|
_blockSelectMode.value = AppSettings.isBlockSelectModeEnabled(app)
|
||||||
_uiTheme.value = AppSettings.getUiTheme(app)
|
_uiTheme.value = AppSettings.getUiTheme(app)
|
||||||
|
_fourColorOverride.value = AppSettings.getFourColorOverride(app)
|
||||||
|
applyModeSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pasteLineWrap(text: String, endCol: Int, wordWrap: Boolean) {
|
fun pasteLineWrap(text: String, endCol: Int, wordWrap: Boolean) {
|
||||||
@@ -472,8 +560,48 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
is TerminalInputAction.ToggleInsert -> {
|
is TerminalInputAction.ToggleInsert -> {
|
||||||
if (!isNvt) {
|
if (!isNvt) {
|
||||||
ip.processToggleInsert()
|
c.toggleInsert()
|
||||||
_isInsertMode.value = ip.isInsertMode
|
_isInsertMode.value = c.isInsertMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.ToggleDocMode -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.toggleDocMode()
|
||||||
|
AppSettings.setEntryAssistDocMode(getApplication(), c.isDocMode)
|
||||||
|
_isDocMode.value = c.isDocMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.ToggleWordWrap -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.toggleWordWrap()
|
||||||
|
AppSettings.setEntryAssistWordWrap(getApplication(), c.isWordWrap)
|
||||||
|
_isWordWrap.value = c.isWordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.ToggleApl -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.toggleAplMode()
|
||||||
|
_isAplMode.value = c.isAplMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.SetDocMode -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.isDocMode = action.enabled
|
||||||
|
AppSettings.setEntryAssistDocMode(getApplication(), action.enabled)
|
||||||
|
_isDocMode.value = action.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.SetWordWrap -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.isWordWrap = action.enabled
|
||||||
|
AppSettings.setEntryAssistWordWrap(getApplication(), action.enabled)
|
||||||
|
_isWordWrap.value = action.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TerminalInputAction.SetAplMode -> {
|
||||||
|
if (!isNvt) {
|
||||||
|
c.isAplMode = action.enabled
|
||||||
|
_isAplMode.value = action.enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TerminalInputAction.SetCursor -> {
|
is TerminalInputAction.SetCursor -> {
|
||||||
@@ -769,6 +897,20 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_isKeyboardLocked.value = locked
|
_isKeyboardLocked.value = locked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newClient.inputProcessor.setBellListener {
|
||||||
|
playBellSound()
|
||||||
|
}
|
||||||
|
|
||||||
|
newClient.oia.registerOIAListener(object : haus.nightmare.lib3270j.ecl.ECLOIAListener {
|
||||||
|
override fun oiaChanged(event: haus.nightmare.lib3270j.ecl.ECLOIAEvent) {
|
||||||
|
_isInsertMode.value = newClient.isInsertMode
|
||||||
|
_isDocMode.value = newClient.isDocMode
|
||||||
|
_isWordWrap.value = newClient.isWordWrap
|
||||||
|
_isAplMode.value = newClient.isAplMode
|
||||||
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
newClient.addScreenUpdateListener(object : ScreenUpdateListener {
|
newClient.addScreenUpdateListener(object : ScreenUpdateListener {
|
||||||
override fun onScreenUpdated() {
|
override fun onScreenUpdated() {
|
||||||
val buf = newClient.screenBuffer
|
val buf = newClient.screenBuffer
|
||||||
@@ -779,6 +921,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_cursorAddress.value = buf.cursorAddress
|
_cursorAddress.value = buf.cursorAddress
|
||||||
}
|
}
|
||||||
_isKeyboardLocked.value = newClient.inputProcessor.isKeyboardLocked
|
_isKeyboardLocked.value = newClient.inputProcessor.isKeyboardLocked
|
||||||
|
_isInsertMode.value = newClient.isInsertMode
|
||||||
|
_isDocMode.value = newClient.isDocMode
|
||||||
|
_isWordWrap.value = newClient.isWordWrap
|
||||||
|
_isAplMode.value = newClient.isAplMode
|
||||||
_screenVersion.value = System.currentTimeMillis()
|
_screenVersion.value = System.currentTimeMillis()
|
||||||
|
|
||||||
// Drive CUT mode file transfers if active
|
// Drive CUT mode file transfers if active
|
||||||
@@ -804,6 +950,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
override fun onSoundAlarm() {
|
override fun onSoundAlarm() {
|
||||||
log.fine("Sound Alarm")
|
log.fine("Sound Alarm")
|
||||||
|
playBellSound()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -811,6 +958,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_cols.value = model.defaultCols
|
_cols.value = model.defaultCols
|
||||||
_screenBuffer.value = newClient.screenBuffer
|
_screenBuffer.value = newClient.screenBuffer
|
||||||
|
|
||||||
|
applyModeSettings()
|
||||||
|
|
||||||
newClient.connect()
|
newClient.connect()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
log.severe("Failed to connect: ${e.message}")
|
log.severe("Failed to connect: ${e.message}")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,19 @@ object AppSettings {
|
|||||||
private const val KEY_DYNAMIC_ROWS = "dynamic_rows"
|
private const val KEY_DYNAMIC_ROWS = "dynamic_rows"
|
||||||
private const val KEY_DYNAMIC_COLS = "dynamic_cols"
|
private const val KEY_DYNAMIC_COLS = "dynamic_cols"
|
||||||
|
|
||||||
|
// Entry Assist & Modes aligned with j3270
|
||||||
|
private const val KEY_ENTRY_ASSIST_DOC_MODE = "entry_assist_doc_mode"
|
||||||
|
private const val KEY_ENTRY_ASSIST_WORD_WRAP = "entry_assist_word_wrap"
|
||||||
|
private const val KEY_ENTRY_ASSIST_START_COL = "entry_assist_start_col"
|
||||||
|
private const val KEY_ENTRY_ASSIST_END_COL = "entry_assist_end_col"
|
||||||
|
private const val KEY_ENTRY_ASSIST_BELL = "entry_assist_bell"
|
||||||
|
private const val KEY_ENTRY_ASSIST_BELL_COL = "entry_assist_bell_col"
|
||||||
|
private const val KEY_ENTRY_ASSIST_TAB_STOPS = "entry_assist_tab_stops"
|
||||||
|
private const val KEY_INSERT_OFF_ON_AID = "insert_off_on_aid"
|
||||||
|
private const val KEY_FOUR_COLOR_OVERRIDE = "four_color_override"
|
||||||
|
private const val KEY_NUMERIC_FIELD_LOCK = "numeric_field_lock"
|
||||||
|
private const val KEY_AUTO_SKIP_ENABLED = "auto_skip_enabled"
|
||||||
|
|
||||||
enum class StartupBehavior {
|
enum class StartupBehavior {
|
||||||
SHOW_CONNECT,
|
SHOW_CONNECT,
|
||||||
DO_NOTHING,
|
DO_NOTHING,
|
||||||
@@ -155,6 +168,111 @@ object AppSettings {
|
|||||||
getPrefs(context).edit().putInt(KEY_DYNAMIC_COLS, cols.coerceIn(80, 255)).apply()
|
getPrefs(context).edit().putInt(KEY_DYNAMIC_COLS, cols.coerceIn(80, 255)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Entry Assist (Document Mode) ==========
|
||||||
|
fun getEntryAssistDocMode(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_ENTRY_ASSIST_DOC_MODE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistDocMode(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_ENTRY_ASSIST_DOC_MODE, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistWordWrap(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_ENTRY_ASSIST_WORD_WRAP, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistWordWrap(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_ENTRY_ASSIST_WORD_WRAP, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistStartCol(context: Context): Int {
|
||||||
|
return getPrefs(context).getInt(KEY_ENTRY_ASSIST_START_COL, 1).coerceIn(1, 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistStartCol(context: Context, col: Int) {
|
||||||
|
getPrefs(context).edit().putInt(KEY_ENTRY_ASSIST_START_COL, col.coerceIn(1, 80)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistEndCol(context: Context): Int {
|
||||||
|
return getPrefs(context).getInt(KEY_ENTRY_ASSIST_END_COL, 80).coerceIn(1, 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistEndCol(context: Context, col: Int) {
|
||||||
|
getPrefs(context).edit().putInt(KEY_ENTRY_ASSIST_END_COL, col.coerceIn(1, 80)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistBell(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_ENTRY_ASSIST_BELL, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistBell(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_ENTRY_ASSIST_BELL, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistBellCol(context: Context): Int {
|
||||||
|
return getPrefs(context).getInt(KEY_ENTRY_ASSIST_BELL_COL, 75).coerceIn(1, 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistBellCol(context: Context, col: Int) {
|
||||||
|
getPrefs(context).edit().putInt(KEY_ENTRY_ASSIST_BELL_COL, col.coerceIn(1, 80)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistTabStops(context: Context): String {
|
||||||
|
return getPrefs(context).getString(KEY_ENTRY_ASSIST_TAB_STOPS, "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75")
|
||||||
|
?: "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEntryAssistTabStops(context: Context, stops: String) {
|
||||||
|
getPrefs(context).edit().putString(KEY_ENTRY_ASSIST_TAB_STOPS, stops).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryAssistTabStopsArray(context: Context): IntArray {
|
||||||
|
val s = getEntryAssistTabStops(context)
|
||||||
|
if (s.isBlank()) return intArrayOf()
|
||||||
|
val parts = s.split(",")
|
||||||
|
val list = mutableListOf<Int>()
|
||||||
|
for (p in parts) {
|
||||||
|
val v = p.trim().toIntOrNull()
|
||||||
|
if (v != null && v > 0) {
|
||||||
|
list.add(v - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Terminal Operational Modes ==========
|
||||||
|
fun getInsertOffOnAid(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_INSERT_OFF_ON_AID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInsertOffOnAid(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_INSERT_OFF_ON_AID, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFourColorOverride(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_FOUR_COLOR_OVERRIDE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFourColorOverride(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_FOUR_COLOR_OVERRIDE, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNumericFieldLock(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_NUMERIC_FIELD_LOCK, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNumericFieldLock(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_NUMERIC_FIELD_LOCK, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAutoSkipEnabled(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_AUTO_SKIP_ENABLED, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutoSkipEnabled(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().putBoolean(KEY_AUTO_SKIP_ENABLED, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
// ========== INI Config Export / Import ==========
|
// ========== INI Config Export / Import ==========
|
||||||
|
|
||||||
fun exportToIni(context: Context, writer: Writer) {
|
fun exportToIni(context: Context, writer: Writer) {
|
||||||
@@ -186,6 +304,23 @@ object AppSettings {
|
|||||||
p.println("blockSelectMode = ${isBlockSelectModeEnabled(context)}")
|
p.println("blockSelectMode = ${isBlockSelectModeEnabled(context)}")
|
||||||
p.println()
|
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("[graphics]")
|
||||||
p.println("graphicsMode = ${getDefaultGraphicsMode(context)}")
|
p.println("graphicsMode = ${getDefaultGraphicsMode(context)}")
|
||||||
p.println()
|
p.println()
|
||||||
@@ -236,6 +371,25 @@ object AppSettings {
|
|||||||
"blockselectmode" -> setBlockSelectModeEnabled(context, value.toBoolean())
|
"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" -> {
|
"graphics" -> {
|
||||||
if (key.equals("graphicsmode", ignoreCase = true) || key.equals("mode", ignoreCase = true)) {
|
if (key.equals("graphicsmode", ignoreCase = true) || key.equals("mode", ignoreCase = true)) {
|
||||||
setDefaultGraphicsMode(context, value.uppercase(Locale.US))
|
setDefaultGraphicsMode(context, value.uppercase(Locale.US))
|
||||||
|
|||||||
@@ -37,6 +37,56 @@ private val TerminalKeyboardOptions = KeyboardOptions(
|
|||||||
keyboardType = KeyboardType.Ascii
|
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
|
@Composable
|
||||||
fun FileTransferDialog(
|
fun FileTransferDialog(
|
||||||
initialHostType: String = "TSO",
|
initialHostType: String = "TSO",
|
||||||
@@ -54,15 +104,17 @@ fun FileTransferDialog(
|
|||||||
else -> FTConfig.HostType.TSO
|
else -> FTConfig.HostType.TSO
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostFile by remember { mutableStateOf("") }
|
var hostFile by remember { mutableStateOf(lastTransferState?.hostFile ?: "") }
|
||||||
var localFile by remember { mutableStateOf("") }
|
var localFile by remember { mutableStateOf(lastTransferState?.localFile ?: "") }
|
||||||
var localDisplayName by remember { mutableStateOf("") }
|
var localDisplayName by remember { mutableStateOf("") }
|
||||||
var isReceive by remember { mutableStateOf(true) }
|
var isReceive by remember { mutableStateOf(lastTransferState?.isReceive ?: true) }
|
||||||
var isAscii by remember { mutableStateOf(true) }
|
var isAscii by remember { mutableStateOf(lastTransferState?.isAscii ?: true) }
|
||||||
var hostType by remember { mutableStateOf(initialTypeEnum) }
|
var hostType by remember { mutableStateOf(lastTransferState?.hostType ?: initialTypeEnum) }
|
||||||
var recfm by remember { mutableStateOf(FTConfig.RecordFormat.DEFAULT) }
|
var recfm by remember { mutableStateOf(lastTransferState?.recfm ?: FTConfig.RecordFormat.DEFAULT) }
|
||||||
var lreclStr by remember { mutableStateOf("") }
|
var lreclStr by remember { mutableStateOf(lastTransferState?.lreclStr ?: "") }
|
||||||
var blksizeStr by remember { mutableStateOf("") }
|
var blksizeStr by remember { mutableStateOf(lastTransferState?.blksizeStr ?: "") }
|
||||||
|
var spaceStr by remember { mutableStateOf(lastTransferState?.spaceStr ?: "") }
|
||||||
|
var cmsOptions by remember { mutableStateOf(lastTransferState?.cmsOptions ?: "") }
|
||||||
var overwrite by remember { mutableStateOf(true) }
|
var overwrite by remember { mutableStateOf(true) }
|
||||||
var showBrowseDialog by remember { mutableStateOf(false) }
|
var showBrowseDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -86,8 +138,7 @@ fun FileTransferDialog(
|
|||||||
localFile = tempFile.absolutePath
|
localFile = tempFile.absolutePath
|
||||||
localDisplayName = displayName
|
localDisplayName = displayName
|
||||||
if (hostFile.isBlank()) {
|
if (hostFile.isBlank()) {
|
||||||
val cleanBase = displayName.substringBeforeLast('.').replace(Regex("[^a-zA-Z0-9]"), "").uppercase()
|
hostFile = formatHostFilename(displayName, hostType)
|
||||||
hostFile = if (hostType == FTConfig.HostType.TSO) "'$cleanBase'" else "$cleanBase FILE A"
|
|
||||||
}
|
}
|
||||||
Toast.makeText(context, "Selected: $displayName", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Selected: $displayName", Toast.LENGTH_SHORT).show()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -143,8 +194,7 @@ fun FileTransferDialog(
|
|||||||
onSelectDataset = { selected ->
|
onSelectDataset = { selected ->
|
||||||
hostFile = selected
|
hostFile = selected
|
||||||
if (localFile.isBlank()) {
|
if (localFile.isBlank()) {
|
||||||
val base = selected.trim('\'').substringAfterLast('.').substringAfterLast(' ')
|
val safeName = formatLocalFilenameFromHost(selected, hostType)
|
||||||
val safeName = if (base.isNotBlank()) "$base.txt" else "download.txt"
|
|
||||||
val tempDir = File(context.cacheDir, "ft_download").apply { if (!exists()) mkdirs() }
|
val tempDir = File(context.cacheDir, "ft_download").apply { if (!exists()) mkdirs() }
|
||||||
val tempFile = File(tempDir, safeName)
|
val tempFile = File(tempDir, safeName)
|
||||||
localFile = tempFile.absolutePath
|
localFile = tempFile.absolutePath
|
||||||
@@ -361,8 +411,7 @@ fun FileTransferDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (isReceive) {
|
if (isReceive) {
|
||||||
val defName = if (hostFile.isNotBlank()) {
|
val defName = if (hostFile.isNotBlank()) {
|
||||||
val base = hostFile.trim('\'').substringAfterLast('.').substringAfterLast(' ')
|
formatLocalFilenameFromHost(hostFile, hostType)
|
||||||
if (base.isNotBlank()) "$base.txt" else "download.txt"
|
|
||||||
} else "download.txt"
|
} else "download.txt"
|
||||||
createDocumentLauncher.launch(defName)
|
createDocumentLauncher.launch(defName)
|
||||||
} else {
|
} else {
|
||||||
@@ -467,10 +516,35 @@ fun FileTransferDialog(
|
|||||||
onValueChange = { blksizeStr = it.filter { c -> c.isDigit() } },
|
onValueChange = { blksizeStr = it.filter { c -> c.isDigit() } },
|
||||||
label = { Text("BLKSIZE", fontSize = 11.sp) },
|
label = { Text("BLKSIZE", fontSize = 11.sp) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done),
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
setRecfm(recfm)
|
||||||
lreclStr.trim().toIntOrNull()?.let { setLrecl(it) }
|
lreclStr.trim().toIntOrNull()?.let { setLrecl(it) }
|
||||||
blksizeStr.trim().toIntOrNull()?.let { setBlksize(it) }
|
blksizeStr.trim().toIntOrNull()?.let { setBlksize(it) }
|
||||||
|
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)
|
onStartTransfer(config)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import haus.nightmare.lib3270j.ConnectionState
|
import haus.nightmare.lib3270j.ConnectionState
|
||||||
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
|
||||||
|
|
||||||
@@ -23,6 +25,11 @@ fun TwoRowKeyBar(
|
|||||||
connectionState: ConnectionState,
|
connectionState: ConnectionState,
|
||||||
isShiftPressed: Boolean = false,
|
isShiftPressed: Boolean = false,
|
||||||
isLightPenMode: Boolean = false,
|
isLightPenMode: Boolean = false,
|
||||||
|
isInsertMode: Boolean = false,
|
||||||
|
isDocMode: Boolean = false,
|
||||||
|
isWordWrap: Boolean = false,
|
||||||
|
isAplMode: Boolean = false,
|
||||||
|
fourColorOverride: Boolean = false,
|
||||||
hapticFeedbackEnabled: Boolean = true,
|
hapticFeedbackEnabled: Boolean = true,
|
||||||
onClearShift: () -> Unit = {},
|
onClearShift: () -> Unit = {},
|
||||||
onConnectClick: () -> Unit,
|
onConnectClick: () -> Unit,
|
||||||
@@ -34,6 +41,11 @@ fun TwoRowKeyBar(
|
|||||||
onScriptClick: () -> Unit = {},
|
onScriptClick: () -> Unit = {},
|
||||||
onPrinterSessionClick: () -> Unit = {},
|
onPrinterSessionClick: () -> Unit = {},
|
||||||
onToggleLightPen: () -> Unit = {},
|
onToggleLightPen: () -> Unit = {},
|
||||||
|
onToggleInsert: () -> Unit = {},
|
||||||
|
onToggleDocMode: () -> Unit = {},
|
||||||
|
onToggleWordWrap: () -> Unit = {},
|
||||||
|
onToggleAplMode: () -> Unit = {},
|
||||||
|
onToggleFourColorOverride: () -> Unit = {},
|
||||||
onSettingsClick: () -> Unit = {},
|
onSettingsClick: () -> Unit = {},
|
||||||
onSendAid: (Int) -> Unit,
|
onSendAid: (Int) -> Unit,
|
||||||
onReset: () -> Unit,
|
onReset: () -> Unit,
|
||||||
@@ -51,6 +63,11 @@ fun TwoRowKeyBar(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var menuExpanded by remember { mutableStateOf(false) }
|
var menuExpanded by remember { mutableStateOf(false) }
|
||||||
|
var lastDismissTime by remember { mutableLongStateOf(0L) }
|
||||||
|
|
||||||
|
BackHandler(enabled = menuExpanded) {
|
||||||
|
menuExpanded = false
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -73,11 +90,22 @@ fun TwoRowKeyBar(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
hapticFeedbackEnabled = hapticFeedbackEnabled,
|
hapticFeedbackEnabled = hapticFeedbackEnabled,
|
||||||
innerPaddingHorizontal = 0.dp,
|
innerPaddingHorizontal = 0.dp,
|
||||||
onClick = { menuExpanded = true }
|
onClick = {
|
||||||
|
if (System.currentTimeMillis() - lastDismissTime > 200L) {
|
||||||
|
menuExpanded = !menuExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = menuExpanded,
|
expanded = menuExpanded,
|
||||||
onDismissRequest = { menuExpanded = false },
|
onDismissRequest = {
|
||||||
|
menuExpanded = false
|
||||||
|
lastDismissTime = System.currentTimeMillis()
|
||||||
|
},
|
||||||
|
properties = PopupProperties(
|
||||||
|
focusable = false,
|
||||||
|
dismissOnClickOutside = true
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(Color(0xFF2C2D30))
|
.background(Color(0xFF2C2D30))
|
||||||
.heightIn(max = 420.dp)
|
.heightIn(max = 420.dp)
|
||||||
@@ -178,6 +206,42 @@ fun TwoRowKeyBar(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
HorizontalDivider(color = Color(0xFF3E4044))
|
HorizontalDivider(color = Color(0xFF3E4044))
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Insert Mode: ${if (isInsertMode) "ON" else "OFF"}", color = if (isInsertMode) Color(0xFF7890F0) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleInsert()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Document Mode (DOC): ${if (isDocMode) "ON" else "OFF"}", color = if (isDocMode) Color(0xFF38D9A9) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleDocMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Word Wrap: ${if (isWordWrap) "ON" else "OFF"}", color = if (isWordWrap) Color(0xFF38D9A9) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleWordWrap()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("APL Keyboard Mode: ${if (isAplMode) "ON" else "OFF"}", color = if (isAplMode) Color(0xFFFFA94D) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleAplMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Base 4-Color Override: ${if (fourColorOverride) "ON" else "OFF"}", color = if (fourColorOverride) Color(0xFFFFD43B) else Color.White) },
|
||||||
|
onClick = {
|
||||||
|
menuExpanded = false
|
||||||
|
onToggleFourColorOverride()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(color = Color(0xFF3E4044))
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package haus.nightmare.a3270.ui
|
package haus.nightmare.a3270.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -27,15 +28,29 @@ fun OiaStatusBar(
|
|||||||
isTls: Boolean = false,
|
isTls: Boolean = false,
|
||||||
isTlsVerified: Boolean = true,
|
isTlsVerified: Boolean = true,
|
||||||
graphicsMode: String = "NONE",
|
graphicsMode: String = "NONE",
|
||||||
codePage: String = "037",
|
|
||||||
isLightPenMode: Boolean = false,
|
isLightPenMode: Boolean = false,
|
||||||
isInsertMode: Boolean = false,
|
isInsertMode: Boolean = false,
|
||||||
|
isDocMode: Boolean = false,
|
||||||
|
isWordWrap: Boolean = false,
|
||||||
|
isAplMode: Boolean = false,
|
||||||
|
onToggleInsert: (() -> Unit)? = null,
|
||||||
|
onToggleDocMode: (() -> Unit)? = null,
|
||||||
|
onToggleWordWrap: (() -> Unit)? = null,
|
||||||
|
onToggleAplMode: (() -> Unit)? = null,
|
||||||
inhibitReason: Int = 0,
|
inhibitReason: Int = 0,
|
||||||
luName: String = "",
|
luName: String = "",
|
||||||
isNumericField: Boolean = false,
|
isNumericField: Boolean = false,
|
||||||
modelName: String = "",
|
uiTheme: String = "DARK",
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val isLight = uiTheme.equals("LIGHT", ignoreCase = true)
|
||||||
|
val barBg = if (isLight) Color(0xFFE9ECEF) else Color(0xFF161719)
|
||||||
|
val defaultTextClr = if (isLight) Color(0xFF212529) else Color.White
|
||||||
|
val sysAvailClr = if (isLight) Color(0xFF005AC8) else Color(0xFF7890F0)
|
||||||
|
val inputInhibitedClr = if (isLight) Color(0xFF141414) else Color(0xFFFFFFFF)
|
||||||
|
val attentionClr = if (isLight) Color(0xFFB46400) else Color(0xFFFFFF00)
|
||||||
|
val commCheckClr = if (isLight) Color(0xFFBE1414) else Color(0xFFFF0000)
|
||||||
|
|
||||||
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
|
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
|
||||||
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
|
||||||
val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr)
|
val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr)
|
||||||
@@ -66,24 +81,24 @@ fun OiaStatusBar(
|
|||||||
else -> "X LOCKED"
|
else -> "X LOCKED"
|
||||||
}
|
}
|
||||||
val clr = when (inhibitReason) {
|
val clr = when (inhibitReason) {
|
||||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> Color(0xFFFF0000)
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> commCheckClr
|
||||||
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> Color(0xFFFFFF00)
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> attentionClr
|
||||||
else -> Color(0xFFFFFFFF)
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NUMERIC_ONLY -> attentionClr
|
||||||
|
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OVERFLOW -> attentionClr
|
||||||
|
else -> inputInhibitedClr
|
||||||
}
|
}
|
||||||
txt to clr
|
txt to clr
|
||||||
} else if (isKeyboardLocked) {
|
} else if (isKeyboardLocked) {
|
||||||
"X SYSTEM" to Color(0xFFFFFFFF)
|
"X SYSTEM" to inputInhibitedClr
|
||||||
} else if (isInsertMode) {
|
|
||||||
"INSERT" to Color(0xFF7890F0)
|
|
||||||
} else {
|
} else {
|
||||||
"READY" to Color(0xFF7890F0)
|
"READY" to sysAvailClr
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(26.dp)
|
.height(26.dp)
|
||||||
.background(Color(0xFF161719))
|
.background(barBg)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
@@ -101,7 +116,7 @@ fun OiaStatusBar(
|
|||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = hostText,
|
text = hostText,
|
||||||
color = Color.White,
|
color = defaultTextClr,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -148,24 +163,6 @@ fun OiaStatusBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodePage Badge
|
|
||||||
if (connectionState.isConnected() && codePage.isNotBlank()) {
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Surface(
|
|
||||||
color = Color(0xFFFA5252).copy(alpha = 0.2f),
|
|
||||||
shape = RoundedCornerShape(4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "CP$codePage",
|
|
||||||
color = Color(0xFFFF8787),
|
|
||||||
fontSize = 9.sp,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field Status: NUM vs ALPHA
|
// Field Status: NUM vs ALPHA
|
||||||
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
@@ -184,24 +181,6 @@ fun OiaStatusBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model / Dimensions Badge
|
|
||||||
if (connectionState.isConnected() && rows > 0 && cols > 0) {
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Surface(
|
|
||||||
color = Color(0xFF495057).copy(alpha = 0.3f),
|
|
||||||
shape = RoundedCornerShape(4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (modelName.isNotBlank()) modelName else "${rows}x${cols}",
|
|
||||||
color = Color(0xFFCED4DA),
|
|
||||||
fontSize = 9.sp,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Light Pen Badge
|
// Light Pen Badge
|
||||||
if (isLightPenMode) {
|
if (isLightPenMode) {
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
@@ -257,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)
|
// Terminal Keyboard Lock Status Badge (X SYSTEM vs READY)
|
||||||
Surface(
|
Surface(
|
||||||
color = lockStatusColor.copy(alpha = 0.2f),
|
color = lockStatusColor.copy(alpha = 0.2f),
|
||||||
@@ -276,7 +327,7 @@ fun OiaStatusBar(
|
|||||||
// Cursor Position & Buffer Address
|
// Cursor Position & Buffer Address
|
||||||
Text(
|
Text(
|
||||||
text = posStr,
|
text = posStr,
|
||||||
color = Color(0xFF22B8CF),
|
color = if (isLight) Color(0xFF0C8599) else Color(0xFF22B8CF),
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ fun SettingsDialog(
|
|||||||
initialStartupBehavior: String = "SHOW_CONNECT",
|
initialStartupBehavior: String = "SHOW_CONNECT",
|
||||||
initialDynamicRows: Int = 62,
|
initialDynamicRows: Int = 62,
|
||||||
initialDynamicCols: Int = 160,
|
initialDynamicCols: Int = 160,
|
||||||
|
initialEntryAssistDocMode: Boolean = false,
|
||||||
|
initialEntryAssistWordWrap: Boolean = false,
|
||||||
|
initialEntryAssistStartCol: Int = 1,
|
||||||
|
initialEntryAssistEndCol: Int = 80,
|
||||||
|
initialEntryAssistBell: Boolean = false,
|
||||||
|
initialEntryAssistBellCol: Int = 75,
|
||||||
|
initialEntryAssistTabStops: String = "5,10,15,20,25,30,35,40,45,50,55,60,65,70,75",
|
||||||
|
initialInsertOffOnAid: Boolean = true,
|
||||||
|
initialFourColorOverride: Boolean = false,
|
||||||
|
initialNumericFieldLock: Boolean = true,
|
||||||
|
initialAutoSkipEnabled: Boolean = true,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSave: (
|
onSave: (
|
||||||
maskHiddenInput: Boolean,
|
maskHiddenInput: Boolean,
|
||||||
@@ -47,7 +58,18 @@ fun SettingsDialog(
|
|||||||
blockSelectMode: Boolean,
|
blockSelectMode: Boolean,
|
||||||
startupBehavior: String,
|
startupBehavior: String,
|
||||||
dynamicRows: Int,
|
dynamicRows: Int,
|
||||||
dynamicCols: Int
|
dynamicCols: Int,
|
||||||
|
entryAssistDocMode: Boolean,
|
||||||
|
entryAssistWordWrap: Boolean,
|
||||||
|
entryAssistStartCol: Int,
|
||||||
|
entryAssistEndCol: Int,
|
||||||
|
entryAssistBell: Boolean,
|
||||||
|
entryAssistBellCol: Int,
|
||||||
|
entryAssistTabStops: String,
|
||||||
|
insertOffOnAid: Boolean,
|
||||||
|
fourColorOverride: Boolean,
|
||||||
|
numericFieldLock: Boolean,
|
||||||
|
autoSkipEnabled: Boolean
|
||||||
) -> Unit
|
) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
@@ -68,6 +90,18 @@ fun SettingsDialog(
|
|||||||
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
|
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
|
||||||
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
|
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
|
||||||
|
|
||||||
|
var entryAssistDocMode by remember { mutableStateOf(initialEntryAssistDocMode) }
|
||||||
|
var entryAssistWordWrap by remember { mutableStateOf(initialEntryAssistWordWrap) }
|
||||||
|
var entryAssistStartColStr by remember { mutableStateOf(initialEntryAssistStartCol.toString()) }
|
||||||
|
var entryAssistEndColStr by remember { mutableStateOf(initialEntryAssistEndCol.toString()) }
|
||||||
|
var entryAssistBell by remember { mutableStateOf(initialEntryAssistBell) }
|
||||||
|
var entryAssistBellColStr by remember { mutableStateOf(initialEntryAssistBellCol.toString()) }
|
||||||
|
var entryAssistTabStops by remember { mutableStateOf(initialEntryAssistTabStops) }
|
||||||
|
var insertOffOnAid by remember { mutableStateOf(initialInsertOffOnAid) }
|
||||||
|
var fourColorOverride by remember { mutableStateOf(initialFourColorOverride) }
|
||||||
|
var numericFieldLock by remember { mutableStateOf(initialNumericFieldLock) }
|
||||||
|
var autoSkipEnabled by remember { mutableStateOf(initialAutoSkipEnabled) }
|
||||||
|
|
||||||
var showImportDialog by remember { mutableStateOf(false) }
|
var showImportDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
@@ -416,7 +450,207 @@ fun SettingsDialog(
|
|||||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// SECTION 4: INI CONFIGURATION BACKUP
|
// SECTION 4: ENTRY ASSIST & MODES
|
||||||
|
Text("Entry Assist & Modes", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
|
Text("Document Mode word wrap, margins, audible bell, and operational locks matching j3270.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Document Mode
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Document Mode (DOC)", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Enable word wrapping and margin tracking during text entry.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = entryAssistDocMode,
|
||||||
|
onCheckedChange = { entryAssistDocMode = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Word Wrap
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Word Wrap (V)", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Automatically wrap partial words at the right margin.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = entryAssistWordWrap,
|
||||||
|
onCheckedChange = { entryAssistWordWrap = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Margins (Left / Right)
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Entry Assist Margins", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Left margin (1-80) and Right margin (1-80).", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entryAssistStartColStr,
|
||||||
|
onValueChange = { entryAssistStartColStr = it },
|
||||||
|
label = { Text("Left Col (1-80)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entryAssistEndColStr,
|
||||||
|
onValueChange = { entryAssistEndColStr = it },
|
||||||
|
label = { Text("Right Col (1-80)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Audible EOL Bell
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Audible EOL Bell Warning", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Beep warning signal when typing reaches the bell column.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = entryAssistBell,
|
||||||
|
onCheckedChange = { entryAssistBell = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entryAssistBellColStr,
|
||||||
|
onValueChange = { entryAssistBellColStr = it },
|
||||||
|
label = { Text("Bell Column (1-80)") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||||
|
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Tab Stops
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entryAssistTabStops,
|
||||||
|
onValueChange = { entryAssistTabStops = it },
|
||||||
|
label = { Text("Word Tab Stops") },
|
||||||
|
placeholder = { Text("5,10,15,20,25,30,35,40,45,50,55,60,65,70,75") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Operational Mode Toggles
|
||||||
|
// 1. Reset Insert on AID
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Reset Insert Mode on AID Key", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Automatically cancel insert mode upon Enter, PF, PA, or Clear.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = insertOffOnAid,
|
||||||
|
onCheckedChange = { insertOffOnAid = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// 2. Base 4-Color Override
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Base 4-Color Override Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Force 3279 4-color mapping (green/white/red/turquoise), ignoring host extended colors.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = fourColorOverride,
|
||||||
|
onCheckedChange = { fourColorOverride = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// 3. Numeric Field Lock
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Numeric Field Lock (-NUMERIC)", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Inhibit keyboard if a non-numeric character is typed in a numeric-only field.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = numericFieldLock,
|
||||||
|
onCheckedChange = { numericFieldLock = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// 4. Auto-Skip
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
|
Text("Auto-Skip Across Fields", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||||
|
Text("Automatically advance cursor to next unprotected field when field is filled.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = autoSkipEnabled,
|
||||||
|
onCheckedChange = { autoSkipEnabled = it },
|
||||||
|
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// SECTION 5: INI CONFIGURATION BACKUP
|
||||||
Text("Configuration File (INI)", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
Text("Configuration File (INI)", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
|
||||||
Text("Compatible with j3270 desktop INI configuration files.", fontSize = 12.sp, color = Color.LightGray)
|
Text("Compatible with j3270 desktop INI configuration files.", fontSize = 12.sp, color = Color.LightGray)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -459,6 +693,9 @@ fun SettingsDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
val dRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
|
val dRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
|
||||||
val dCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
|
val dCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
|
||||||
|
val eaStartCol = entryAssistStartColStr.trim().toIntOrNull()?.coerceIn(1, 80) ?: 1
|
||||||
|
val eaEndCol = entryAssistEndColStr.trim().toIntOrNull()?.coerceIn(1, 80) ?: 80
|
||||||
|
val eaBellCol = entryAssistBellColStr.trim().toIntOrNull()?.coerceIn(1, 80) ?: 75
|
||||||
onSave(
|
onSave(
|
||||||
maskHiddenInput,
|
maskHiddenInput,
|
||||||
cursorBlink,
|
cursorBlink,
|
||||||
@@ -472,7 +709,18 @@ fun SettingsDialog(
|
|||||||
blockSelectMode,
|
blockSelectMode,
|
||||||
startupBehavior,
|
startupBehavior,
|
||||||
dRows,
|
dRows,
|
||||||
dCols
|
dCols,
|
||||||
|
entryAssistDocMode,
|
||||||
|
entryAssistWordWrap,
|
||||||
|
eaStartCol,
|
||||||
|
eaEndCol,
|
||||||
|
entryAssistBell,
|
||||||
|
eaBellCol,
|
||||||
|
entryAssistTabStops,
|
||||||
|
insertOffOnAid,
|
||||||
|
fourColorOverride,
|
||||||
|
numericFieldLock,
|
||||||
|
autoSkipEnabled
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
@@ -517,6 +765,17 @@ fun SettingsDialog(
|
|||||||
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
|
||||||
defaultCodePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)
|
defaultCodePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)
|
||||||
defaultGraphicsMode = haus.nightmare.a3270.storage.AppSettings.getDefaultGraphicsMode(context)
|
defaultGraphicsMode = haus.nightmare.a3270.storage.AppSettings.getDefaultGraphicsMode(context)
|
||||||
|
entryAssistDocMode = haus.nightmare.a3270.storage.AppSettings.getEntryAssistDocMode(context)
|
||||||
|
entryAssistWordWrap = haus.nightmare.a3270.storage.AppSettings.getEntryAssistWordWrap(context)
|
||||||
|
entryAssistStartColStr = haus.nightmare.a3270.storage.AppSettings.getEntryAssistStartCol(context).toString()
|
||||||
|
entryAssistEndColStr = haus.nightmare.a3270.storage.AppSettings.getEntryAssistEndCol(context).toString()
|
||||||
|
entryAssistBell = haus.nightmare.a3270.storage.AppSettings.getEntryAssistBell(context)
|
||||||
|
entryAssistBellColStr = haus.nightmare.a3270.storage.AppSettings.getEntryAssistBellCol(context).toString()
|
||||||
|
entryAssistTabStops = haus.nightmare.a3270.storage.AppSettings.getEntryAssistTabStops(context)
|
||||||
|
insertOffOnAid = haus.nightmare.a3270.storage.AppSettings.getInsertOffOnAid(context)
|
||||||
|
fourColorOverride = haus.nightmare.a3270.storage.AppSettings.getFourColorOverride(context)
|
||||||
|
numericFieldLock = haus.nightmare.a3270.storage.AppSettings.getNumericFieldLock(context)
|
||||||
|
autoSkipEnabled = haus.nightmare.a3270.storage.AppSettings.getAutoSkipEnabled(context)
|
||||||
android.widget.Toast.makeText(context, "Settings imported from INI", android.widget.Toast.LENGTH_SHORT).show()
|
android.widget.Toast.makeText(context, "Settings imported from INI", android.widget.Toast.LENGTH_SHORT).show()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.widget.Toast.makeText(context, "Error importing: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
|
android.widget.Toast.makeText(context, "Error importing: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
|
||||||
|
|||||||
@@ -111,4 +111,10 @@ class TerminalInputView(context: Context) : View(context) {
|
|||||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideSoftKeyboard() {
|
||||||
|
clearFocus()
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ fun TerminalView(
|
|||||||
cursorStyle: String = "BLOCK",
|
cursorStyle: String = "BLOCK",
|
||||||
isInsertMode: Boolean = false,
|
isInsertMode: Boolean = false,
|
||||||
blockSelectMode: Boolean = false,
|
blockSelectMode: Boolean = false,
|
||||||
|
fourColorOverride: Boolean = false,
|
||||||
searchHighlightAddr: Int = -1,
|
searchHighlightAddr: Int = -1,
|
||||||
searchHighlightLen: Int = 0,
|
searchHighlightLen: Int = 0,
|
||||||
onTapAddress: (Int) -> Unit,
|
onTapAddress: (Int) -> Unit,
|
||||||
@@ -256,11 +257,11 @@ fun TerminalView(
|
|||||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||||
val gridW = metrics.gridWidth.toInt()
|
val gridW = metrics.gridWidth.toInt()
|
||||||
val gridH = metrics.gridHeight.toInt()
|
val gridH = metrics.gridHeight.toInt()
|
||||||
if (gridW > 0 && gridH > 0) {
|
val gWidth = graphicsPlane.canvasWidth
|
||||||
graphicsPlane.resize(gridW, gridH)
|
val gHeight = graphicsPlane.canvasHeight
|
||||||
val rgb = graphicsPlane.rgbBuffer
|
val rgb = graphicsPlane.rgbBuffer
|
||||||
if (rgb != null) {
|
if (gridW > 0 && gridH > 0 && rgb != null && gWidth > 0 && gHeight > 0) {
|
||||||
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
|
val bmp = android.graphics.Bitmap.createBitmap(rgb, gWidth, gHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||||
bmp,
|
bmp,
|
||||||
null,
|
null,
|
||||||
@@ -269,7 +270,6 @@ fun TerminalView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Draw Crosshair Ruler if enabled
|
// 2. Draw Crosshair Ruler if enabled
|
||||||
if (crosshairRuler && cols > 0 && rows > 0) {
|
if (crosshairRuler && cols > 0 && rows > 0) {
|
||||||
@@ -320,7 +320,7 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute 3270 color and field intensity
|
// Compute 3270 color and field intensity
|
||||||
fgColor = getFgColorForAttribute(ea, currentFieldEa, currentFA)
|
fgColor = getFgColorForAttribute(ea, currentFieldEa, currentFA, fourColorOverride)
|
||||||
bgColor = getBgColorForAttribute(ea, currentFieldEa)
|
bgColor = getBgColorForAttribute(ea, currentFieldEa)
|
||||||
|
|
||||||
csVal = if (ea.cs != 0.toByte()) (ea.cs.toInt() and 0xFF)
|
csVal = if (ea.cs != 0.toByte()) (ea.cs.toInt() and 0xFF)
|
||||||
@@ -728,7 +728,13 @@ private fun copySelection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFgColorForAttribute(ea: ExtendedAttribute, currentFieldEa: ExtendedAttribute?, currentFA: Byte): Color {
|
private 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)
|
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 if (currentFieldEa != null && currentFieldEa.fg != 0.toByte()) (currentFieldEa.fg.toInt() and 0xFF)
|
||||||
else 0
|
else 0
|
||||||
@@ -736,6 +742,7 @@ private fun getFgColorForAttribute(ea: ExtendedAttribute, currentFieldEa: Extend
|
|||||||
if (fg in 0xf0..0xff) {
|
if (fg in 0xf0..0xff) {
|
||||||
return HOST_COLORS[fg - 0xf0]
|
return HOST_COLORS[fg - 0xf0]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val fa = currentFA.toInt() and 0xFF
|
val fa = currentFA.toInt() and 0xFF
|
||||||
return if (faIsProtected(fa)) {
|
return if (faIsProtected(fa)) {
|
||||||
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_WHITE] else HOST_COLORS[HOST_COLOR_TURQUOISE]
|
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_WHITE] else HOST_COLORS[HOST_COLOR_TURQUOISE]
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user